Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa580c516e | |||
| 7f4c450553 | |||
| 761ff7ed5a | |||
| 117d767f05 | |||
| 8405bfe6f9 | |||
| ccdb1479f7 | |||
| c8f68f44af | |||
| 3954a555f8 | |||
| b6934922fa | |||
| 944e6f5569 | |||
| d51b36e80d | |||
| c9724e2024 | |||
| 830e476120 | |||
| fe2e372997 | |||
| a15deedf0d | |||
| 22bf8163cd | |||
| b8390331af | |||
| 47b740ff35 | |||
| 39c14e6556 | |||
| 57cd791348 | |||
| 3c612e284e | |||
| 8cd1ab5c8f | |||
| a6c22cadb8 | |||
| a628ecf72b | |||
| 8d70233d83 | |||
| ae89600201 | |||
| 934d43b525 | |||
| 858c04bacf | |||
| 2a5355b1f8 | |||
| 5cf2ac4c3e | |||
| 71173da5ad | |||
| e304f4f34f | |||
| 7d37f645ba | |||
| c50738005d | |||
| effff6f88d | |||
| 45b223a2ef | |||
| 90544ba713 | |||
| e55c2e9598 | |||
| 6ee52474e1 | |||
| 4bf9f0b96c | |||
| 79e2fa89df | |||
| 2ad0ded73f | |||
| 1ab05e5c3b | |||
| 4b4a1644ff | |||
| 7fd0ec8ce6 | |||
| 2d1e08b50e | |||
| b881c52118 | |||
| 6fb59949a2 | |||
| 7d41dc21c1 | |||
| 6365968dc3 | |||
| 1abb3c8c22 | |||
| 33f4bb45d1 | |||
| 4897994b35 | |||
| 0a773c82af | |||
| b34d970076 | |||
| 19cf781431 | |||
| 637e65e5a0 | |||
| b3f83fd363 | |||
| 02ac3a6814 | |||
| 97891d36ab | |||
| 65c87d5e0f | |||
| ae3b53540e | |||
| 0e9009b0de | |||
| be2864c34b | |||
| c9bdac2e03 | |||
| 040de3d973 | |||
| 1703380ebc | |||
| e935885cd3 | |||
| da809bb9d7 | |||
| c39c9aa1da | |||
| ad61662cc4 | |||
| e42bcd0115 | |||
| ad8c025393 | |||
| 1b0db3c8b0 | |||
| f9a8c1969c | |||
| 645c11f0bd | |||
| ece49a158e | |||
| b139b8fdd6 | |||
| b14aa4f0dc | |||
| 9b392a22e1 | |||
| 36547a7343 | |||
| 876390aa68 | |||
| 297ecfbae3 | |||
| eeb0012e7f | |||
| 35cf82f11c | |||
| 82f6c2c550 | |||
| 3e3988a67f | |||
| 2f4694dc95 | |||
| f072dab07b | |||
| fc02e6f4a5 | |||
| 0651a09a3c | |||
| 2c5f1e0417 | |||
| c9682ca64d | |||
| bceb024588 | |||
| 17bba4d4a2 | |||
| 485448cbc7 | |||
| 244dad447b | |||
| 22e63a7367 | |||
| 83907132b5 | |||
| 7dc9beb171 | |||
| 0664e46a4b | |||
| 2ca97a42c5 | |||
| 773e415dff | |||
| 9e673559c4 | |||
| 879ef603fe | |||
| 7e0a163f12 | |||
| 8e4088e08f | |||
| 59161c663b | |||
| 93252fc5d2 | |||
| e4b8d1807d | |||
| 33e0ccdd10 | |||
| d59139a2ab | |||
| df831833b1 | |||
| c065db6da1 | |||
| a55be809f3 | |||
| a9e1ebc0a8 | |||
| 55af09a350 | |||
| 199fdd6728 | |||
| 4035e91672 | |||
| bc9194d740 | |||
| f601c47218 | |||
| 066d559377 | |||
| 2c3219ffcb | |||
| cf88bf9c23 | |||
| b8303b9a22 | |||
| a3f084dcde | |||
| 0d6b8fc6fc | |||
| 261a936bb8 | |||
| 159d9425a7 | |||
| 3a50b3678d | |||
| 6fa352f407 | |||
| 4b80b2c233 | |||
| d881755503 | |||
| fd125ecc68 | |||
| 29f7f1a57d | |||
| 8ecaabfce9 | |||
| 1797ff67c0 | |||
| f1ba5e95ec | |||
| d8c0f9d1d9 | |||
| df4b5fc87d | |||
| d7cdc8b3b0 | |||
| 5b53ca7cf1 | |||
| 194d1dae51 | |||
| 61322ede6c | |||
| a8edaedc8b | |||
| 25145f72e5 | |||
| 3ddf8b5922 | |||
| dbe9e4aade | |||
| 715be4dad0 | |||
| 570b7d0d97 | |||
| 80ac0ab17f | |||
| 9ee8174d5f | |||
| 831aa03c9f | |||
| d372597bdb | |||
| 172437b6fc | |||
| 7640a42bfc | |||
| fde04bd625 | |||
| ad14a5ccba | |||
| 2348d12e9d | |||
| 5cafc05e13 | |||
| e982257271 | |||
| 340fd81778 | |||
| 223f94077f | |||
| f13aa21d0f | |||
| 2c34a17d88 | |||
| 6b005a666e | |||
| 1d1bcb0a63 | |||
| 3f5f1328e7 | |||
| 8cca8decde | |||
| be5bbd3b9b | |||
| 3f94a754e4 | |||
| 780f378fb1 | |||
| b874c17bcb | |||
| 16e4831499 | |||
| 9d709f0db8 | |||
| a8d394efd7 | |||
| 95a5283c86 | |||
| ef7d898747 | |||
| 388c408080 | |||
| 7b77e41253 | |||
| c0bfebf3a4 | |||
| 6f9f1c3a35 | |||
| 8128edad43 | |||
| eb8a13d8c2 | |||
| 8399edce6a | |||
| 2311d5eabe | |||
| afc8f4fdf6 | |||
| 66de2f91b6 | |||
| bd88695e59 | |||
| 23e8f7e0aa | |||
| d559ec0208 | |||
| ed99025bd6 | |||
| 57d48f53e0 | |||
| 68fa42249e | |||
| c5bc761a52 | |||
| 3762bdbccd | |||
| c81caa4d2c | |||
| 13dd3084c2 | |||
| e1021a96af | |||
| 5b0781253f | |||
| a04b7eed28 | |||
| c47427633c | |||
| 56e2c6650d | |||
| 82f0fb8a79 | |||
| 0e5b293b1f | |||
| eaae7aee39 | |||
| a4885c2c3a | |||
| f5aaee006e | |||
| db6745e8ff | |||
| ba34855602 | |||
| e6fa97c738 | |||
| 5b481a27c6 | |||
| bdc7ff1035 | |||
| da5f060741 | |||
| a56d335380 | |||
| d8aed552bc | |||
| d7286fa06e | |||
| 906f554d74 | |||
| cb44d5431a | |||
| a69eb8a66e | |||
| 1b411b1fed | |||
| 5d57959608 | |||
| 31e57c2ff8 | |||
| 734393d638 | |||
| 96504e2fb0 | |||
| ecfe802065 | |||
| 1ac9d54dab | |||
| 72d7e8aaaa | |||
| 0395696866 | |||
| 0667683e4d | |||
| aca0781c4b | |||
| ac798d9d6d | |||
| b389d0eb9c | |||
| e46fc13fea | |||
| bce0b4a8a0 | |||
| bf303ed471 | |||
| 562046c278 | |||
| 4cc28977cb | |||
| 3ce4624aee | |||
| 2b8ced9c59 |
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with: { go-version: '1.22' }
|
||||
with: { go-version: '1.24' }
|
||||
|
||||
- name: Build go2rtc_win64
|
||||
env: { GOOS: windows, GOARCH: amd64 }
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win32
|
||||
env: { GOOS: windows, GOARCH: 386 }
|
||||
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win32
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_amd64
|
||||
env: { GOOS: darwin, GOARCH: amd64 }
|
||||
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_amd64
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -102,14 +102,14 @@ jobs:
|
||||
env: { GOOS: freebsd, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_amd64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_freebsd_arm64
|
||||
env: { GOOS: freebsd, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
|
||||
|
||||
docker-master:
|
||||
@@ -159,6 +159,7 @@ jobs:
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/386
|
||||
linux/arm/v6
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Build Go binary
|
||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
go2rtc.yaml
|
||||
go2rtc.json
|
||||
|
||||
go2rtc_freebsd*
|
||||
go2rtc_linux*
|
||||
go2rtc_mac*
|
||||
go2rtc_win*
|
||||
|
||||
+15
-16
@@ -2,14 +2,19 @@
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.22"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
||||
ARG GO_VERSION="1.24"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
# 1. Download ngrok binary (for support arm/v6)
|
||||
FROM alpine AS ngrok
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz /
|
||||
RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin
|
||||
|
||||
|
||||
# 2. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
@@ -30,21 +35,14 @@ COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||
# font-droid for FFmpeg drawtext filter (+2MB)
|
||||
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
|
||||
RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
|
||||
|
||||
# Hardware Acceleration for Intel CPU (+50MB)
|
||||
ARG TARGETARCH
|
||||
@@ -56,7 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
||||
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||
|
||||
COPY --from=rootfs / /
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
VOLUME /config
|
||||
|
||||
@@ -115,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
|
||||
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
||||
|
||||
- `go2rtc_win64.zip` - Windows 64-bit
|
||||
- `go2rtc_win32.zip` - Windows 32-bit
|
||||
- `go2rtc_win64.zip` - Windows 10+ 64-bit
|
||||
- `go2rtc_win32.zip` - Windows 7+ 32-bit
|
||||
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
||||
- `go2rtc_linux_amd64` - Linux 64-bit
|
||||
- `go2rtc_linux_i386` - Linux 32-bit
|
||||
@@ -124,8 +124,10 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
||||
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
||||
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
|
||||
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
|
||||
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
|
||||
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
||||
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
|
||||
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
|
||||
- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit
|
||||
- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit
|
||||
|
||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
@@ -170,7 +172,7 @@ Available modules:
|
||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||
- [webrtc](#module-webrtc) - WebRTC Server
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server
|
||||
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||
@@ -231,7 +233,7 @@ streams:
|
||||
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||
dahua_camera:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||
amcrest_doorbell:
|
||||
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
|
||||
@@ -241,7 +243,7 @@ streams:
|
||||
**Recommendations**
|
||||
|
||||
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
||||
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
|
||||
- **Dahua Doorbell** users may want to change [audio codec](https://github.com/AlexxIT/go2rtc/issues/49#issuecomment-2127107379) for proper 2-way audio. Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio
|
||||
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
|
||||
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
|
||||
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
|
||||
@@ -350,7 +352,7 @@ streams:
|
||||
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
|
||||
|
||||
# [RTSP] video with rotation, should be transcoded, so select H264
|
||||
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||
rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||
```
|
||||
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
@@ -648,10 +650,11 @@ This source type support Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||
- Roborock S7 MaxV - video and two way audio
|
||||
- Roborock Qrevo MaxV - video and two way audio
|
||||
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link.
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
@@ -679,13 +682,18 @@ Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC proto
|
||||
|
||||
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
||||
|
||||
**switchbot**
|
||||
|
||||
Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
|
||||
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
|
||||
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
|
||||
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
||||
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
||||
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
|
||||
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
|
||||
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
|
||||
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
||||
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
||||
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}]
|
||||
```
|
||||
|
||||
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
|
||||
@@ -880,7 +888,7 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format.
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Init()
|
||||
streams.Init()
|
||||
|
||||
api.Init()
|
||||
ws.Init()
|
||||
|
||||
ffmpeg.Init()
|
||||
mjpeg.Init()
|
||||
v4l2.Init()
|
||||
|
||||
shell.RunUntilSignal()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
## Example
|
||||
|
||||
```shell
|
||||
go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/onvif"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var rawURL = os.Args[1]
|
||||
var operation = os.Args[2]
|
||||
var token string
|
||||
if len(os.Args) > 3 {
|
||||
token = os.Args[3]
|
||||
}
|
||||
|
||||
client, err := onvif.NewClient(rawURL)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
switch operation {
|
||||
case onvif.ServiceGetServiceCapabilities:
|
||||
b, err = client.MediaRequest(operation)
|
||||
case onvif.DeviceGetCapabilities,
|
||||
onvif.DeviceGetDeviceInformation,
|
||||
onvif.DeviceGetDiscoveryMode,
|
||||
onvif.DeviceGetDNS,
|
||||
onvif.DeviceGetHostname,
|
||||
onvif.DeviceGetNetworkDefaultGateway,
|
||||
onvif.DeviceGetNetworkInterfaces,
|
||||
onvif.DeviceGetNetworkProtocols,
|
||||
onvif.DeviceGetNTP,
|
||||
onvif.DeviceGetScopes,
|
||||
onvif.DeviceGetServices,
|
||||
onvif.DeviceGetSystemDateAndTime,
|
||||
onvif.DeviceSystemReboot:
|
||||
b, err = client.DeviceRequest(operation)
|
||||
case onvif.MediaGetProfiles,
|
||||
onvif.MediaGetVideoEncoderConfigurations,
|
||||
onvif.MediaGetVideoSources,
|
||||
onvif.MediaGetVideoSourceConfigurations,
|
||||
onvif.MediaGetAudioEncoderConfigurations,
|
||||
onvif.MediaGetAudioSources,
|
||||
onvif.MediaGetAudioSourceConfigurations:
|
||||
b, err = client.MediaRequest(operation)
|
||||
case onvif.MediaGetProfile:
|
||||
b, err = client.GetProfile(token)
|
||||
case onvif.MediaGetVideoSourceConfiguration:
|
||||
b, err = client.GetVideoSourceConfiguration(token)
|
||||
case onvif.MediaGetStreamUri:
|
||||
b, err = client.GetStreamUri(token)
|
||||
case onvif.MediaGetSnapshotUri:
|
||||
b, err = client.GetSnapshotUri(token)
|
||||
default:
|
||||
log.Printf("unknown action\n")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.WriteFile(u.Hostname()+"_"+operation+".xml", b, 0644); err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,50 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.22
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.59
|
||||
github.com/pion/ice/v2 v2.3.24
|
||||
github.com/pion/interceptor v0.1.29
|
||||
github.com/pion/rtcp v1.2.14
|
||||
github.com/pion/rtp v1.8.6
|
||||
github.com/pion/sdp/v3 v3.0.9
|
||||
github.com/pion/srtp/v2 v2.0.18
|
||||
github.com/miekg/dns v1.1.63
|
||||
github.com/pion/ice/v2 v2.3.37
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.11
|
||||
github.com/pion/sdp/v3 v3.0.10
|
||||
github.com/pion/srtp/v2 v2.0.20
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.2.40
|
||||
github.com/pion/webrtc/v3 v3.3.5
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/asticode/go-astikit v0.52.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/pion/datachannel v1.5.6 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.11 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.16 // indirect
|
||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||
github.com/pion/sctp v1.8.36 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,92 +1,88 @@
|
||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y=
|
||||
github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
||||
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
|
||||
github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
|
||||
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
|
||||
github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI=
|
||||
github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
|
||||
github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
|
||||
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
||||
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
|
||||
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
|
||||
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
|
||||
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
|
||||
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o=
|
||||
github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA=
|
||||
github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU=
|
||||
github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
|
||||
github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
|
||||
github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
@@ -96,59 +92,45 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -160,44 +142,31 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# only debian 13 (trixie) has latest ffmpeg
|
||||
# https://packages.debian.org/trixie/ffmpeg
|
||||
ARG DEBIAN_VERSION="trixie-slim"
|
||||
ARG GO_VERSION="1.22-bookworm"
|
||||
ARG GO_VERSION="1.24-bookworm"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM debian:${DEBIAN_VERSION} AS base
|
||||
|
||||
+2
-4
@@ -69,6 +69,8 @@ func Init() {
|
||||
}
|
||||
|
||||
if cfg.Mod.Listen != "" {
|
||||
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||
Port, _ = strconv.Atoi(port)
|
||||
go listen("tcp", cfg.Mod.Listen)
|
||||
}
|
||||
|
||||
@@ -92,10 +94,6 @@ func listen(network, address string) {
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] listen")
|
||||
|
||||
if network == "tcp" {
|
||||
Port = ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
Handler: Handler,
|
||||
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/www"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/www"
|
||||
)
|
||||
|
||||
func initStatic(staticDir string) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -38,20 +39,19 @@ type Message struct {
|
||||
Value any `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
func (m *Message) String() (value string) {
|
||||
if s, ok := m.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Message) GetString(key string) string {
|
||||
if v, ok := m.Value.(map[string]any); ok {
|
||||
if s, ok := v[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
func (m *Message) Unmarshal(v any) error {
|
||||
b, err := json.Marshal(m.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ""
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
|
||||
@@ -19,15 +19,15 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local
|
||||
|
||||
## Environment variables
|
||||
|
||||
Also go2rtc support templates for using environment variables in any part of config:
|
||||
There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||
|
||||
rtsp:
|
||||
username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
||||
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set
|
||||
```
|
||||
|
||||
## JSON Schema
|
||||
|
||||
+4
-10
@@ -45,22 +45,16 @@ func Init() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if daemon {
|
||||
if daemon && os.Getppid() != 1 {
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Println("Daemon not supported on Windows")
|
||||
fmt.Println("Daemon mode is not supported on Windows")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
for i, arg := range args {
|
||||
if arg == "-daemon" {
|
||||
args[i] = ""
|
||||
}
|
||||
}
|
||||
// Re-run the program in background and exit
|
||||
cmd := exec.Command(os.Args[0], args...)
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Failed to start daemon:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
||||
|
||||
@@ -18,7 +18,7 @@ func LoadConfig(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
func PatchConfig(key string, value any, path ...string) error {
|
||||
func PatchConfig(path []string, value any) error {
|
||||
if ConfigPath == "" {
|
||||
return errors.New("config file disabled")
|
||||
}
|
||||
@@ -26,7 +26,7 @@ func PatchConfig(key string, value any, path ...string) error {
|
||||
// empty config is OK
|
||||
b, _ := os.ReadFile(ConfigPath)
|
||||
|
||||
b, err := yaml.Patch(b, key, value, path...)
|
||||
b, err := yaml.Patch(b, path, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+33
-15
@@ -3,12 +3,14 @@ package app
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var MemoryLog = newBuffer(16)
|
||||
var MemoryLog = newBuffer()
|
||||
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
if s, ok := modules[module]; ok {
|
||||
@@ -38,11 +40,17 @@ func initLogger() {
|
||||
|
||||
var writer io.Writer
|
||||
|
||||
switch modules["output"] {
|
||||
switch output, path, _ := strings.Cut(modules["output"], ":"); output {
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "stdout":
|
||||
writer = os.Stdout
|
||||
case "file":
|
||||
if path == "" {
|
||||
path = "go2rtc.log"
|
||||
}
|
||||
// if fail - only MemoryLog will be available
|
||||
writer, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
timeFormat := modules["time"]
|
||||
@@ -99,15 +107,19 @@ var modules = map[string]string{
|
||||
"time": zerolog.TimeFormatUnixMs,
|
||||
}
|
||||
|
||||
const chunkSize = 1 << 16
|
||||
const (
|
||||
chunkCount = 16
|
||||
chunkSize = 1 << 16
|
||||
)
|
||||
|
||||
type circularBuffer struct {
|
||||
chunks [][]byte
|
||||
r, w int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newBuffer(chunks int) *circularBuffer {
|
||||
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
|
||||
func newBuffer() *circularBuffer {
|
||||
b := &circularBuffer{chunks: make([][]byte, 0, chunkCount)}
|
||||
// create first chunk
|
||||
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||
return b
|
||||
@@ -116,16 +128,17 @@ func newBuffer(chunks int) *circularBuffer {
|
||||
func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
b.mu.Lock()
|
||||
// check if chunk has size
|
||||
if len(b.chunks[b.w])+n > chunkSize {
|
||||
// increase write chunk index
|
||||
if b.w++; b.w == cap(b.chunks) {
|
||||
if b.w++; b.w == chunkCount {
|
||||
b.w = 0
|
||||
}
|
||||
// check overflow
|
||||
if b.r == b.w {
|
||||
// increase read chunk index
|
||||
if b.r++; b.r == cap(b.chunks) {
|
||||
if b.r++; b.r == chunkCount {
|
||||
b.r = 0
|
||||
}
|
||||
}
|
||||
@@ -140,29 +153,34 @@ func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
b.chunks[b.w] = append(b.chunks[b.w], p...)
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||
for i := b.r; ; {
|
||||
var nn int
|
||||
if nn, err = w.Write(b.chunks[i]); err != nil {
|
||||
return
|
||||
}
|
||||
n += int64(nn)
|
||||
buf := make([]byte, 0, chunkCount*chunkSize)
|
||||
|
||||
// use temp buffer inside mutex because w.Write can take some time
|
||||
b.mu.Lock()
|
||||
for i := b.r; ; {
|
||||
buf = append(buf, b.chunks[i]...)
|
||||
if i == b.w {
|
||||
break
|
||||
}
|
||||
if i++; i == cap(b.chunks) {
|
||||
if i++; i == chunkCount {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
return
|
||||
b.mu.Unlock()
|
||||
|
||||
nn, err := w.Write(buf)
|
||||
return int64(nn), err
|
||||
}
|
||||
|
||||
func (b *circularBuffer) Reset() {
|
||||
b.mu.Lock()
|
||||
b.chunks[0] = b.chunks[0][:0]
|
||||
b.r = 0
|
||||
b.w = 0
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -7,13 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("bubble", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := bubble.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
|
||||
return bubble.Dial(source)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,16 +2,8 @@ package debug
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
api.HandleFunc("api/stack", stackHandler)
|
||||
|
||||
streams.HandleFunc("null", nullHandler)
|
||||
}
|
||||
|
||||
func nullHandler(string) (core.Producer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package doorbird
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/doorbird"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.RedirectFunc("doorbird", func(rawURL string) (string, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// https://www.doorbird.com/downloads/api_lan.pdf
|
||||
switch u.Query().Get("media") {
|
||||
case "video":
|
||||
u.Path = "/bha-api/video.cgi"
|
||||
case "audio":
|
||||
u.Path = "/bha-api/audio-receive.cgi"
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
|
||||
u.Scheme = "http"
|
||||
|
||||
return u.String(), nil
|
||||
})
|
||||
|
||||
streams.HandleFunc("doorbird", func(source string) (core.Producer, error) {
|
||||
return doorbird.Dial(source)
|
||||
})
|
||||
}
|
||||
+1
-10
@@ -10,25 +10,16 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("dvrip", handle)
|
||||
streams.HandleFunc("dvrip", dvrip.Dial)
|
||||
|
||||
// DVRIP client autodiscovery
|
||||
api.HandleFunc("api/dvrip", apiDvrip)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
client, err := dvrip.Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const Port = 34569 // UDP port number for dvrip discovery
|
||||
|
||||
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+74
-32
@@ -1,15 +1,17 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -47,9 +49,11 @@ func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
func execHandle(rawURL string) (core.Producer, error) {
|
||||
func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
|
||||
var path string
|
||||
var query url.Values
|
||||
|
||||
// RTSP flow should have `{output}` inside URL
|
||||
// pipe flow may have `#{params}` inside URL
|
||||
@@ -61,35 +65,59 @@ func execHandle(rawURL string) (core.Producer, error) {
|
||||
sum := md5.Sum([]byte(rawURL))
|
||||
path = "/" + hex.EncodeToString(sum[:])
|
||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
|
||||
query = streams.ParseQuery(rawURL[i+1:])
|
||||
rawURL = rawURL[:i]
|
||||
}
|
||||
|
||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
|
||||
cmd.Stderr = &logWriter{
|
||||
buf: make([]byte, 512),
|
||||
debug: log.Debug().Enabled(),
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return handlePipe(rawURL, cmd, query)
|
||||
if s := query.Get("killsignal"); s != "" {
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
cmd.Cancel = func() error {
|
||||
log.Debug().Msgf("[exec] kill with signal=%d", sig)
|
||||
return cmd.Process.Signal(sig)
|
||||
}
|
||||
}
|
||||
|
||||
return handleRTSP(rawURL, cmd, path)
|
||||
}
|
||||
if s := query.Get("killtimeout"); s != "" {
|
||||
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
|
||||
}
|
||||
|
||||
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
||||
if query.Get("backchannel") == "1" {
|
||||
return stdin.NewClient(cmd)
|
||||
}
|
||||
|
||||
r, err := PipeCloser(cmd, query)
|
||||
if path == "" {
|
||||
prod, err = handlePipe(rawURL, cmd)
|
||||
} else {
|
||||
prod, err = handleRTSP(rawURL, cmd, path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = cmd.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd := struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
// add buffer for pipe reader to reduce syscall
|
||||
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||
// stop cmd on close pipe call
|
||||
cmd,
|
||||
}
|
||||
|
||||
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||
|
||||
ts := time.Now()
|
||||
@@ -98,17 +126,22 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod, err := magic.Open(r)
|
||||
prod, err := magic.Open(rd)
|
||||
if err != nil {
|
||||
_ = r.Close()
|
||||
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||
}
|
||||
|
||||
if info, ok := prod.(core.Info); ok {
|
||||
info.SetProtocol("pipe")
|
||||
setRemoteInfo(info, source, cmd.Args)
|
||||
}
|
||||
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
|
||||
|
||||
return prod, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||
func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) {
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
@@ -130,29 +163,26 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||
ts := time.Now()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("[exec]")
|
||||
log.Error().Err(err).Str("source", source).Msg("[exec]")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
timeout := time.NewTimer(30 * time.Second)
|
||||
defer timeout.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 60):
|
||||
_ = cmd.Process.Kill()
|
||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||
case <-timeout.C:
|
||||
// haven't received data from app in timeout
|
||||
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||
return nil, errors.New("exec: timeout")
|
||||
case <-done:
|
||||
// limit message size
|
||||
case <-cmd.Done():
|
||||
// app fail before we receive any data
|
||||
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||
case prod := <-waiter:
|
||||
// app started successfully
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||
prod.OnClose = func() error {
|
||||
log.Debug().Msgf("[exec] kill rtsp")
|
||||
return errors.Join(cmd.Process.Kill(), cmd.Wait())
|
||||
}
|
||||
setRemoteInfo(prod, source, cmd.Args)
|
||||
prod.OnClose = cmd.Close
|
||||
return prod, nil
|
||||
}
|
||||
}
|
||||
@@ -209,3 +239,15 @@ func trimSpace(b []byte) []byte {
|
||||
}
|
||||
return b[start:stop]
|
||||
}
|
||||
|
||||
func setRemoteInfo(info core.Info, source string, args []string) {
|
||||
info.SetSource(source)
|
||||
|
||||
if i := core.Index(args, "-i"); i > 0 && i < len(args)-1 {
|
||||
rawURL := args[i+1]
|
||||
if u, err := url.Parse(rawURL); err == nil && u.Host != "" {
|
||||
info.SetRemoteAddr(u.Host)
|
||||
info.SetURL(rawURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
||||
func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add buffer for pipe reader to reduce syscall
|
||||
return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil
|
||||
}
|
||||
|
||||
type pipeCloser struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
cmd *exec.Cmd
|
||||
query url.Values
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Close() error {
|
||||
return errors.Join(p.Closer.Close(), p.Kill(), p.Wait())
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Kill() error {
|
||||
if s := p.query.Get("killsignal"); s != "" {
|
||||
log.Trace().Msgf("[exec] kill with custom sig=%s", s)
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
return p.cmd.Process.Signal(sig)
|
||||
}
|
||||
return p.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Wait() error {
|
||||
if s := p.query.Get("killtimeout"); s != "" {
|
||||
timeout := time.Duration(core.Atoi(s)) * time.Second
|
||||
timer := time.AfterFunc(timeout, func() {
|
||||
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
||||
_ = p.cmd.Process.Kill()
|
||||
})
|
||||
defer timer.Stop() // stop timer if Wait ends before timeout
|
||||
}
|
||||
return p.cmd.Wait()
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package ffmpeg
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -44,7 +43,7 @@ func Init() {
|
||||
return "", err
|
||||
}
|
||||
args := parseArgs(url[7:])
|
||||
if slices.Contains(args.Codecs, "auto") {
|
||||
if core.Contains(args.Codecs, "auto") {
|
||||
return "", nil // force call streams.HandleFunc("ffmpeg")
|
||||
}
|
||||
return "exec:" + args.String(), nil
|
||||
@@ -180,6 +179,7 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
Version: verAV,
|
||||
}
|
||||
|
||||
var source = s
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||
query = streams.ParseQuery(s[i+1:])
|
||||
@@ -222,6 +222,10 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
default:
|
||||
s += "?video&audio"
|
||||
}
|
||||
s += "&source=ffmpeg:" + url.QueryEscape(source)
|
||||
for _, v := range query["query"] {
|
||||
s += "&" + v
|
||||
}
|
||||
args.Input = inputTemplate("rtsp", s, query)
|
||||
} else if i = strings.Index(s, "?"); i > 0 {
|
||||
switch s[:i] {
|
||||
|
||||
+132
-74
@@ -31,7 +31,7 @@ func TestParseArgsFile(t *testing.T) {
|
||||
{
|
||||
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
||||
source: "/media/bbb.mp4#video=h265#rotate=-90",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||
@@ -53,85 +53,143 @@ func TestParseArgsFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseArgsDevice(t *testing.T) {
|
||||
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
||||
args := parseArgs("device?video=0&video_size=1920x1080")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
||||
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
||||
args = parseArgs("device?video=0&framerate=20#video=h265")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
|
||||
source: "device?video=0&video_size=1920x1080",
|
||||
expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped",
|
||||
source: "device?video=0&framerate=20#video=h265",
|
||||
expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[DEVICE] video/audio",
|
||||
source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)",
|
||||
expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsIpCam(t *testing.T) {
|
||||
// [HTTP] video will be copied
|
||||
args := parseArgs("http://example.com")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args = parseArgs("http://example.com#video=h264")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [HLS] video will be copied, audio will be skipped
|
||||
args = parseArgs("https://example.com#video=copy")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video will be copied without transcoding codecs
|
||||
args = parseArgs("rtsp://example.com")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
||||
args = parseArgs("rtsp://example.com#input=rtsp/udp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
||||
args = parseArgs("rtmp://example.com#input=rtsp/udp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[HTTP] video will be copied",
|
||||
source: "http://example.com",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||
source: "http://example.com#video=h264",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[HLS] video will be copied, audio will be skipped",
|
||||
source: "https://example.com#video=copy",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video will be copied without transcoding codecs",
|
||||
source: "rtsp://example.com",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
|
||||
source: "rtsp://example.com#video=h265#width=1280#height=720",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
|
||||
source: "rtsp://example.com#input=rtsp/udp",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
|
||||
source: "rtmp://example.com#input=rtsp/udp",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsAudio(t *testing.T) {
|
||||
// [AUDIO] audio will be transcoded to AAC, video will be skipped
|
||||
args := parseArgs("rtsp:///example.com#audio=aac")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to AAC/16000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=aac/16000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMA, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcma")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcma/16000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcma/48000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
|
||||
source: "rtsp://example.com#audio=aac",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=aac/16000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
|
||||
source: "rtsp://example.com#audio=opus",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcmu",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcmu/16000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcmu/48000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcma",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcma/16000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcma/48000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsHwVaapi(t *testing.T) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
url string
|
||||
query url.Values
|
||||
ffmpeg core.Producer
|
||||
@@ -31,7 +31,8 @@ func NewProducer(url string) (core.Producer, error) {
|
||||
return nil, errors.New("ffmpeg: unsupported params: " + url[i:])
|
||||
}
|
||||
|
||||
p.Type = "FFmpeg producer"
|
||||
p.ID = core.NewID()
|
||||
p.FormatName = "ffmpeg"
|
||||
p.Medias = []*core.Media{
|
||||
{
|
||||
// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG
|
||||
@@ -81,7 +82,7 @@ func (p *Producer) Stop() error {
|
||||
|
||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if p.ffmpeg == nil {
|
||||
return json.Marshal(p.SuperProducer)
|
||||
return json.Marshal(p.Connection)
|
||||
}
|
||||
return json.Marshal(p.ffmpeg)
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("gopro", handleGoPro)
|
||||
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
|
||||
return gopro.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/gopro", apiGoPro)
|
||||
}
|
||||
|
||||
func handleGoPro(rawURL string) (core.Producer, error) {
|
||||
return gopro.Dial(rawURL)
|
||||
}
|
||||
|
||||
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||
var items []*api.Source
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -45,14 +45,9 @@ func Init() {
|
||||
return "", nil
|
||||
})
|
||||
|
||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
||||
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
|
||||
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||
client, err := hass.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
return hass.NewClient(source)
|
||||
})
|
||||
|
||||
// load static entries from Hass config
|
||||
|
||||
+4
-7
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
medias := mp4.ParseQuery(r.URL.Query())
|
||||
if medias != nil {
|
||||
c := mp4.NewConsumer(medias)
|
||||
c.Type = "HLS/fMP4 consumer"
|
||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
||||
c.UserAgent = r.UserAgent()
|
||||
c.FormatName = "hls/fmp4"
|
||||
c.WithRequest(r)
|
||||
cons = c
|
||||
} else {
|
||||
c := mpegts.NewConsumer()
|
||||
c.Type = "HLS/TS consumer"
|
||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
||||
c.UserAgent = r.UserAgent()
|
||||
c.FormatName = "hls/mpegts"
|
||||
c.WithRequest(r)
|
||||
cons = c
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||
@@ -20,9 +19,8 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||
codecs := msg.String()
|
||||
medias := mp4.ParseCodecs(codecs, true)
|
||||
cons := mp4.NewConsumer(medias)
|
||||
cons.Type = "HLS/fMP4 consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.FormatName = "hls/fmp4"
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func apiPair(id, url string) error {
|
||||
|
||||
streams.New(id, conn.URL())
|
||||
|
||||
return app.PatchConfig(id, conn.URL(), "streams")
|
||||
return app.PatchConfig([]string{"streams", id}, conn.URL())
|
||||
}
|
||||
|
||||
func apiUnpair(id string) error {
|
||||
@@ -112,7 +112,7 @@ func apiUnpair(id string) error {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
rawURL := findHomeKitURL(stream)
|
||||
rawURL := findHomeKitURL(stream.Sources())
|
||||
if rawURL == "" {
|
||||
return errors.New("not homekit source")
|
||||
}
|
||||
@@ -123,15 +123,15 @@ func apiUnpair(id string) error {
|
||||
|
||||
streams.Delete(id)
|
||||
|
||||
return app.PatchConfig(id, nil, "streams")
|
||||
return app.PatchConfig([]string{"streams", id}, nil)
|
||||
}
|
||||
|
||||
func findHomeKitURLs() map[string]*url.URL {
|
||||
urls := map[string]*url.URL{}
|
||||
for id, stream := range streams.Streams() {
|
||||
if rawURL := findHomeKitURL(stream); rawURL != "" {
|
||||
for name, sources := range streams.GetAllSources() {
|
||||
if rawURL := findHomeKitURL(sources); rawURL != "" {
|
||||
if u, err := url.Parse(rawURL); err == nil {
|
||||
urls[id] = u
|
||||
urls[name] = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-24
@@ -79,7 +79,7 @@ func Init() {
|
||||
Handler: homekit.ServerHandler(srv),
|
||||
}
|
||||
|
||||
if url := findHomeKitURL(stream); url != "" {
|
||||
if url := findHomeKitURL(stream.Sources()); url != "" {
|
||||
// 1. Act as transparent proxy for HomeKit camera
|
||||
dial := func() (net.Conn, error) {
|
||||
client, err := homekit.Dial(url, srtp.Server)
|
||||
@@ -118,8 +118,8 @@ func Init() {
|
||||
servers[host] = srv
|
||||
}
|
||||
|
||||
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
||||
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
||||
api.HandleFunc(hap.PathPairSetup, hapHandler)
|
||||
api.HandleFunc(hap.PathPairVerify, hapHandler)
|
||||
|
||||
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||
|
||||
@@ -148,13 +148,19 @@ func streamHandler(rawURL string) (core.Producer, error) {
|
||||
return client, err
|
||||
}
|
||||
|
||||
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
||||
srv, ok := servers[r.Host]
|
||||
if !ok {
|
||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||
return
|
||||
func resolve(host string) *server {
|
||||
if len(servers) == 1 {
|
||||
for _, srv := range servers {
|
||||
return srv
|
||||
}
|
||||
}
|
||||
if srv, ok := servers[host]; ok {
|
||||
return srv
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hapHandler(w http.ResponseWriter, r *http.Request) {
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -162,32 +168,29 @@ func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
|
||||
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
|
||||
srv, ok := servers[r.Host]
|
||||
if !ok {
|
||||
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
|
||||
// Doesn't support Home Assistant and any other open source projects
|
||||
// because they don't send the host header in requests.
|
||||
srv := resolve(r.Host)
|
||||
if srv == nil {
|
||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||
_ = hap.WriteBackoff(rw)
|
||||
return
|
||||
}
|
||||
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
switch r.RequestURI {
|
||||
case hap.PathPairSetup:
|
||||
err = srv.hap.PairSetup(r, rw, conn)
|
||||
case hap.PathPairVerify:
|
||||
err = srv.hap.PairVerify(r, rw, conn)
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
|
||||
if err != nil && err != io.EOF {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
|
||||
func findHomeKitURL(stream *streams.Stream) string {
|
||||
sources := stream.Sources()
|
||||
func findHomeKitURL(sources []string) string {
|
||||
if len(sources) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ func (s *server) DelPair(conn net.Conn, id string) {
|
||||
}
|
||||
|
||||
func (s *server) PatchConfig() {
|
||||
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
|
||||
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
|
||||
log.Error().Err(err).Msgf(
|
||||
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
||||
)
|
||||
|
||||
+25
-8
@@ -11,9 +11,10 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||
"github.com/AlexxIT/go2rtc/pkg/image"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/multipart"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,21 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
prod, err := do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info, ok := prod.(core.Info); ok {
|
||||
info.SetProtocol("http")
|
||||
info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn
|
||||
info.SetURL(rawURL)
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func do(req *http.Request) (core.Producer, error) {
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,14 +82,15 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case ct == "image/jpeg":
|
||||
return mjpeg.NewClient(res), nil
|
||||
|
||||
case ct == "multipart/x-mixed-replace":
|
||||
return multipart.Open(res.Body)
|
||||
|
||||
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
|
||||
return hls.OpenURL(req.URL, res.Body)
|
||||
case ct == "image/jpeg":
|
||||
return image.Open(res)
|
||||
case ct == "multipart/x-mixed-replace":
|
||||
return mpjpeg.Open(res.Body)
|
||||
//https://www.iana.org/assignments/media-types/audio/basic
|
||||
case ct == "audio/basic":
|
||||
return pcm.Open(res.Body)
|
||||
}
|
||||
|
||||
return magic.Open(res.Body)
|
||||
|
||||
+3
-12
@@ -7,16 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("isapi", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn, err := isapi.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
streams.HandleFunc("isapi", func(source string) (core.Producer, error) {
|
||||
return isapi.Dial(source)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,16 +4,10 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ivideon", func(url string) (core.Producer, error) {
|
||||
id := strings.Replace(url[8:], "/", ":", 1)
|
||||
prod := ivideon.NewClient(id)
|
||||
if err := prod.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
|
||||
return ivideon.Dial(source)
|
||||
})
|
||||
}
|
||||
|
||||
+11
-16
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/y4m"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -44,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := magic.NewKeyframe()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
@@ -100,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := mjpeg.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||
@@ -117,7 +115,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
wr := mjpeg.NewWriter(w)
|
||||
_, _ = cons.WriteTo(wr)
|
||||
} else {
|
||||
cons.Type = "ASCII passive consumer "
|
||||
cons.FormatName = "ascii"
|
||||
|
||||
query := r.URL.Query()
|
||||
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
|
||||
@@ -135,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
|
||||
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
|
||||
prod, _ := mpjpeg.Open(r.Body)
|
||||
prod.WithRequest(r)
|
||||
|
||||
client := mjpeg.NewClient(res)
|
||||
stream.AddProducer(client)
|
||||
stream.AddProducer(prod)
|
||||
|
||||
if err := client.Start(); err != nil && err != io.EOF {
|
||||
if err := prod.Start(); err != nil && err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
stream.RemoveProducer(client)
|
||||
stream.RemoveProducer(prod)
|
||||
}
|
||||
|
||||
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
@@ -155,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
}
|
||||
|
||||
cons := mjpeg.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
||||
@@ -183,8 +179,7 @@ func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := y4m.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
||||
+3
-4
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -100,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
medias := mp4.ParseQuery(r.URL.Query())
|
||||
cons := mp4.NewConsumer(medias)
|
||||
cons.Type = "MP4/HTTP active consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.FormatName = "mp4"
|
||||
cons.Protocol = "http"
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
||||
+3
-7
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
@@ -24,9 +23,8 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
cons := mp4.NewConsumer(medias)
|
||||
cons.Type = "MSE/WebSocket active consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.FormatName = "mse/fmp4"
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Debug().Err(err).Msg("[mp4] add consumer")
|
||||
@@ -57,9 +55,7 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
cons := mp4.NewKeyframe(medias)
|
||||
cons.Type = "MP4/WebSocket active consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -18,8 +17,7 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := aac.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -31,8 +30,7 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := mpegts.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
+8
-12
@@ -2,6 +2,7 @@ package nest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
@@ -10,19 +11,13 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("nest", streamNest)
|
||||
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
|
||||
return nest.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/nest", apiNest)
|
||||
}
|
||||
|
||||
func streamNest(url string) (core.Producer, error) {
|
||||
client, err := nest.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
cliendID := query.Get("client_id")
|
||||
@@ -44,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var items []*api.Source
|
||||
|
||||
for name, deviceID := range devices {
|
||||
query.Set("device_id", deviceID)
|
||||
for _, device := range devices {
|
||||
query.Set("device_id", device.DeviceID)
|
||||
query.Set("protocols", strings.Join(device.Protocols, ","))
|
||||
|
||||
items = append(items, &api.Source{
|
||||
Name: name, URL: "nest:?" + query.Encode(),
|
||||
Name: device.Name, URL: "nest:?" + query.Encode(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# ONVIF
|
||||
|
||||
A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
|
||||
|
||||
Go2rtc has one video source and one profile per stream.
|
||||
|
||||
## Tested clients
|
||||
|
||||
Go2rtc works as ONVIF server:
|
||||
|
||||
- Happytime onvif client (windows)
|
||||
- Home Assistant ONVIF integration (linux)
|
||||
- Onvier (android)
|
||||
- ONVIF Device Manager (windows)
|
||||
|
||||
PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
|
||||
|
||||
## Tested cameras
|
||||
|
||||
Go2rtc works as ONVIF client:
|
||||
|
||||
- Dahua IPC-K42
|
||||
- OpenIPC
|
||||
- Reolink RLC-520A
|
||||
- TP-Link Tapo TC60
|
||||
@@ -55,49 +55,73 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
action := onvif.GetRequestAction(b)
|
||||
if action == "" {
|
||||
operation := onvif.GetRequestAction(b)
|
||||
if operation == "" {
|
||||
http.Error(w, "malformed request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] %s", action)
|
||||
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
|
||||
|
||||
var res string
|
||||
switch operation {
|
||||
case onvif.DeviceGetNetworkInterfaces, // important for Hass
|
||||
onvif.DeviceGetSystemDateAndTime, // important for Hass
|
||||
onvif.DeviceGetDiscoveryMode,
|
||||
onvif.DeviceGetDNS,
|
||||
onvif.DeviceGetHostname,
|
||||
onvif.DeviceGetNetworkDefaultGateway,
|
||||
onvif.DeviceGetNetworkProtocols,
|
||||
onvif.DeviceGetNTP,
|
||||
onvif.DeviceGetScopes,
|
||||
onvif.MediaGetVideoEncoderConfigurations,
|
||||
onvif.MediaGetAudioEncoderConfigurations,
|
||||
onvif.MediaGetAudioSources,
|
||||
onvif.MediaGetAudioSourceConfigurations:
|
||||
b = onvif.StaticResponse(operation)
|
||||
|
||||
switch action {
|
||||
case onvif.ActionGetCapabilities:
|
||||
case onvif.DeviceGetCapabilities:
|
||||
// important for Hass: Media section
|
||||
res = onvif.GetCapabilitiesResponse(r.Host)
|
||||
b = onvif.GetCapabilitiesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetSystemDateAndTime:
|
||||
// important for Hass
|
||||
res = onvif.GetSystemDateAndTimeResponse()
|
||||
case onvif.DeviceGetServices:
|
||||
b = onvif.GetServicesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetNetworkInterfaces:
|
||||
// important for Hass: none
|
||||
res = onvif.GetNetworkInterfacesResponse()
|
||||
|
||||
case onvif.ActionGetDeviceInformation:
|
||||
case onvif.DeviceGetDeviceInformation:
|
||||
// important for Hass: SerialNumber (unique server ID)
|
||||
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
|
||||
case onvif.ActionGetServiceCapabilities:
|
||||
case onvif.ServiceGetServiceCapabilities:
|
||||
// important for Hass
|
||||
res = onvif.GetServiceCapabilitiesResponse()
|
||||
// TODO: check path links to media
|
||||
b = onvif.GetMediaServiceCapabilitiesResponse()
|
||||
|
||||
case onvif.ActionSystemReboot:
|
||||
res = onvif.SystemRebootResponse()
|
||||
case onvif.DeviceSystemReboot:
|
||||
b = onvif.StaticResponse(operation)
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
case onvif.ActionGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
res = onvif.GetProfilesResponse(streams.GetAll())
|
||||
case onvif.MediaGetVideoSources:
|
||||
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
|
||||
|
||||
case onvif.ActionGetStreamUri:
|
||||
case onvif.MediaGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
b = onvif.GetProfilesResponse(streams.GetAllNames())
|
||||
|
||||
case onvif.MediaGetProfile:
|
||||
token := onvif.FindTagValue(b, "ProfileToken")
|
||||
b = onvif.GetProfileResponse(token)
|
||||
|
||||
case onvif.MediaGetVideoSourceConfigurations:
|
||||
// important for Happytime Onvif Client
|
||||
b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())
|
||||
|
||||
case onvif.MediaGetVideoSourceConfiguration:
|
||||
token := onvif.FindTagValue(b, "ConfigurationToken")
|
||||
b = onvif.GetVideoSourceConfigurationResponse(token)
|
||||
|
||||
case onvif.MediaGetStreamUri:
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -105,16 +129,23 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
|
||||
res = onvif.GetStreamUriResponse(uri)
|
||||
b = onvif.GetStreamUriResponse(uri)
|
||||
|
||||
case onvif.MediaGetSnapshotUri:
|
||||
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
|
||||
b = onvif.GetSnapshotUriResponse(uri)
|
||||
|
||||
default:
|
||||
http.Error(w, "unsupported action", http.StatusBadRequest)
|
||||
http.Error(w, "unsupported operation", http.StatusBadRequest)
|
||||
log.Warn().Msgf("[onvif] unsupported operation: %s", operation)
|
||||
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] server response:\n%s", b)
|
||||
|
||||
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
||||
if _, err = w.Write([]byte(res)); err != nil {
|
||||
if _, err = w.Write(b); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
@@ -160,7 +191,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if l := log.Trace(); l.Enabled() {
|
||||
b, _ := client.GetProfiles()
|
||||
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
|
||||
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ring"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ring", func(source string) (core.Producer, error) {
|
||||
return ring.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/ring", apiRing)
|
||||
}
|
||||
|
||||
func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
var ringAPI *ring.RingRestClient
|
||||
var err error
|
||||
|
||||
// Check auth method
|
||||
if email := query.Get("email"); email != "" {
|
||||
// Email/Password Flow
|
||||
password := query.Get("password")
|
||||
code := query.Get("code")
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Try authentication (this will trigger 2FA if needed)
|
||||
if _, err = ringAPI.GetAuth(code); err != nil {
|
||||
if ringAPI.Using2FA {
|
||||
// Return 2FA prompt
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"needs_2fa": true,
|
||||
"prompt": ringAPI.PromptFor2FA,
|
||||
})
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Refresh Token Flow
|
||||
refreshToken := query.Get("refresh_token")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
|
||||
RefreshToken: refreshToken,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch devices
|
||||
devices, err := ringAPI.FetchRingDevices()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create clean query with only required parameters
|
||||
cleanQuery := url.Values{}
|
||||
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
||||
|
||||
var items []*api.Source
|
||||
for _, camera := range devices.AllCameras {
|
||||
cleanQuery.Set("device_id", camera.DeviceID)
|
||||
|
||||
// Stream source
|
||||
items = append(items, &api.Source{
|
||||
Name: camera.Description,
|
||||
URL: "ring:?" + cleanQuery.Encode(),
|
||||
})
|
||||
|
||||
// Snapshot source
|
||||
items = append(items, &api.Source{
|
||||
Name: camera.Description + " Snapshot",
|
||||
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
|
||||
})
|
||||
}
|
||||
|
||||
api.ResponseSources(w, items)
|
||||
}
|
||||
@@ -11,22 +11,13 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("roborock", handle)
|
||||
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
|
||||
return roborock.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/roborock", apiHandle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := roborock.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
var Auth struct {
|
||||
UserData *roborock.UserInfo `json:"user_data"`
|
||||
BaseURL string `json:"base_url"`
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
## Tested client
|
||||
|
||||
| From | To | Comment |
|
||||
|--------|---------------------------------|---------|
|
||||
| go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK |
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
|
||||
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
|
||||
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
|
||||
```
|
||||
|
||||
## Tested server
|
||||
|
||||
| From | To | Comment |
|
||||
|------------------------|--------|---------------------|
|
||||
| OBS 31.0.2 | go2rtc | OK |
|
||||
| OpenIPC 2.5.03.02-lite | go2rtc | OK |
|
||||
| FFmpeg 6.1 | go2rtc | OK |
|
||||
| GoPro Black 12 | go2rtc | OK, 1080p, 5000kbps |
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
listen: :1935
|
||||
streams:
|
||||
tmp:
|
||||
```
|
||||
|
||||
**OBS**
|
||||
|
||||
Settings > Stream:
|
||||
|
||||
- Service: Custom
|
||||
- Server: rtmp://192.168.10.101/tmp
|
||||
- Stream Key: <empty>
|
||||
- Use auth: <disabled>
|
||||
|
||||
**OpenIPC**
|
||||
|
||||
WebUI > Majestic > Settings > Outgoing
|
||||
|
||||
- Enable
|
||||
- Address: rtmp://192.168.10.101/tmp
|
||||
- Save
|
||||
- Restart
|
||||
|
||||
**FFmpeg**
|
||||
|
||||
```shell
|
||||
ffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp
|
||||
```
|
||||
|
||||
**GoPro**
|
||||
|
||||
GoPro Quik > Camera > Translation > Other
|
||||
+3
-10
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -128,17 +127,13 @@ func tcpHandle(netConn net.Conn) error {
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamsHandle(url string) (core.Producer, error) {
|
||||
client, err := rtmp.DialPlay(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
return rtmp.DialPlay(url)
|
||||
}
|
||||
|
||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||
cons := flv.NewConsumer()
|
||||
run := func() {
|
||||
wr, err := rtmp.DialPublish(url)
|
||||
wr, err := rtmp.DialPublish(url, cons)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
cons.Type = "HTTP-FLV consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
||||
+33
-3
@@ -1,6 +1,7 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -147,6 +148,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
var closer func()
|
||||
|
||||
trace := log.Trace().Enabled()
|
||||
level := zerolog.WarnLevel
|
||||
|
||||
conn.Listen(func(msg any) {
|
||||
if trace {
|
||||
@@ -184,12 +186,38 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
if query.Get("backchannel") == "1" {
|
||||
conn.Medias = append(conn.Medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||
{Name: core.CodecPCM, ClockRate: 16000},
|
||||
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||
{Name: core.CodecPCM, ClockRate: 8000},
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if s := query.Get("pkt_size"); s != "" {
|
||||
conn.PacketSize = uint16(core.Atoi(s))
|
||||
}
|
||||
|
||||
// param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html
|
||||
if s := query.Get("log_level"); s != "" {
|
||||
if lvl, err := zerolog.ParseLevel(s); err == nil {
|
||||
level = lvl
|
||||
}
|
||||
}
|
||||
|
||||
// will help to protect looping requests to same source
|
||||
conn.Connection.Source = query.Get("source")
|
||||
|
||||
if err := stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,8 +254,10 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
})
|
||||
|
||||
if err := conn.Accept(); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
if errors.Is(err, rtsp.FailedAuth) {
|
||||
log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication")
|
||||
} else if err != io.EOF {
|
||||
log.WithLevel(level).Err(err).Caller().Send()
|
||||
}
|
||||
if closer != nil {
|
||||
closer()
|
||||
|
||||
@@ -1,8 +1,55 @@
|
||||
## Testing notes
|
||||
## Examples
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
test1-basic: ffmpeg:virtual?video#video=h264
|
||||
test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264
|
||||
test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output}
|
||||
# known RTSP sources
|
||||
rtsp-dahua1: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
rtsp-dahua2: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=1
|
||||
rtsp-tplink1: rtsp://admin:password@192.168.10.91/stream1
|
||||
rtsp-tplink2: rtsp://admin:password@192.168.10.91/stream2
|
||||
rtsp-reolink1: rtsp://admin:password@192.168.10.92/h264Preview_01_main
|
||||
rtsp-reolink2: rtsp://admin:password@192.168.10.92/h264Preview_01_sub
|
||||
rtsp-sonoff1: rtsp://admin:password@192.168.10.93/av_stream/ch0
|
||||
rtsp-sonoff2: rtsp://admin:password@192.168.10.93/av_stream/ch1
|
||||
|
||||
# known RTMP sources
|
||||
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
|
||||
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
|
||||
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
|
||||
|
||||
# known HTTP sources
|
||||
http-reolink1: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password
|
||||
http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_sub.bcs&user=admin&password=password
|
||||
http-reolink3: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password
|
||||
|
||||
# known ONVIF sources
|
||||
onvif-dahua1: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000
|
||||
onvif-dahua2: onvif://admin:password@192.168.10.90?subtype=MediaProfile00001
|
||||
onvif-dahua3: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000&snapshot
|
||||
onvif-tplink1: onvif://admin:password@192.168.10.91:2020?subtype=profile_1
|
||||
onvif-tplink2: onvif://admin:password@192.168.10.91:2020?subtype=profile_2
|
||||
onvif-reolink1: onvif://admin:password@192.168.10.92:8000?subtype=000
|
||||
onvif-reolink2: onvif://admin:password@192.168.10.92:8000?subtype=001
|
||||
onvif-reolink3: onvif://admin:password@192.168.10.92:8000?subtype=000&snapshot
|
||||
onvif-openipc1: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_000
|
||||
onvif-openipc2: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_001
|
||||
|
||||
# some EXEC examples
|
||||
exec-h264-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f h264 -
|
||||
exec-flv-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f flv -
|
||||
exec-mpegts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f mpegts -
|
||||
exec-adts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f adts -
|
||||
exec-mjpeg-pipe: exec:ffmpeg -re -i bbb.mp4 -c mjpeg -f mjpeg -
|
||||
exec-hevc-pipe: exec:ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc -
|
||||
exec-wav-pipe: exec:ffmpeg -re -i bbb.mp4 -c pcm_alaw -ar 8000 -ac 1 -f wav -
|
||||
exec-y4m-pipe: exec:ffmpeg -re -i bbb.mp4 -c rawvideo -f yuv4mpegpipe -
|
||||
exec-pcma-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_alaw -ar:a 8000 -ac:a 1 -f wav -
|
||||
exec-pcmu-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -f wav -
|
||||
exec-s16le-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_s16le -ar:a 16000 -ac:a 1 -f wav -
|
||||
|
||||
# some FFmpeg examples
|
||||
ffmpeg-video-h264: ffmpeg:virtual?video#video=h264
|
||||
ffmpeg-video-4K: ffmpeg:virtual?video&size=4K#video=h264
|
||||
ffmpeg-video-10s: ffmpeg:virtual?video&duration=10#video=h264
|
||||
ffmpeg-video-src2: ffmpeg:virtual?video=testsrc2&size=2K#video=h264
|
||||
```
|
||||
|
||||
@@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
producers:
|
||||
for prodN, prod := range s.producers {
|
||||
// check for loop request, ex. `camera1: ffmpeg:camera1`
|
||||
if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
}
|
||||
|
||||
if prodErrors[prodN] != nil {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
@@ -129,7 +135,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
for _, media := range prodMedias {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
for _, codec := range media.Codecs {
|
||||
prod = appendString(prod, codec.PrintName())
|
||||
prod = appendString(prod, media.Kind+":"+codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +143,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
for _, media := range consMedias {
|
||||
if media.Direction == core.DirectionSendonly {
|
||||
for _, codec := range media.Codecs {
|
||||
cons = appendString(cons, codec.PrintName())
|
||||
cons = appendString(cons, media.Kind+":"+codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
)
|
||||
|
||||
func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
// without source - return all streams list
|
||||
if src == "" && r.Method != "POST" {
|
||||
api.ResponseJSON(w, streams)
|
||||
return
|
||||
}
|
||||
|
||||
// Not sure about all this API. Should be rewrited...
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
stream := Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.WithRequest(r)
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
api.ResponsePrettyJSON(w, stream)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, streams[src])
|
||||
}
|
||||
|
||||
case "PUT":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
name = src
|
||||
}
|
||||
|
||||
if New(name, query["src"]...) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig([]string{"streams", name}, query["src"]); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "PATCH":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
|
||||
if Patch(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "POST":
|
||||
// with dst - redirect source to dst
|
||||
if dst := query.Get("dst"); dst != "" {
|
||||
if stream := Get(dst); stream != nil {
|
||||
if err := Validate(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if err = stream.Play(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
api.ResponseJSON(w, stream)
|
||||
}
|
||||
} else if stream = Get(src); stream != nil {
|
||||
if err := Validate(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if err = stream.Publish(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
delete(streams, src)
|
||||
|
||||
if err := app.PatchConfig([]string{"streams", src}, nil); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
dot := make([]byte, 0, 1024)
|
||||
dot = append(dot, "digraph {\n"...)
|
||||
if query.Has("src") {
|
||||
for _, name := range query["src"] {
|
||||
if stream := streams[name]; stream != nil {
|
||||
dot = AppendDOT(dot, stream)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, stream := range streams {
|
||||
dot = AppendDOT(dot, stream)
|
||||
}
|
||||
}
|
||||
dot = append(dot, '}')
|
||||
|
||||
api.Response(w, dot, "text/vnd.graphviz")
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func AppendDOT(dot []byte, stream *Stream) []byte {
|
||||
for _, prod := range stream.producers {
|
||||
if prod.conn == nil {
|
||||
continue
|
||||
}
|
||||
c, err := marshalConn(prod.conn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dot = c.appendDOT(dot, "producer")
|
||||
}
|
||||
for _, cons := range stream.consumers {
|
||||
c, err := marshalConn(cons)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dot = c.appendDOT(dot, "consumer")
|
||||
}
|
||||
return dot
|
||||
}
|
||||
|
||||
func marshalConn(v any) (*conn, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c conn
|
||||
if err = json.Unmarshal(b, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
const bytesK = "KMGTP"
|
||||
|
||||
func humanBytes(i int) string {
|
||||
if i < 1000 {
|
||||
return fmt.Sprintf("%d B", i)
|
||||
}
|
||||
|
||||
f := float64(i) / 1000
|
||||
var n uint8
|
||||
for f >= 1000 && n < 5 {
|
||||
f /= 1000
|
||||
n++
|
||||
}
|
||||
return fmt.Sprintf("%.2f %cB", f, bytesK[n])
|
||||
}
|
||||
|
||||
type node struct {
|
||||
ID uint32 `json:"id"`
|
||||
Codec map[string]any `json:"codec"`
|
||||
Parent uint32 `json:"parent"`
|
||||
Childs []uint32 `json:"childs"`
|
||||
Bytes int `json:"bytes"`
|
||||
//Packets uint32 `json:"packets"`
|
||||
//Drops uint32 `json:"drops"`
|
||||
}
|
||||
|
||||
var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"}
|
||||
|
||||
func (n *node) name() string {
|
||||
if name, ok := n.Codec["codec_name"].(string); ok {
|
||||
return name
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (n *node) codec() []byte {
|
||||
b := make([]byte, 0, 128)
|
||||
for _, k := range codecKeys {
|
||||
if v := n.Codec[k]; v != nil {
|
||||
b = fmt.Appendf(b, "%s=%v\n", k, v)
|
||||
}
|
||||
}
|
||||
if l := len(b); l > 0 {
|
||||
return b[:l-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (n *node) appendDOT(dot []byte, group string) []byte {
|
||||
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec())
|
||||
//for _, sink := range n.Childs {
|
||||
// dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink)
|
||||
//}
|
||||
return dot
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
ID uint32 `json:"id"`
|
||||
FormatName string `json:"format_name"`
|
||||
Protocol string `json:"protocol"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Source string `json:"source"`
|
||||
URL string `json:"url"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Receivers []node `json:"receivers"`
|
||||
Senders []node `json:"senders"`
|
||||
BytesRecv int `json:"bytes_recv"`
|
||||
BytesSend int `json:"bytes_send"`
|
||||
}
|
||||
|
||||
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||
host := c.host()
|
||||
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
||||
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
|
||||
if group == "producer" {
|
||||
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||
} else {
|
||||
dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend))
|
||||
}
|
||||
|
||||
for _, recv := range c.Receivers {
|
||||
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes))
|
||||
dot = recv.appendDOT(dot, "node")
|
||||
}
|
||||
for _, send := range c.Senders {
|
||||
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes))
|
||||
//dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes))
|
||||
//dot = send.appendDOT(dot, "node")
|
||||
}
|
||||
return dot
|
||||
}
|
||||
|
||||
func (c *conn) host() (s string) {
|
||||
if c.Protocol == "pipe" {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
if s = c.RemoteAddr; s == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "forwarded"); i > 0 {
|
||||
s = s[i+10:]
|
||||
}
|
||||
|
||||
if s[0] == '[' {
|
||||
if i := strings.Index(s, "]"); i > 0 {
|
||||
return s[1:i]
|
||||
}
|
||||
}
|
||||
|
||||
if i := strings.IndexAny(s, " ,:"); i > 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *conn) label() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("format_name=" + c.FormatName)
|
||||
if c.Protocol != "" {
|
||||
sb.WriteString("\nprotocol=" + c.Protocol)
|
||||
}
|
||||
if c.Source != "" {
|
||||
sb.WriteString("\nsource=" + c.Source)
|
||||
}
|
||||
if c.URL != "" {
|
||||
sb.WriteString("\nurl=" + c.URL)
|
||||
}
|
||||
if c.UserAgent != "" {
|
||||
sb.WriteString("\nuser_agent=" + c.UserAgent)
|
||||
}
|
||||
// escape quotes https://github.com/AlexxIT/go2rtc/issues/1603
|
||||
return strings.ReplaceAll(sb.String(), `"`, `'`)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type Handler func(url string) (core.Producer, error)
|
||||
type Handler func(source string) (core.Producer, error)
|
||||
|
||||
var handlers = map[string]Handler{}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error {
|
||||
}
|
||||
|
||||
func (s *Stream) AddInternalProducer(conn core.Producer) {
|
||||
producer := &Producer{conn: conn, state: stateInternal}
|
||||
producer := &Producer{conn: conn, state: stateInternal, url: "internal"}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
||||
}
|
||||
|
||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if p.conn != nil {
|
||||
return json.Marshal(p.conn)
|
||||
if conn := p.conn; conn != nil {
|
||||
return json.Marshal(conn)
|
||||
}
|
||||
|
||||
info := core.Info{URL: p.url}
|
||||
info := map[string]string{"url": p.url}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -21,6 +21,12 @@ func NewStream(source any) *Stream {
|
||||
return &Stream{
|
||||
producers: []*Producer{NewProducer(source)},
|
||||
}
|
||||
case []string:
|
||||
s := new(Stream)
|
||||
for _, str := range source {
|
||||
s.producers = append(s.producers, NewProducer(str))
|
||||
}
|
||||
return s
|
||||
case []any:
|
||||
s := new(Stream)
|
||||
for _, src := range source {
|
||||
@@ -41,11 +47,12 @@ func NewStream(source any) *Stream {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) Sources() (sources []string) {
|
||||
func (s *Stream) Sources() []string {
|
||||
sources := make([]string, 0, len(s.producers))
|
||||
for _, prod := range s.producers {
|
||||
sources = append(sources, prod.url)
|
||||
}
|
||||
return
|
||||
return sources
|
||||
}
|
||||
|
||||
func (s *Stream) SetSource(source string) {
|
||||
@@ -70,7 +77,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
||||
}
|
||||
|
||||
func (s *Stream) AddProducer(prod core.Producer) {
|
||||
producer := &Producer{conn: prod, state: stateExternal}
|
||||
producer := &Producer{conn: prod, state: stateExternal, url: "external"}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
@@ -112,19 +119,12 @@ producers:
|
||||
}
|
||||
|
||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
if !s.mu.TryLock() {
|
||||
log.Warn().Msgf("[streams] json locked")
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
var info struct {
|
||||
var info = struct {
|
||||
Producers []*Producer `json:"producers"`
|
||||
Consumers []core.Consumer `json:"consumers"`
|
||||
}{
|
||||
Producers: s.producers,
|
||||
Consumers: s.consumers,
|
||||
}
|
||||
info.Producers = s.producers
|
||||
info.Consumers = s.consumers
|
||||
|
||||
s.mu.Unlock()
|
||||
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+51
-120
@@ -2,7 +2,6 @@ package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -29,7 +26,8 @@ func Init() {
|
||||
streams[name] = NewStream(item)
|
||||
}
|
||||
|
||||
api.HandleFunc("api/streams", streamsHandler)
|
||||
api.HandleFunc("api/streams", apiStreams)
|
||||
api.HandleFunc("api/streams.dot", apiStreamsDOT)
|
||||
|
||||
if cfg.Publish == nil {
|
||||
return
|
||||
@@ -44,10 +42,6 @@ func Init() {
|
||||
})
|
||||
}
|
||||
|
||||
func Get(name string) *Stream {
|
||||
return streams[name]
|
||||
}
|
||||
|
||||
var sanitize = regexp.MustCompile(`\s`)
|
||||
|
||||
// Validate - not allow creating dynamic streams with spaces in the source
|
||||
@@ -58,13 +52,19 @@ func Validate(source string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(name string, source string) *Stream {
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
func New(name string, sources ...string) *Stream {
|
||||
for _, source := range sources {
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
stream := NewStream(source)
|
||||
stream := NewStream(sources)
|
||||
|
||||
streamsMu.Lock()
|
||||
streams[name] = stream
|
||||
streamsMu.Unlock()
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ func Patch(name string, source string) *Stream {
|
||||
return nil
|
||||
}
|
||||
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check an existing stream with this name
|
||||
if stream, ok := streams[name]; ok {
|
||||
stream.SetSource(source)
|
||||
@@ -104,7 +108,9 @@ func Patch(name string, source string) *Stream {
|
||||
}
|
||||
|
||||
// create new stream with this name
|
||||
return New(name, source)
|
||||
stream := NewStream(source)
|
||||
streams[name] = stream
|
||||
return stream
|
||||
}
|
||||
|
||||
func GetOrPatch(query url.Values) *Stream {
|
||||
@@ -115,7 +121,7 @@ func GetOrPatch(query url.Values) *Stream {
|
||||
}
|
||||
|
||||
// check if src is stream name
|
||||
if stream, ok := streams[source]; ok {
|
||||
if stream := Get(source); stream != nil {
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -130,116 +136,41 @@ func GetOrPatch(query url.Values) *Stream {
|
||||
return Patch(source, source)
|
||||
}
|
||||
|
||||
func GetAll() (names []string) {
|
||||
var log zerolog.Logger
|
||||
|
||||
// streams map
|
||||
|
||||
var streams = map[string]*Stream{}
|
||||
var streamsMu sync.Mutex
|
||||
|
||||
func Get(name string) *Stream {
|
||||
streamsMu.Lock()
|
||||
defer streamsMu.Unlock()
|
||||
return streams[name]
|
||||
}
|
||||
|
||||
func Delete(name string) {
|
||||
streamsMu.Lock()
|
||||
defer streamsMu.Unlock()
|
||||
delete(streams, name)
|
||||
}
|
||||
|
||||
func GetAllNames() []string {
|
||||
streamsMu.Lock()
|
||||
names := make([]string, 0, len(streams))
|
||||
for name := range streams {
|
||||
names = append(names, name)
|
||||
}
|
||||
return
|
||||
streamsMu.Unlock()
|
||||
return names
|
||||
}
|
||||
|
||||
func Streams() map[string]*Stream {
|
||||
return streams
|
||||
}
|
||||
|
||||
func Delete(id string) {
|
||||
delete(streams, id)
|
||||
}
|
||||
|
||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
// without source - return all streams list
|
||||
if src == "" && r.Method != "POST" {
|
||||
api.ResponseJSON(w, streams)
|
||||
return
|
||||
}
|
||||
|
||||
// Not sure about all this API. Should be rewrited...
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
stream := Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
api.ResponsePrettyJSON(w, stream)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, streams[src])
|
||||
}
|
||||
|
||||
case "PUT":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
name = src
|
||||
}
|
||||
|
||||
if New(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig(name, src, "streams"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "PATCH":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
|
||||
if Patch(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "POST":
|
||||
// with dst - redirect source to dst
|
||||
if dst := query.Get("dst"); dst != "" {
|
||||
if stream := Get(dst); stream != nil {
|
||||
if err := Validate(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if err = stream.Play(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
api.ResponseJSON(w, stream)
|
||||
}
|
||||
} else if stream = Get(src); stream != nil {
|
||||
if err := Validate(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if err = stream.Publish(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
delete(streams, src)
|
||||
|
||||
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
func GetAllSources() map[string][]string {
|
||||
streamsMu.Lock()
|
||||
sources := make(map[string][]string, len(streams))
|
||||
for name, stream := range streams {
|
||||
sources[name] = stream.Sources()
|
||||
}
|
||||
streamsMu.Unlock()
|
||||
return sources
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
var streams = map[string]*Stream{}
|
||||
var streamsMu sync.Mutex
|
||||
|
||||
@@ -8,11 +8,15 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
|
||||
return kasa.Dial(url)
|
||||
streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
|
||||
return kasa.Dial(source)
|
||||
})
|
||||
|
||||
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
|
||||
return tapo.Dial(url)
|
||||
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
|
||||
return tapo.Dial(source)
|
||||
})
|
||||
|
||||
streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
|
||||
return tapo.Dial(source)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# V4L2
|
||||
|
||||
What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux):
|
||||
|
||||
- V4L2 (Video for Linux API version 2) works only in Linux
|
||||
- supports USB cameras and other similar devices
|
||||
- one device can only be connected to one software simultaneously
|
||||
- cameras support a fixed list of formats, resolutions and frame rates
|
||||
- basic cameras supports only RAW (non-compressed) pixel formats
|
||||
- regular cameras supports MJPEG format (series of JPEG frames)
|
||||
- advances cameras support H264 format (MSE/MP4, WebRTC compatible)
|
||||
- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage
|
||||
- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage
|
||||
- H265 (HEVC) format is also supported (if the camera supports it)
|
||||
|
||||
Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%.
|
||||
|
||||
Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**.
|
||||
|
||||
## RAW format
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10
|
||||
```
|
||||
|
||||
Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured.
|
||||
|
||||
```
|
||||
ffplay http://localhost:1984/api/stream.mjpeg?src=camera1
|
||||
```
|
||||
|
||||
**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth.
|
||||
|
||||
```
|
||||
ffplay http://localhost:1984/api/stream.y4m?src=camera1
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !linux
|
||||
|
||||
package v4l2
|
||||
|
||||
func Init() {
|
||||
// not supported
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package v4l2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/pkg/v4l2/device"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("v4l2", func(source string) (core.Producer, error) {
|
||||
return v4l2.Open(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/v4l2", apiV4L2)
|
||||
}
|
||||
|
||||
func apiV4L2(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []*api.Source
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
continue
|
||||
}
|
||||
|
||||
path := "/dev/" + file.Name()
|
||||
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
formats, _ := dev.ListFormats()
|
||||
for _, fourCC := range formats {
|
||||
name, ffmpeg := findFormat(fourCC)
|
||||
source := &api.Source{Name: name}
|
||||
|
||||
sizes, _ := dev.ListSizes(fourCC)
|
||||
for _, wh := range sizes {
|
||||
if source.Info != "" {
|
||||
source.Info += " "
|
||||
}
|
||||
|
||||
source.Info += fmt.Sprintf("%dx%d", wh[0], wh[1])
|
||||
|
||||
frameRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1])
|
||||
for _, fr := range frameRates {
|
||||
source.Info += fmt.Sprintf("@%d", fr)
|
||||
|
||||
if source.URL == "" && ffmpeg != "" {
|
||||
source.URL = fmt.Sprintf(
|
||||
"v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d",
|
||||
path, ffmpeg, wh[0], wh[1], fr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if source.Info != "" {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
}
|
||||
|
||||
func findFormat(fourCC uint32) (name, ffmpeg string) {
|
||||
for _, format := range device.Formats {
|
||||
if format.FourCC == fourCC {
|
||||
return format.Name, format.FFmpeg
|
||||
}
|
||||
}
|
||||
return string(binary.LittleEndian.AppendUint32(nil, fourCC)), ""
|
||||
}
|
||||
@@ -11,13 +11,15 @@ If an external connection via STUN is used:
|
||||
|
||||
- Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you not open your server to the World
|
||||
- For about 20% of users, the techology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/)
|
||||
- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate
|
||||
- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate:
|
||||
- https://habr.com/ru/companies/flashphoner/articles/480006/
|
||||
- https://www.youtube.com/watch?v=FXVg2ckuKfs
|
||||
|
||||
## Default config
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
listen: ":8555/tcp"
|
||||
listen: ":8555"
|
||||
ice_servers:
|
||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||
```
|
||||
@@ -29,7 +31,7 @@ webrtc:
|
||||
```yaml
|
||||
webrtc:
|
||||
# fix local TCP or UDP or both ports for WebRTC media
|
||||
listen: ":8555/tcp" # address of your local server
|
||||
listen: ":8555" # address of your local server
|
||||
|
||||
# add additional host candidates manually
|
||||
# order is important, the first will have a higher priority
|
||||
@@ -53,17 +55,20 @@ webrtc:
|
||||
# including candidates from the `listen` option
|
||||
# use `candidates: []` to remove all auto discovery candidates
|
||||
candidates: [ 192.168.1.123 ]
|
||||
|
||||
# enable localhost candidates
|
||||
loopback: true
|
||||
|
||||
# list of network types to be used for connection
|
||||
# including candidates from the `listen` option
|
||||
networks: [ udp4, udp6, tcp4, tcp6 ]
|
||||
|
||||
# list of interfaces to be used for connection
|
||||
# not related to the `listen` option
|
||||
# including interfaces from unspecified `listen` option (empty host)
|
||||
interfaces: [ eno1 ]
|
||||
|
||||
# list of host IP-addresses to be used for connection
|
||||
# not related to the `listen` option
|
||||
# including IPs from unspecified `listen` option (empty host)
|
||||
ips: [ 192.168.1.123 ]
|
||||
|
||||
# range for random UDP ports [min, max] to be used for connection
|
||||
@@ -71,14 +76,16 @@ webrtc:
|
||||
udp_ports: [ 50000, 50100 ]
|
||||
```
|
||||
|
||||
By default go2rtc uses **fixed TCP** port and multiple **random UDP** ports for each WebRTC connection - `listen: ":8555/tcp"`.
|
||||
By default go2rtc uses **fixed TCP** port and **fixed UDP** ports for each **direct** WebRTC connection - `listen: ":8555"`.
|
||||
|
||||
You can set **fixed TCP** and **fixed UDP** port for all connections - `listen: ":8555"`. This may has lower performance, but it's your choice.
|
||||
You can set **fixed TCP** and **random UDP** port for all connections - `listen: ":8555/tcp"`.
|
||||
|
||||
Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`.
|
||||
|
||||
## Config filters
|
||||
|
||||
**Importan!** By default go2rtc exclude all Docker-like candidates (`172.16.0.0/12`). This can not be disabled.
|
||||
|
||||
Filters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it.
|
||||
|
||||
For example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not.
|
||||
@@ -97,8 +104,6 @@ For example, go2rtc inside closed docker container (ex. [Frigate](https://frigat
|
||||
webrtc:
|
||||
listen: ":8555" # use fixed TCP and UDP ports
|
||||
candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding)
|
||||
filters:
|
||||
candidates: [] # skip all internal docker candidates
|
||||
```
|
||||
|
||||
## Userful links
|
||||
|
||||
@@ -2,11 +2,12 @@ package webrtc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
@@ -73,16 +74,21 @@ func FilterCandidate(candidate *pion.ICECandidate) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// remove any Docker-like IP from candidates
|
||||
if ip := net.ParseIP(candidate.Address); ip != nil && xnet.Docker.Contains(ip) {
|
||||
return false
|
||||
}
|
||||
|
||||
// host candidate should be in the hosts list
|
||||
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
||||
if !slices.Contains(filters.Candidates, candidate.Address) {
|
||||
if !core.Contains(filters.Candidates, candidate.Address) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if filters.Networks != nil {
|
||||
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
|
||||
if !slices.Contains(filters.Networks, networkType) {
|
||||
if !core.Contains(filters.Networks, networkType) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,11 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
||||
// https://aws.amazon.com/kinesis/video-streams/
|
||||
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
||||
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
||||
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
|
||||
return kinesisClient(rawURL, query, "webrtc/kinesis", nil)
|
||||
} else if format == "openipc" {
|
||||
return openIPCClient(rawURL, query)
|
||||
} else if format == "switchbot" {
|
||||
return switchbotClient(rawURL, query)
|
||||
} else {
|
||||
return go2rtcClient(rawURL)
|
||||
}
|
||||
@@ -54,6 +56,8 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
||||
} else if format == "wyze" {
|
||||
// https://github.com/mrlt8/docker-wyze-bridge
|
||||
return wyzeClient(rawURL)
|
||||
} else if format == "creality" {
|
||||
return crealityClient(rawURL)
|
||||
} else {
|
||||
return whepClient(rawURL)
|
||||
}
|
||||
@@ -77,17 +81,23 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
// 2. Create PeerConnection
|
||||
pc, err := PeerConnection(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// waiter will wait PC error or WS error or nil (connection OK)
|
||||
var connState core.Waiter
|
||||
var connMu sync.Mutex
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WebSocket async"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "ws"
|
||||
prod.URL = url
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
@@ -132,7 +142,8 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
if msg.Type != "webrtc/answer" {
|
||||
return nil, errors.New("wrong answer: " + msg.Type)
|
||||
err = errors.New("wrong answer: " + msg.String())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answer := msg.String()
|
||||
@@ -180,8 +191,9 @@ func whepClient(url string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WHEP sync"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.URL = url
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1600
|
||||
func crealityClient(url string) (core.Producer, error) {
|
||||
pc, err := PeerConnection(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.FormatName = "webrtc/creality"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.URL = url
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
}
|
||||
|
||||
// TODO: return webrtc.SessionDescription
|
||||
offer, err := prod.CreateCompleteOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := offerToB64(offer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "plain/text")
|
||||
|
||||
// TODO: change http.DefaultClient settings
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answer, err := answerFromB64(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = prod.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func offerToB64(sdp string) (io.Reader, error) {
|
||||
// JS object
|
||||
v := map[string]string{
|
||||
"type": "offer",
|
||||
"sdp": sdp,
|
||||
}
|
||||
|
||||
// bytes
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// base64, why? who knows...
|
||||
s := base64.StdEncoding.EncodeToString(b)
|
||||
|
||||
return strings.NewReader(s), nil
|
||||
}
|
||||
|
||||
func answerFromB64(r io.Reader) (string, error) {
|
||||
// base64
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// bytes
|
||||
if b, err = base64.StdEncoding.DecodeString(string(b)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// JS object
|
||||
var v map[string]string
|
||||
if err = json.Unmarshal(b, &v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// string "v=0..."
|
||||
return v["sdp"], nil
|
||||
}
|
||||
+32
-17
@@ -34,7 +34,10 @@ func (k kinesisResponse) String() string {
|
||||
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
||||
}
|
||||
|
||||
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
|
||||
func kinesisClient(
|
||||
rawURL string, query url.Values, format string,
|
||||
sdpOffer func(prod *webrtc.Conn, query url.Values) (any, error),
|
||||
) (core.Producer, error) {
|
||||
// 1. Connect to signalign server
|
||||
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||
if err != nil {
|
||||
@@ -79,8 +82,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = desc
|
||||
prod.FormatName = format
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "ws"
|
||||
prod.URL = rawURL
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
@@ -106,23 +111,33 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
||||
}
|
||||
})
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
var payload any
|
||||
|
||||
if sdpOffer == nil {
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
}
|
||||
|
||||
// 4. Create offer
|
||||
var offer string
|
||||
if offer, err = prod.CreateOffer(medias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. Send offer
|
||||
payload = pion.SessionDescription{
|
||||
Type: pion.SDPTypeOffer,
|
||||
SDP: offer,
|
||||
}
|
||||
} else {
|
||||
if payload, err = sdpOffer(prod, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Create offer
|
||||
offer, err := prod.CreateOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. Send offer
|
||||
req.Action = "SDP_OFFER"
|
||||
req.Payload, _ = json.Marshal(pion.SessionDescription{
|
||||
Type: pion.SDPTypeOffer,
|
||||
SDP: offer,
|
||||
})
|
||||
req.Payload, _ = json.Marshal(payload)
|
||||
if err = conn.WriteJSON(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,5 +231,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
|
||||
"ice_servers": []string{string(kvs.Servers)},
|
||||
}
|
||||
|
||||
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
|
||||
return kinesisClient(kvs.URL, query, "webrtc/wyze", nil)
|
||||
}
|
||||
|
||||
@@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/Milestone"
|
||||
prod.FormatName = "webrtc/milestone"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.URL = rawURL
|
||||
|
||||
offer, err := mc.GetOffer()
|
||||
if err != nil {
|
||||
|
||||
@@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
var connState core.Waiter
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/OpenIPC"
|
||||
prod.FormatName = "webrtc/openipc"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "ws"
|
||||
prod.URL = rawURL
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -62,9 +63,10 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
|
||||
// 3. other - receive/response raw SDP
|
||||
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("src")
|
||||
stream := streams.Get(url)
|
||||
u := r.URL.Query().Get("src")
|
||||
stream := streams.Get(u)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,6 +88,21 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
offer = desc.SDP
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offerB64 := r.Form.Get("data")
|
||||
b, err := base64.StdEncoding.DecodeString(offerB64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offer = string(b)
|
||||
|
||||
default:
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -100,11 +117,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
desc = "WebRTC/JSON sync"
|
||||
desc = "webrtc/json"
|
||||
case MimeSDP:
|
||||
desc = "WebRTC/WHEP sync"
|
||||
desc = "webrtc/whep"
|
||||
default:
|
||||
desc = "WebRTC/HTTP sync"
|
||||
desc = "webrtc/post"
|
||||
}
|
||||
|
||||
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
||||
@@ -123,6 +140,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(v)
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
answerB64 := base64.StdEncoding.EncodeToString([]byte(answer))
|
||||
_, err = w.Write([]byte(answerB64))
|
||||
|
||||
case MimeSDP:
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
@@ -168,8 +190,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// create new webrtc instance
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WHIP sync"
|
||||
prod.Mode = core.ModePassiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.UserAgent = r.UserAgent()
|
||||
|
||||
if err = prod.SetOffer(string(offer)); err != nil {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
)
|
||||
|
||||
func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
return kinesisClient(rawURL, query, "webrtc/switchbot", func(prod *webrtc.Conn, query url.Values) (any, error) {
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
}
|
||||
|
||||
offer, err := prod.CreateOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := struct {
|
||||
Type string `json:"type"`
|
||||
SDP string `json:"sdp"`
|
||||
Resolution int `json:"resolution"`
|
||||
PlayType int `json:"play_type"`
|
||||
}{
|
||||
Type: "offer",
|
||||
SDP: offer,
|
||||
}
|
||||
|
||||
switch query.Get("resolution") {
|
||||
case "hd":
|
||||
v.Resolution = 0
|
||||
case "sd":
|
||||
v.Resolution = 1
|
||||
}
|
||||
|
||||
return v, nil
|
||||
})
|
||||
}
|
||||
+36
-19
@@ -24,7 +24,7 @@ func Init() {
|
||||
} `yaml:"webrtc"`
|
||||
}
|
||||
|
||||
cfg.Mod.Listen = ":8555/tcp"
|
||||
cfg.Mod.Listen = ":8555"
|
||||
cfg.Mod.IceServers = []pion.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
}
|
||||
@@ -40,15 +40,17 @@ func Init() {
|
||||
AddCandidate(network, candidate)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// create pionAPI with custom codecs list and custom network settings
|
||||
serverAPI, err := webrtc.NewServerAPI(network, address, &filters)
|
||||
serverAPI, err = webrtc.NewServerAPI(network, address, &filters)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
// use same API for WebRTC server and client if no address
|
||||
clientAPI := serverAPI
|
||||
clientAPI = serverAPI
|
||||
|
||||
if address != "" {
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
||||
@@ -81,11 +83,13 @@ func Init() {
|
||||
streams.HandleFunc("webrtc", streamsHandler)
|
||||
}
|
||||
|
||||
var serverAPI, clientAPI *pion.API
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
||||
|
||||
func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {
|
||||
var stream *streams.Stream
|
||||
var mode core.Mode
|
||||
|
||||
@@ -104,8 +108,30 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
var offer struct {
|
||||
Type string `json:"type"`
|
||||
SDP string `json:"sdp"`
|
||||
ICEServers []pion.ICEServer `json:"ice_servers"`
|
||||
}
|
||||
|
||||
// V2 - json/object exchange, V1 - raw SDP exchange
|
||||
apiV2 := msg.Type == "webrtc"
|
||||
|
||||
if apiV2 {
|
||||
if err = msg.Unmarshal(&offer); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
offer.SDP = msg.String()
|
||||
}
|
||||
|
||||
// create new PeerConnection instance
|
||||
pc, err := PeerConnection(false)
|
||||
var pc *pion.PeerConnection
|
||||
if offer.ICEServers == nil {
|
||||
pc, err = PeerConnection(false)
|
||||
} else {
|
||||
pc, err = serverAPI.NewPeerConnection(pion.Configuration{ICEServers: offer.ICEServers})
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
@@ -117,8 +143,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
defer sendAnswer.Done(nil)
|
||||
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = "WebRTC/WebSocket async"
|
||||
conn.Mode = mode
|
||||
conn.Protocol = "ws"
|
||||
conn.UserAgent = tr.Request.UserAgent()
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
@@ -145,20 +171,10 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
})
|
||||
|
||||
// V2 - json/object exchange, V1 - raw SDP exchange
|
||||
apiV2 := msg.Type == "webrtc"
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer.SDP)
|
||||
|
||||
// 1. SetOffer, so we can get remote client codecs
|
||||
var offer string
|
||||
if apiV2 {
|
||||
offer = msg.GetString("sdp")
|
||||
} else {
|
||||
offer = msg.String()
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
if err = conn.SetOffer(offer.SDP); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
@@ -207,8 +223,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
||||
|
||||
// create new webrtc instance
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = desc
|
||||
conn.FormatName = desc
|
||||
conn.UserAgent = userAgent
|
||||
conn.Protocol = "http"
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebRTCAPIv1(t *testing.T) {
|
||||
raw := `{"type":"webrtc/offer","value":"v=0\n..."}`
|
||||
msg := new(ws.Message)
|
||||
err := json.Unmarshal([]byte(raw), msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "v=0\n...", msg.String())
|
||||
}
|
||||
|
||||
func TestWebRTCAPIv2(t *testing.T) {
|
||||
raw := `{"type":"webrtc","value":{"type":"offer","sdp":"v=0\n...","ice_servers":[{"urls":["stun:stun.l.google.com:19302"]}]}}`
|
||||
msg := new(ws.Message)
|
||||
err := json.Unmarshal([]byte(raw), msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
var offer struct {
|
||||
Type string `json:"type"`
|
||||
SDP string `json:"sdp"`
|
||||
ICEServers []pion.ICEServer `json:"ice_servers"`
|
||||
}
|
||||
err = msg.Unmarshal(&offer)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "offer", offer.Type)
|
||||
require.Equal(t, "v=0\n...", offer.SDP)
|
||||
require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0])
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func Init() {
|
||||
if stream == nil {
|
||||
return "", errors.New(api.StreamNotFound)
|
||||
}
|
||||
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
|
||||
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ package webtorrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var upgrader *websocket.Upgrader
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/bubble"
|
||||
"github.com/AlexxIT/go2rtc/internal/debug"
|
||||
"github.com/AlexxIT/go2rtc/internal/doorbird"
|
||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
@@ -24,19 +25,21 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/nest"
|
||||
"github.com/AlexxIT/go2rtc/internal/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/internal/onvif"
|
||||
"github.com/AlexxIT/go2rtc/internal/ring"
|
||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/tapo"
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/webtorrent"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.9.3"
|
||||
app.Version = "1.9.9"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
|
||||
@@ -78,10 +81,13 @@ func main() {
|
||||
mpegts.Init() // mpegts passive source
|
||||
roborock.Init() // roborock source
|
||||
homekit.Init() // homekit source
|
||||
ring.Init() // ring source
|
||||
nest.Init() // nest source
|
||||
bubble.Init() // bubble source
|
||||
expr.Init() // expr source
|
||||
gopro.Init() // gopro source
|
||||
doorbird.Init() // doorbird source
|
||||
v4l2.Init() // v4l2 source
|
||||
|
||||
// 6. Helper modules
|
||||
|
||||
|
||||
@@ -1,3 +1,85 @@
|
||||
# Notes
|
||||
|
||||
go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.
|
||||
Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.
|
||||
|
||||
## Producers (input)
|
||||
|
||||
- The initiator of the connection can be go2rtc - **Source protocols**
|
||||
- The initiator of the connection can be an external program - **Ingress protocols**
|
||||
- Codecs can be incoming - **Recevers codecs**
|
||||
- Codecs can be outgoing (two way audio) - **Senders codecs**
|
||||
|
||||
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|
||||
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
|
||||
| adts | http,tcp,pipe | http | aac | | `http:` |
|
||||
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
|
||||
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
|
||||
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
|
||||
| gopro | http+udp | | TODO | | `gopro:` |
|
||||
| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` |
|
||||
| hls/mpegts | http | | h264,h265,aac,opus | | `http:` |
|
||||
| homekit | homekit+udp | | h264,eld* | | `homekit:` |
|
||||
| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` |
|
||||
| ivideon | ws | | h264 | | `ivideon:` |
|
||||
| kasa | http | | h264,pcm_mulaw | | `kasa:` |
|
||||
| h264 | http,tcp,pipe | http | h264 | | `http:` |
|
||||
| hevc | http,tcp,pipe | http | hevc | | `http:` |
|
||||
| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||
| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||
| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` |
|
||||
| nest/webrtc | http+udp | | TODO | | `nest:` |
|
||||
| roborock | mqtt+udp | | h264,opus | opus | `roborock:` |
|
||||
| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` |
|
||||
| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` |
|
||||
| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` |
|
||||
| tapo | http | | h264,pcma | pcm_alaw | `tapo:` |
|
||||
| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` |
|
||||
| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` |
|
||||
| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` |
|
||||
| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` |
|
||||
|
||||
- **eld** - rare variant of aac codec
|
||||
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||
- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep
|
||||
|
||||
## Consumers (output)
|
||||
|
||||
| Format | Protocol | Send codecs | Recv codecs | Example |
|
||||
|--------------|-------------|------------------------------|-------------------------|---------------------------------------|
|
||||
| adts | http | aac | | `GET /api/stream.adts` |
|
||||
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
|
||||
| flv | http | h264,aac | | `GET /api/stream.flv` |
|
||||
| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` |
|
||||
| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` |
|
||||
| homekit | homekit+udp | h264,opus | | Apple HomeKit app |
|
||||
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
|
||||
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
|
||||
| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` |
|
||||
| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` |
|
||||
| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` |
|
||||
| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` |
|
||||
| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` |
|
||||
| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` |
|
||||
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
|
||||
|
||||
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||
|
||||
## Snapshots
|
||||
|
||||
| Format | Protocol | Send codecs | Example |
|
||||
|--------|----------|-------------|-----------------------|
|
||||
| jpeg | http | mjpeg | `GET /api/frame.jpeg` |
|
||||
| mp4 | http | h264,hevc | `GET /api/frame.mp4` |
|
||||
|
||||
## Developers
|
||||
|
||||
File naming:
|
||||
|
||||
- `pkg/{format}/producer.go` - producer for this format (also if support backchannel)
|
||||
- `pkg/{format}/consumer.go` - consumer for this format
|
||||
- `pkg/{format}/backchanel.go` - producer with only backchannel func
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://www.wowza.com/blog/streaming-protocols
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
)
|
||||
|
||||
func IsADTS(b []byte) bool {
|
||||
_ = b[1]
|
||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||
}
|
||||
|
||||
|
||||
+12
-11
@@ -8,15 +8,12 @@ import (
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
core.Connection
|
||||
wr *core.WriteBuffer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
cons := &Consumer{
|
||||
wr: core.NewWriteBuffer(nil),
|
||||
}
|
||||
cons.Medias = []*core.Media{
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
@@ -25,7 +22,16 @@ func NewConsumer() *Consumer {
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons
|
||||
wr := core.NewWriteBuffer(nil)
|
||||
return &Consumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "adts",
|
||||
Medias: medias,
|
||||
Transport: wr,
|
||||
},
|
||||
wr: wr,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
@@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.wr.Close()
|
||||
}
|
||||
|
||||
+12
-14
@@ -10,9 +10,8 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
rd *bufio.Reader
|
||||
cl io.Closer
|
||||
}
|
||||
|
||||
func Open(r io.Reader) (*Producer, error) {
|
||||
@@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codec := ADTSToCodec(b)
|
||||
|
||||
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
||||
prod.Type = "ADTS producer"
|
||||
prod.Medias = []*core.Media{
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
Codecs: []*core.Codec{ADTSToCodec(b)},
|
||||
},
|
||||
}
|
||||
return prod, nil
|
||||
return &Producer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "adts",
|
||||
Medias: medias,
|
||||
Transport: r,
|
||||
},
|
||||
rd: rd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Producer) Start() error {
|
||||
@@ -66,8 +69,3 @@ func (c *Producer) Start() error {
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
_ = c.SuperProducer.Close()
|
||||
return c.cl.Close()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||
|
||||
if len(packet.Payload) < int(2+headersSize) {
|
||||
// In very rare cases noname cameras may send data not according to the standard
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1328
|
||||
if IsADTS(packet.Payload) {
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAAC
|
||||
clone.Timestamp = timestamp
|
||||
clone.Payload = clone.Payload[ADTSHeaderSize:]
|
||||
handler(&clone)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package aac
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuggy_RTSP_AAC(t *testing.T) {
|
||||
// https: //github.com/AlexxIT/go2rtc/issues/1328
|
||||
payload, _ := hex.DecodeString("fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0")
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: 36944,
|
||||
Timestamp: 4217191328,
|
||||
SSRC: 12892774,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
var size int
|
||||
|
||||
RTPDepay(func(packet *core.Packet) {
|
||||
size = len(packet.Payload)
|
||||
})(packet)
|
||||
|
||||
require.Equal(t, len(payload), size+ADTSHeaderSize)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
// Deprecated: should be rewritten to core.Connection
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
@@ -43,8 +44,12 @@ type Client struct {
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{url: url}
|
||||
func Dial(rawURL string) (*Client, error) {
|
||||
client := &Client{url: rawURL}
|
||||
if err := client.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -226,7 +231,7 @@ func (c *Client) Handle() error {
|
||||
Header: rtp.Header{
|
||||
Timestamp: core.Now90000(),
|
||||
},
|
||||
Payload: annexb.EncodeToAVCC(b[6:], false),
|
||||
Payload: annexb.EncodeToAVCC(b[6:]),
|
||||
}
|
||||
c.videoTrack.WriteRTP(pkt)
|
||||
} else {
|
||||
|
||||
+10
-5
@@ -65,11 +65,16 @@ func (c *Client) Stop() error {
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "Bubble active producer",
|
||||
Medias: c.medias,
|
||||
Recv: c.recv,
|
||||
Receivers: c.receivers,
|
||||
info := &core.Connection{
|
||||
ID: core.ID(c),
|
||||
FormatName: "bubble",
|
||||
Protocol: "http",
|
||||
Medias: c.medias,
|
||||
Recv: c.recv,
|
||||
Receivers: c.receivers,
|
||||
}
|
||||
if c.conn != nil {
|
||||
info.RemoteAddr = c.conn.RemoteAddr().String()
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+14
-3
@@ -46,7 +46,7 @@ func FFmpegCodecName(name string) string {
|
||||
case CodecH264:
|
||||
return "h264"
|
||||
case CodecH265:
|
||||
return "h265"
|
||||
return "hevc"
|
||||
case CodecJPEG:
|
||||
return "mjpeg"
|
||||
case CodecRAW:
|
||||
@@ -69,8 +69,14 @@ func FFmpegCodecName(name string) string {
|
||||
return "vp9"
|
||||
case CodecAV1:
|
||||
return "av1"
|
||||
case CodecELD:
|
||||
return "aac/eld"
|
||||
case CodecFLAC:
|
||||
return "flac"
|
||||
case CodecMP3:
|
||||
return "mp3"
|
||||
}
|
||||
return ""
|
||||
return name
|
||||
}
|
||||
|
||||
func (c *Codec) String() (s string) {
|
||||
@@ -151,7 +157,12 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Name == "" {
|
||||
switch c.Name {
|
||||
case "PCM":
|
||||
// https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/
|
||||
// check pkg/rtsp/rtsp_test.go TestHikvisionPCM
|
||||
c.Name = CodecPCML
|
||||
case "":
|
||||
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||
switch payloadType {
|
||||
case "0":
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func NewID() uint32 {
|
||||
return id.Add(1)
|
||||
}
|
||||
|
||||
// Deprecated: use NewID instead
|
||||
func ID(v any) uint32 {
|
||||
p := uintptr(reflect.ValueOf(v).UnsafePointer())
|
||||
return 0x8000_0000 | uint32(p)
|
||||
}
|
||||
|
||||
var id atomic.Uint32
|
||||
|
||||
type Info interface {
|
||||
SetProtocol(string)
|
||||
SetRemoteAddr(string)
|
||||
SetSource(string)
|
||||
SetURL(string)
|
||||
WithRequest(*http.Request)
|
||||
GetSource() string
|
||||
}
|
||||
|
||||
// Connection just like webrtc.PeerConnection
|
||||
// - ID and RemoteAddr used for building Connection(s) graph
|
||||
// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection
|
||||
// - FormatName and Protocol has FFmpeg compatible names
|
||||
// - Transport used for auto closing on Stop
|
||||
type Connection struct {
|
||||
ID uint32 `json:"id,omitempty"`
|
||||
FormatName string `json:"format_name,omitempty"` // rtsp, webrtc, mp4, mjpeg, mpjpeg...
|
||||
Protocol string `json:"protocol,omitempty"` // tcp, udp, http, ws, pipe...
|
||||
RemoteAddr string `json:"remote_addr,omitempty"` // host:port other info
|
||||
Source string `json:"source,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Recv int `json:"bytes_recv,omitempty"`
|
||||
Send int `json:"bytes_send,omitempty"`
|
||||
|
||||
Transport any `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Connection) GetMedias() []*Media {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
||||
for _, receiver := range c.Receivers {
|
||||
if receiver.Codec == codec {
|
||||
return receiver, nil
|
||||
}
|
||||
}
|
||||
receiver := NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, receiver)
|
||||
return receiver, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Stop() error {
|
||||
for _, receiver := range c.Receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
for _, sender := range c.Senders {
|
||||
sender.Close()
|
||||
}
|
||||
if closer, ok := c.Transport.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated:
|
||||
func (c *Connection) Codecs() []*Codec {
|
||||
codecs := make([]*Codec, len(c.Senders))
|
||||
for i, sender := range c.Senders {
|
||||
codecs[i] = sender.Codec
|
||||
}
|
||||
return codecs
|
||||
}
|
||||
|
||||
func (c *Connection) SetProtocol(s string) {
|
||||
c.Protocol = s
|
||||
}
|
||||
|
||||
func (c *Connection) SetRemoteAddr(s string) {
|
||||
if c.RemoteAddr == "" {
|
||||
c.RemoteAddr = s
|
||||
} else {
|
||||
c.RemoteAddr += " forwarded " + s
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connection) SetSource(s string) {
|
||||
c.Source = s
|
||||
}
|
||||
|
||||
func (c *Connection) SetURL(s string) {
|
||||
c.URL = s
|
||||
}
|
||||
|
||||
func (c *Connection) WithRequest(r *http.Request) {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
c.Protocol = "ws"
|
||||
} else {
|
||||
c.Protocol = "http"
|
||||
}
|
||||
|
||||
c.RemoteAddr = r.RemoteAddr
|
||||
if remote := r.Header.Get("X-Forwarded-For"); remote != "" {
|
||||
c.RemoteAddr += " forwarded " + remote
|
||||
}
|
||||
|
||||
c.UserAgent = r.UserAgent()
|
||||
}
|
||||
|
||||
func (c *Connection) GetSource() string {
|
||||
return c.Source
|
||||
}
|
||||
|
||||
// Create like os.Create, init Consumer with existing Transport
|
||||
func Create(w io.Writer) (*Connection, error) {
|
||||
return &Connection{Transport: w}, nil
|
||||
}
|
||||
|
||||
// Open like os.Open, init Producer from existing Transport
|
||||
func Open(r io.Reader) (*Connection, error) {
|
||||
return &Connection{Transport: r}, nil
|
||||
}
|
||||
|
||||
// Dial like net.Dial, init Producer via Dialing
|
||||
func Dial(rawURL string) (*Connection, error) {
|
||||
return &Connection{}, nil
|
||||
}
|
||||
+4
-85
@@ -1,5 +1,7 @@
|
||||
package core
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
DirectionRecvonly = "recvonly"
|
||||
DirectionSendonly = "sendonly"
|
||||
@@ -90,89 +92,6 @@ func (m Mode) String() string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Recv int `json:"recv,omitempty"`
|
||||
Send int `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
UnsupportedCodec = "unsupported codec"
|
||||
WrongMediaDirection = "wrong media direction"
|
||||
)
|
||||
|
||||
type SuperProducer struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Recv int `json:"recv,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SuperProducer) GetMedias() []*Media {
|
||||
return s.Medias
|
||||
}
|
||||
|
||||
func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
||||
for _, receiver := range s.Receivers {
|
||||
if receiver.Codec == codec {
|
||||
return receiver, nil
|
||||
}
|
||||
}
|
||||
receiver := NewReceiver(media, codec)
|
||||
s.Receivers = append(s.Receivers, receiver)
|
||||
return receiver, nil
|
||||
}
|
||||
|
||||
func (s *SuperProducer) Close() error {
|
||||
for _, receiver := range s.Receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SuperConsumer struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Send int `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SuperConsumer) GetMedias() []*Media {
|
||||
return s.Medias
|
||||
}
|
||||
|
||||
func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) {
|
||||
// return 0, nil
|
||||
//}
|
||||
|
||||
func (s *SuperConsumer) Close() error {
|
||||
for _, sender := range s.Senders {
|
||||
sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SuperConsumer) Codecs() []*Codec {
|
||||
codecs := make([]*Codec, len(s.Senders))
|
||||
for i, sender := range s.Senders {
|
||||
codecs[i] = sender.Codec
|
||||
}
|
||||
return codecs
|
||||
func (m Mode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.String())
|
||||
}
|
||||
|
||||
+11
-3
@@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool {
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
||||
return KindAudio
|
||||
@@ -124,9 +124,13 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
|
||||
codec := media.Codecs[0]
|
||||
|
||||
name := codec.Name
|
||||
if name == CodecELD {
|
||||
switch codec.Name {
|
||||
case CodecELD:
|
||||
name = CodecAAC
|
||||
case CodecPCML:
|
||||
name = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server
|
||||
default:
|
||||
name = codec.Name
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
@@ -137,6 +141,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
if media.Direction != "" {
|
||||
md.WithPropertyAttribute(media.Direction)
|
||||
}
|
||||
|
||||
if media.ID != "" {
|
||||
md.WithValueAttribute("control", media.ID)
|
||||
}
|
||||
|
||||
+4
-3
@@ -23,10 +23,11 @@ type Filter func(handler HandlerFunc) HandlerFunc
|
||||
|
||||
// Node - Receiver or Sender or Filter (transform)
|
||||
type Node struct {
|
||||
Codec *Codec `json:"codec"`
|
||||
Input HandlerFunc `json:"-"`
|
||||
Output HandlerFunc `json:"-"`
|
||||
Codec *Codec
|
||||
Input HandlerFunc
|
||||
Output HandlerFunc
|
||||
|
||||
id uint32
|
||||
childs []*Node
|
||||
parent *Node
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package core
|
||||
|
||||
// This code copied from go1.21 for backward support in go1.20.
|
||||
// We need to support go1.20 for Windows 7
|
||||
|
||||
// Index returns the index of the first occurrence of v in s,
|
||||
// or -1 if not present.
|
||||
func Index[S ~[]E, E comparable](s S, v E) int {
|
||||
for i := range s {
|
||||
if v == s[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Contains reports whether v is present in s.
|
||||
func Contains[S ~[]E, E comparable](s S, v E) bool {
|
||||
return Index(s, v) >= 0
|
||||
}
|
||||
|
||||
type Ordered interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
||||
~float32 | ~float64 |
|
||||
~string
|
||||
}
|
||||
|
||||
// Max returns the maximal value in x. It panics if x is empty.
|
||||
// For floating-point E, Max propagates NaNs (any NaN value in x
|
||||
// forces the output to be NaN).
|
||||
func Max[S ~[]E, E Ordered](x S) E {
|
||||
if len(x) < 1 {
|
||||
panic("slices.Max: empty list")
|
||||
}
|
||||
m := x[0]
|
||||
for i := 1; i < len(x); i++ {
|
||||
if x[i] > m {
|
||||
m = x[i]
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
+50
-6
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
@@ -22,7 +23,7 @@ type Receiver struct {
|
||||
|
||||
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
||||
r := &Receiver{
|
||||
Node: Node{Codec: codec},
|
||||
Node: Node{id: NewID(), Codec: codec},
|
||||
Media: media,
|
||||
}
|
||||
r.Input = func(packet *Packet) {
|
||||
@@ -91,7 +92,7 @@ func NewSender(media *Media, codec *Codec) *Sender {
|
||||
|
||||
buf := make(chan *Packet, bufSize)
|
||||
s := &Sender{
|
||||
Node: Node{Codec: codec},
|
||||
Node: Node{id: NewID(), Codec: codec},
|
||||
Media: media,
|
||||
buf: buf,
|
||||
}
|
||||
@@ -139,6 +140,7 @@ func (s *Sender) Start() {
|
||||
s.done = make(chan struct{})
|
||||
|
||||
go func() {
|
||||
// for range on nil chan is OK
|
||||
for packet := range s.buf {
|
||||
s.Output(packet)
|
||||
}
|
||||
@@ -147,7 +149,7 @@ func (s *Sender) Start() {
|
||||
}
|
||||
|
||||
func (s *Sender) Wait() {
|
||||
if done := s.done; s.done != nil {
|
||||
if done := s.done; done != nil {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
@@ -164,10 +166,52 @@ func (s *Sender) State() string {
|
||||
|
||||
func (s *Sender) Close() {
|
||||
// close buffer if exists
|
||||
if buf := s.buf; buf != nil {
|
||||
s.buf = nil
|
||||
defer close(buf)
|
||||
s.mu.Lock()
|
||||
if s.buf != nil {
|
||||
close(s.buf) // exit from for range loop
|
||||
s.buf = nil // prevent writing to closed chan
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
s.Node.Close()
|
||||
}
|
||||
|
||||
func (r *Receiver) MarshalJSON() ([]byte, error) {
|
||||
v := struct {
|
||||
ID uint32 `json:"id"`
|
||||
Codec *Codec `json:"codec"`
|
||||
Childs []uint32 `json:"childs,omitempty"`
|
||||
Bytes int `json:"bytes,omitempty"`
|
||||
Packets int `json:"packets,omitempty"`
|
||||
}{
|
||||
ID: r.Node.id,
|
||||
Codec: r.Node.Codec,
|
||||
Bytes: r.Bytes,
|
||||
Packets: r.Packets,
|
||||
}
|
||||
for _, child := range r.childs {
|
||||
v.Childs = append(v.Childs, child.id)
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (s *Sender) MarshalJSON() ([]byte, error) {
|
||||
v := struct {
|
||||
ID uint32 `json:"id"`
|
||||
Codec *Codec `json:"codec"`
|
||||
Parent uint32 `json:"parent,omitempty"`
|
||||
Bytes int `json:"bytes,omitempty"`
|
||||
Packets int `json:"packets,omitempty"`
|
||||
Drops int `json:"drops,omitempty"`
|
||||
}{
|
||||
ID: s.Node.id,
|
||||
Codec: s.Node.Codec,
|
||||
Bytes: s.Bytes,
|
||||
Packets: s.Packets,
|
||||
Drops: s.Drops,
|
||||
}
|
||||
if s.parent != nil {
|
||||
v.Parent = s.parent.id
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSenser(t *testing.T) {
|
||||
recv := make(chan *Packet) // blocking receiver
|
||||
|
||||
sender := NewSender(nil, &Codec{})
|
||||
sender.Output = func(packet *Packet) {
|
||||
recv <- packet
|
||||
}
|
||||
require.Equal(t, "new", sender.State())
|
||||
|
||||
sender.Start()
|
||||
require.Equal(t, "connected", sender.State())
|
||||
|
||||
sender.Input(&Packet{})
|
||||
sender.Input(&Packet{})
|
||||
|
||||
require.Equal(t, 2, sender.Packets)
|
||||
require.Equal(t, 0, sender.Drops)
|
||||
|
||||
// important to read one before close
|
||||
// because goroutine in Start() can run with nil chan
|
||||
// it's OK in real life, but bad for test
|
||||
_, ok := <-recv
|
||||
require.True(t, ok)
|
||||
|
||||
sender.Close()
|
||||
require.Equal(t, "closed", sender.State())
|
||||
|
||||
sender.Input(&Packet{})
|
||||
|
||||
require.Equal(t, 2, sender.Packets)
|
||||
require.Equal(t, 1, sender.Drops)
|
||||
|
||||
// read 2nd
|
||||
_, ok = <-recv
|
||||
require.True(t, ok)
|
||||
|
||||
// read 3rd
|
||||
select {
|
||||
case <-recv:
|
||||
ok = true
|
||||
default:
|
||||
ok = false
|
||||
}
|
||||
require.False(t, ok)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package doorbird
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Connection
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func Dial(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
|
||||
if u.Port() == "" {
|
||||
u.Host += ":80"
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("POST /bha-api/audio-transmit.cgi?http-user=%s&http-password=%s HTTP/1.0\r\n", user, pass) +
|
||||
"Content-Type: audio/basic\r\n" +
|
||||
"Content-Length: 9999999\r\n" +
|
||||
"Connection: Keep-Alive\r\n" +
|
||||
"Cache-Control: no-cache\r\n" +
|
||||
"\r\n"
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))
|
||||
if _, err = conn.Write([]byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &Client{
|
||||
core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "doorbird",
|
||||
Protocol: "http",
|
||||
URL: rawURL,
|
||||
Medias: medias,
|
||||
Transport: conn,
|
||||
},
|
||||
conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))
|
||||
if n, err := c.conn.Write(pkt.Payload); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() (err error) {
|
||||
_, err = c.conn.Read(nil)
|
||||
return
|
||||
}
|
||||
@@ -8,16 +8,16 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
type Backchannel struct {
|
||||
core.Connection
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() error {
|
||||
func (c *Backchannel) Start() error {
|
||||
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -30,12 +30,7 @@ func (c *Consumer) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if err := c.client.Talk(); err != nil {
|
||||
return err
|
||||
}
|
||||
+11
-6
@@ -8,17 +8,22 @@ func Dial(url string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "dvrip",
|
||||
Protocol: "tcp",
|
||||
RemoteAddr: client.conn.RemoteAddr().String(),
|
||||
Transport: client.conn,
|
||||
}
|
||||
|
||||
if client.stream != "" {
|
||||
prod := &Producer{client: client}
|
||||
prod.Type = "DVRIP active producer"
|
||||
prod := &Producer{Connection: conn, client: client}
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
} else {
|
||||
cons := &Consumer{client: client}
|
||||
cons.Type = "DVRIP active consumer"
|
||||
cons.Medias = []*core.Media{
|
||||
conn.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
@@ -29,6 +34,6 @@ func Dial(url string) (core.Producer, error) {
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons, nil
|
||||
return &Backchannel{Connection: conn, client: client}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
|
||||
client *Client
|
||||
|
||||
@@ -53,7 +53,7 @@ func (c *Producer) Start() error {
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(payload, false),
|
||||
Payload: annexb.EncodeToAVCC(payload),
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
@@ -92,10 +92,6 @@ func (c *Producer) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.client.Play(); err != nil {
|
||||
return err
|
||||
@@ -150,7 +146,7 @@ func (c *Producer) probe() error {
|
||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||
c.videoDT = 90000 / uint32(fps)
|
||||
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
payload := annexb.EncodeToAVCC(b[16:])
|
||||
c.addVideoTrack(b[4], payload)
|
||||
|
||||
case 0xFA: // audio
|
||||
|
||||
+13
-12
@@ -10,17 +10,13 @@ import (
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
core.Connection
|
||||
wr *core.WriteBuffer
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
c := &Consumer{
|
||||
wr: core.NewWriteBuffer(nil),
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
c.Medias = []*core.Media{
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
@@ -36,7 +32,17 @@ func NewConsumer() *Consumer {
|
||||
},
|
||||
},
|
||||
}
|
||||
return c
|
||||
wr := core.NewWriteBuffer(nil)
|
||||
return &Consumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "flv",
|
||||
Medias: medias,
|
||||
Transport: wr,
|
||||
},
|
||||
wr: wr,
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
@@ -86,8 +92,3 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
}
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.wr.Close()
|
||||
}
|
||||
|
||||
+37
-24
@@ -15,18 +15,24 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
rd *core.ReadBuffer
|
||||
|
||||
video, audio *core.Receiver
|
||||
}
|
||||
|
||||
func Open(rd io.Reader) (*Producer, error) {
|
||||
prod := &Producer{rd: core.NewReadBuffer(rd)}
|
||||
prod := &Producer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "flv",
|
||||
Transport: rd,
|
||||
},
|
||||
rd: core.NewReadBuffer(rd),
|
||||
}
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prod.Type = "FLV producer"
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
@@ -57,7 +63,7 @@ const (
|
||||
)
|
||||
|
||||
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
receiver, _ := c.SuperProducer.GetTrack(media, codec)
|
||||
receiver, _ := c.Connection.GetTrack(media, codec)
|
||||
if media.Kind == core.KindVideo {
|
||||
c.video = receiver
|
||||
} else {
|
||||
@@ -117,11 +123,6 @@ func (c *Producer) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
_ = c.SuperProducer.Close()
|
||||
return c.rd.Close()
|
||||
}
|
||||
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.readHeader(); err != nil {
|
||||
return err
|
||||
@@ -139,23 +140,29 @@ func (c *Producer) probe() error {
|
||||
// 1. Empty video/audio flag
|
||||
// 2. MedaData without stereo key for AAC
|
||||
// 3. Audio header after Video keyframe tag
|
||||
waitType := []byte{TagData}
|
||||
timeout := time.Now().Add(core.ProbeTimeout)
|
||||
|
||||
for len(waitType) != 0 && time.Now().Before(timeout) {
|
||||
// OpenIPC camera (on old firmwares) sends:
|
||||
// 1. Empty video/audio flag
|
||||
// 2. No MetaData packet
|
||||
// 3. Sends a video packet in more than 3 seconds
|
||||
waitVideo := true
|
||||
waitAudio := true
|
||||
timeout := time.Now().Add(time.Second * 5)
|
||||
|
||||
for (waitVideo || waitAudio) && time.Now().Before(timeout) {
|
||||
pkt, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {
|
||||
continue
|
||||
} else {
|
||||
waitType = append(waitType[:i], waitType[i+1:]...)
|
||||
}
|
||||
//log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload)
|
||||
|
||||
switch pkt.PayloadType {
|
||||
case TagAudio:
|
||||
if !waitAudio {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = pkt.Payload[1] // bounds
|
||||
|
||||
codecID := pkt.Payload[0] >> 4 // SoundFormat
|
||||
@@ -178,8 +185,13 @@ func (c *Producer) probe() error {
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
waitAudio = false
|
||||
|
||||
case TagVideo:
|
||||
if !waitVideo {
|
||||
continue
|
||||
}
|
||||
|
||||
var codec *core.Codec
|
||||
|
||||
if isExHeader(pkt.Payload) {
|
||||
@@ -212,19 +224,20 @@ func (c *Producer) probe() error {
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
waitVideo = false
|
||||
|
||||
case TagData:
|
||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||
waitType = append(waitType, TagData)
|
||||
continue
|
||||
}
|
||||
// Dahua cameras doesn't send videocodecid
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("width")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitType = append(waitType, TagVideo)
|
||||
if !bytes.Contains(pkt.Payload, []byte("videocodecid")) &&
|
||||
!bytes.Contains(pkt.Payload, []byte("width")) &&
|
||||
!bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitVideo = false
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
waitType = append(waitType, TagAudio)
|
||||
if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
waitAudio = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user