Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| b389d0eb9c | |||
| bf303ed471 | |||
| cd777ba2b4 | |||
| e3188a0a6d | |||
| 2bab0a014d | |||
| a01da18018 | |||
| 9d5a5c1e45 | |||
| 8377ad1d05 | |||
| ec33796bd3 | |||
| 31e4ba2722 | |||
| e0b1a50356 | |||
| 9bb36ebb6c | |||
| 756be9801e | |||
| bd73b07ed8 | |||
| df1d44d24e | |||
| 79245eeff4 | |||
| aa86c1ec25 | |||
| 2ab1d9d774 | |||
| a9e7a73cc8 | |||
| ea17b420d6 | |||
| 660979dfda | |||
| a6b9b4993f | |||
| cc74504ed8 | |||
| 791239be12 | |||
| a79061c7c2 | |||
| 50ad3b20c4 | |||
| 649de0131c | |||
| 8cb513cb89 | |||
| 3932dbaa84 | |||
| 4534b4d8ca | |||
| 8e571a66e3 | |||
| 0ccfcb0ec0 | |||
| 8bae4631d2 | |||
| 268629f551 | |||
| 0bd2fcde54 | |||
| 6f34cf0c95 | |||
| f8bc25d0ae | |||
| 8749562c96 | |||
| d9d2bdff44 | |||
| b3e9ed23ac | |||
| bf3f81ccac | |||
| ff39e2e496 | |||
| d2346a2aed | |||
| 8f57b1acb6 | |||
| 6fafd10482 | |||
| c726651b8b | |||
| 02af2e2849 | |||
| 6d9c7012b0 | |||
| 8a7712a4c8 | |||
| 82fa803a37 | |||
| 78a74da8d6 | |||
| 53242ea02f | |||
| af05083a1f | |||
| c41bddbbea | |||
| 54c8ca0112 | |||
| a518488289 | |||
| 99cc21aacb | |||
| bc8295baee | |||
| 50f9913c41 | |||
| 4c135b5a46 | |||
| 686fb374e9 | |||
| 2b3e6a2730 | |||
| 9143729042 | |||
| 3952f0ba0f | |||
| 7a131822db | |||
| b2399f3bb3 | |||
| 2a8a3f1cbf | |||
| b1ba5bab62 | |||
| 6878f05e57 | |||
| d428a8964a | |||
| f432e72dd0 | |||
| 2929db9cec | |||
| 6d967bc1f9 | |||
| 83c0053b2c | |||
| ecfd7404f5 | |||
| 41badbfb8e | |||
| 0cb013a7fd | |||
| 75020d4df7 | |||
| 69c288b154 | |||
| 0ea651db62 | |||
| 4823e60a92 | |||
| c4949eb81f | |||
| aa4c81c266 | |||
| 063fef5813 | |||
| d9fb734c85 | |||
| a51156cf18 | |||
| 32e0ee4a10 | |||
| e6bea97936 | |||
| 9776e09ca7 | |||
| ad273d3a98 | |||
| 69c301e79f | |||
| 8f2bb3f34b | |||
| e4ff6d224f | |||
| 00751459a2 | |||
| 874c07b887 | |||
| 152df3ef5d | |||
| c950bb0252 | |||
| dd7ea2657a | |||
| 5889791847 | |||
| 9160403b99 | |||
| 5ccbd7c1c2 | |||
| 778245dd1c | |||
| 205018c96a | |||
| eaba451a47 | |||
| b7c11db604 | |||
| f7b98044e6 | |||
| 1b1bdb37db | |||
| ab453d275e | |||
| ee387b79e1 | |||
| e71ed5e7eb | |||
| 122a550599 | |||
| f3f08afac8 | |||
| a0030194cb | |||
| f158ffb33e | |||
| abe617a346 | |||
| e080eac204 |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with: { go-version: '1.21' }
|
with: { go-version: '1.22' }
|
||||||
|
|
||||||
- name: Build go2rtc_win64
|
- name: Build go2rtc_win64
|
||||||
env: { GOOS: windows, GOARCH: amd64 }
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
@@ -123,7 +123,9 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}},enable=false
|
type=semver,pattern={{version}},enable=false
|
||||||
@@ -142,6 +144,14 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -168,10 +178,12 @@ jobs:
|
|||||||
id: meta-hw
|
id: meta-hw
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
flavor: |
|
flavor: |
|
||||||
suffix=-hardware
|
suffix=-hardware,onlatest=true
|
||||||
latest=false
|
latest=auto
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}},enable=false
|
type=semver,pattern={{version}},enable=false
|
||||||
@@ -190,6 +202,14 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
|
go2rtc_linux*
|
||||||
|
go2rtc_mac*
|
||||||
|
go2rtc_win*
|
||||||
|
|
||||||
0_test.go
|
0_test.go
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+3
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.11"
|
||||||
ARG GO_VERSION="1.21"
|
ARG GO_VERSION="1.22"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
@@ -20,6 +20,8 @@ ENV GOARCH=${TARGETARCH}
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
# Cache dependencies
|
# Cache dependencies
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
|
|
||||||

|

|
||||||
<br>
|
<br>
|
||||||
[](https://github.com/AlexxIT/go2rtc/stargazers)
|
[](https://github.com/AlexxIT/go2rtc/stargazers)
|
||||||
[](https://hub.docker.com/r/alexxit/go2rtc)
|
[](https://hub.docker.com/r/alexxit/go2rtc)
|
||||||
@@ -131,7 +131,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
|||||||
|
|
||||||
### go2rtc: Docker
|
### go2rtc: Docker
|
||||||
|
|
||||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo).
|
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok), and [Python](#source-echo).
|
||||||
|
|
||||||
### go2rtc: Home Assistant Add-on
|
### go2rtc: Home Assistant Add-on
|
||||||
|
|
||||||
@@ -429,6 +429,7 @@ streams:
|
|||||||
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||||
picam_h264: exec:libcamera-vid -t 0 --inline -o -
|
picam_h264: exec:libcamera-vid -t 0 --inline -o -
|
||||||
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
|
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
|
||||||
|
pi5cam_h264: exec:libcamera-vid -t 0 --libav-format h264 -o -
|
||||||
canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5
|
canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5
|
||||||
play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1
|
play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1
|
||||||
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
|
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
|
||||||
@@ -552,11 +553,16 @@ echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
|
|||||||
|
|
||||||
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
||||||
|
|
||||||
|
- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`
|
||||||
|
- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
|
kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tested: KD110, KC200, KC401, KC420WS, EC71.
|
||||||
|
|
||||||
#### Source: GoPro
|
#### Source: GoPro
|
||||||
|
|
||||||
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
|
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
|
||||||
@@ -773,7 +779,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
|||||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||||
|
|
||||||
- Supported codecs: H264 for video and AAC for audio
|
- Supported codecs: H264 for video and AAC for audio
|
||||||
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
|
- AAC audio is required for YouTube, videos without audio will not work
|
||||||
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||||
|
|
||||||
You can use API:
|
You can use API:
|
||||||
@@ -786,16 +792,19 @@ Or config file:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
publish:
|
publish:
|
||||||
# publish stream "tplink_tapo" to Telegram
|
# publish stream "video_audio_transcode" to Telegram
|
||||||
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
video_audio_transcode:
|
||||||
# publish stream "other_camera" to Telegram and YouTube
|
|
||||||
other_camera:
|
|
||||||
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
# publish stream "audio_transcode" to Telegram and YouTube
|
||||||
|
audio_transcode:
|
||||||
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
- rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||||
|
|
||||||
streams:
|
streams:
|
||||||
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
|
video_audio_transcode:
|
||||||
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
||||||
|
audio_transcode:
|
||||||
|
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||||
@@ -1187,6 +1196,10 @@ API examples:
|
|||||||
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
|
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
|
||||||
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
|
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
|
||||||
|
|
||||||
|
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/mjpeg/README.md)):
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
||||||
|
|
||||||
### Module: Log
|
### Module: Log
|
||||||
|
|
||||||
You can set different log levels for different modules.
|
You can set different log levels for different modules.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := rtsp.NewClient(os.Args[1])
|
||||||
|
if err := client.Dial(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||||
|
},
|
||||||
|
ID: "streamid=0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.Announce(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if _, err := client.SetupMedia(client.Medias[0]); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if err := client.Record(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.RunUntilSignal()
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astits v1.13.0
|
github.com/asticode/go-astits v1.13.0
|
||||||
github.com/expr-lang/expr v1.16.5
|
github.com/expr-lang/expr v1.16.9
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/miekg/dns v1.1.59
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/pion/ice/v2 v2.3.24
|
github.com/pion/ice/v2 v2.3.24
|
||||||
github.com/pion/interceptor v0.1.29
|
github.com/pion/interceptor v0.1.29
|
||||||
@@ -15,12 +16,12 @@ require (
|
|||||||
github.com/pion/srtp/v2 v2.0.18
|
github.com/pion/srtp/v2 v2.0.18
|
||||||
github.com/pion/stun v0.6.1
|
github.com/pion/stun v0.6.1
|
||||||
github.com/pion/webrtc/v3 v3.2.40
|
github.com/pion/webrtc/v3 v3.2.40
|
||||||
github.com/rs/zerolog v1.32.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
golang.org/x/crypto v0.23.0
|
golang.org/x/crypto v0.24.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/pion/datachannel v1.5.6 // indirect
|
github.com/pion/datachannel v1.5.6 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.11 // indirect
|
github.com/pion/dtls/v2 v2.2.11 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
@@ -40,9 +40,9 @@ require (
|
|||||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.26.0 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.20.0 // indirect
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ 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/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 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
||||||
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
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/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.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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -85,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
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 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
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=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
@@ -115,10 +119,14 @@ 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.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 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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.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 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -134,6 +142,8 @@ 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.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
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.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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -160,6 +170,8 @@ 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.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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -184,6 +196,8 @@ 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.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 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
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
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
# https://packages.debian.org/trixie/ffmpeg
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
ARG DEBIAN_VERSION="trixie-slim"
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
ARG GO_VERSION="1.21-bookworm"
|
ARG GO_VERSION="1.22-bookworm"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func initWS(origin string) {
|
|||||||
if o.Host == r.Host {
|
if o.Host == r.Host {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
|
||||||
// https://github.com/AlexxIT/go2rtc/issues/118
|
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||||
return o.Host[:i] == r.Host
|
return o.Host[:i] == r.Host
|
||||||
@@ -127,7 +127,7 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
|
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||||
|
|
||||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
|
||||||
|
- go2rtc support multiple config files:
|
||||||
|
- `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml`
|
||||||
|
- go2rtc support inline config as multiple formats from command line:
|
||||||
|
- **YAML**: `go2rtc -c '{log: {format: text}}'`
|
||||||
|
- **JSON**: `go2rtc -c '{"log":{"format":"text"}}'`
|
||||||
|
- **key=value**: `go2rtc -c log.format=text`
|
||||||
|
- Every next config will overwrite previous (but only defined params)
|
||||||
|
|
||||||
|
```
|
||||||
|
go2rtc -config "{log: {format: text}}" -config /config/go2rtc.yaml -config "{rtsp: {listen: ''}}" -config /usr/local/go2rtc/go2rtc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
or simple version
|
||||||
|
|
||||||
|
```
|
||||||
|
go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Also go2rtc support templates for using environment variables in any part of config:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Schema
|
||||||
|
|
||||||
|
Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) supports autocomplete and syntax validation.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/website/schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
- Default values may change in updates
|
||||||
|
- FFmpeg module has many presets, they are not listed here because they may also change in updates
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api:
|
||||||
|
listen: ":1984"
|
||||||
|
|
||||||
|
ffmpeg:
|
||||||
|
bin: "ffmpeg"
|
||||||
|
|
||||||
|
log:
|
||||||
|
format: "color"
|
||||||
|
level: "info"
|
||||||
|
output: "stdout"
|
||||||
|
time: "UNIXMS"
|
||||||
|
|
||||||
|
rtsp:
|
||||||
|
listen: ":8554"
|
||||||
|
default_query: "video&audio"
|
||||||
|
|
||||||
|
srtp:
|
||||||
|
listen: ":8443"
|
||||||
|
|
||||||
|
webrtc:
|
||||||
|
listen: ":8555/tcp"
|
||||||
|
ice_servers:
|
||||||
|
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||||
|
```
|
||||||
+62
-105
@@ -1,144 +1,101 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.9.1"
|
var (
|
||||||
var UserAgent = "go2rtc/" + Version
|
Version string
|
||||||
|
UserAgent string
|
||||||
|
ConfigPath string
|
||||||
|
Info = make(map[string]any)
|
||||||
|
)
|
||||||
|
|
||||||
var ConfigPath string
|
const usage = `Usage of go2rtc:
|
||||||
var Info = map[string]any{
|
|
||||||
"version": Version,
|
-c, --config Path to config file or config string as YAML or JSON, support multiple
|
||||||
}
|
-d, --daemon Run in background
|
||||||
|
-v, --version Print version and exit
|
||||||
|
`
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var confs Config
|
var config flagConfig
|
||||||
var daemon bool
|
var daemon bool
|
||||||
var version bool
|
var version bool
|
||||||
|
|
||||||
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
flag.Var(&config, "config", "")
|
||||||
if runtime.GOOS != "windows" {
|
flag.Var(&config, "c", "")
|
||||||
flag.BoolVar(&daemon, "daemon", false, "Run program in background")
|
flag.BoolVar(&daemon, "daemon", false, "")
|
||||||
}
|
flag.BoolVar(&daemon, "d", false, "")
|
||||||
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
flag.BoolVar(&version, "version", false, "")
|
||||||
|
flag.BoolVar(&version, "v", false, "")
|
||||||
|
|
||||||
|
flag.Usage = func() { fmt.Print(usage) }
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
revision, vcsTime := readRevisionTime()
|
||||||
|
|
||||||
if version {
|
if version {
|
||||||
fmt.Println("Current version:", Version)
|
fmt.Printf("go2rtc version %s (%s) %s/%s\n", Version, revision, runtime.GOOS, runtime.GOARCH)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if daemon {
|
if daemon && os.Getppid() != 1 {
|
||||||
args := os.Args[1:]
|
if runtime.GOOS == "windows" {
|
||||||
for i, arg := range args {
|
fmt.Println("Daemon mode is not supported on Windows")
|
||||||
if arg == "-daemon" {
|
os.Exit(1)
|
||||||
args[i] = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-run the program in background and exit
|
// 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 {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Fatal().Err(err).Send()
|
fmt.Println("Failed to start daemon:", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if confs == nil {
|
UserAgent = "go2rtc/" + Version
|
||||||
confs = []string{"go2rtc.yaml"}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, conf := range confs {
|
Info["version"] = Version
|
||||||
if conf[0] != '{' {
|
Info["revision"] = revision
|
||||||
// config as file
|
|
||||||
if ConfigPath == "" {
|
|
||||||
ConfigPath = conf
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := os.ReadFile(conf)
|
initConfig(config)
|
||||||
if data == nil {
|
initLogger()
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
configs = append(configs, data)
|
Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
|
||||||
} else {
|
Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
|
||||||
// config as raw YAML
|
|
||||||
configs = append(configs, []byte(conf))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ConfigPath != "" {
|
if ConfigPath != "" {
|
||||||
if !filepath.IsAbs(ConfigPath) {
|
Logger.Info().Str("path", ConfigPath).Msg("config")
|
||||||
if cwd, err := os.Getwd(); err == nil {
|
}
|
||||||
ConfigPath = filepath.Join(cwd, ConfigPath)
|
}
|
||||||
|
|
||||||
|
func readRevisionTime() (revision, vcsTime string) {
|
||||||
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
for _, setting := range info.Settings {
|
||||||
|
switch setting.Key {
|
||||||
|
case "vcs.revision":
|
||||||
|
if len(setting.Value) > 7 {
|
||||||
|
revision = setting.Value[:7]
|
||||||
|
} else {
|
||||||
|
revision = setting.Value
|
||||||
|
}
|
||||||
|
case "vcs.time":
|
||||||
|
vcsTime = setting.Value
|
||||||
|
case "vcs.modified":
|
||||||
|
if setting.Value == "true" {
|
||||||
|
revision = "mod." + revision
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Info["config_path"] = ConfigPath
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]string `yaml:"log"`
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadConfig(&cfg)
|
|
||||||
|
|
||||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
|
||||||
|
|
||||||
modules = cfg.Mod
|
|
||||||
|
|
||||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
|
||||||
|
|
||||||
migrateStore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(v any) {
|
|
||||||
for _, data := range configs {
|
|
||||||
if err := yaml.Unmarshal(data, v); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[app] read config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PatchConfig(key string, value any, path ...string) error {
|
|
||||||
if ConfigPath == "" {
|
|
||||||
return errors.New("config file disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// empty config is OK
|
|
||||||
b, _ := os.ReadFile(ConfigPath)
|
|
||||||
|
|
||||||
b, err := yaml.Patch(b, key, value, path...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(ConfigPath, b, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal
|
|
||||||
|
|
||||||
type Config []string
|
|
||||||
|
|
||||||
func (c *Config) String() string {
|
|
||||||
return strings.Join(*c, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Set(value string) error {
|
|
||||||
*c = append(*c, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var configs [][]byte
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadConfig(v any) {
|
||||||
|
for _, data := range configs {
|
||||||
|
if err := yaml.Unmarshal(data, v); err != nil {
|
||||||
|
Logger.Warn().Err(err).Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatchConfig(key string, value any, path ...string) error {
|
||||||
|
if ConfigPath == "" {
|
||||||
|
return errors.New("config file disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty config is OK
|
||||||
|
b, _ := os.ReadFile(ConfigPath)
|
||||||
|
|
||||||
|
b, err := yaml.Patch(b, key, value, path...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(ConfigPath, b, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
type flagConfig []string
|
||||||
|
|
||||||
|
func (c *flagConfig) String() string {
|
||||||
|
return strings.Join(*c, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flagConfig) Set(value string) error {
|
||||||
|
*c = append(*c, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs [][]byte
|
||||||
|
|
||||||
|
func initConfig(confs flagConfig) {
|
||||||
|
if confs == nil {
|
||||||
|
confs = []string{"go2rtc.yaml"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range confs {
|
||||||
|
if len(conf) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conf[0] == '{' {
|
||||||
|
// config as raw YAML or JSON
|
||||||
|
configs = append(configs, []byte(conf))
|
||||||
|
} else if data := parseConfString(conf); data != nil {
|
||||||
|
configs = append(configs, data)
|
||||||
|
} else {
|
||||||
|
// config as file
|
||||||
|
if ConfigPath == "" {
|
||||||
|
ConfigPath = conf
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, _ = os.ReadFile(conf); data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(shell.ReplaceEnvVars(string(data)))
|
||||||
|
configs = append(configs, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ConfigPath != "" {
|
||||||
|
if !filepath.IsAbs(ConfigPath) {
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
ConfigPath = filepath.Join(cwd, ConfigPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Info["config_path"] = ConfigPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfString(s string) []byte {
|
||||||
|
i := strings.IndexByte(s, '=')
|
||||||
|
if i < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
items := strings.Split(s[:i], ".")
|
||||||
|
if len(items) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// `log.level=trace` => `{log: {level: trace}}`
|
||||||
|
var pre string
|
||||||
|
var suf = s[i+1:]
|
||||||
|
for _, item := range items {
|
||||||
|
pre += "{" + item + ": "
|
||||||
|
suf += "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(pre + suf)
|
||||||
|
}
|
||||||
+80
-29
@@ -4,49 +4,100 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var MemoryLog *circularBuffer
|
var MemoryLog = newBuffer(16)
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
|
||||||
var writer io.Writer = os.Stdout
|
|
||||||
|
|
||||||
if format != "json" {
|
|
||||||
writer = zerolog.ConsoleWriter{
|
|
||||||
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MemoryLog = newBuffer(16)
|
|
||||||
|
|
||||||
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
|
||||||
lvl = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
func GetLogger(module string) zerolog.Logger {
|
||||||
if s, ok := modules[module]; ok {
|
if s, ok := modules[module]; ok {
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
lvl, err := zerolog.ParseLevel(s)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return log.Level(lvl)
|
return Logger.Level(lvl)
|
||||||
}
|
}
|
||||||
log.Warn().Err(err).Caller().Send()
|
Logger.Warn().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
return log.Logger
|
return Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initLogger support:
|
||||||
|
// - output: empty (only to memory), stderr, stdout
|
||||||
|
// - format: empty (autodetect color support), color, json, text
|
||||||
|
// - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO
|
||||||
|
// - level: disabled, trace, debug, info, warn, error...
|
||||||
|
func initLogger() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod map[string]string `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Mod = modules // defaults
|
||||||
|
|
||||||
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
|
var writer io.Writer
|
||||||
|
|
||||||
|
switch modules["output"] {
|
||||||
|
case "stderr":
|
||||||
|
writer = os.Stderr
|
||||||
|
case "stdout":
|
||||||
|
writer = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
timeFormat := modules["time"]
|
||||||
|
|
||||||
|
if writer != nil {
|
||||||
|
if format := modules["format"]; format != "json" {
|
||||||
|
console := &zerolog.ConsoleWriter{Out: writer}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "text":
|
||||||
|
console.NoColor = true
|
||||||
|
case "color":
|
||||||
|
console.NoColor = false // useless, but anyway
|
||||||
|
default:
|
||||||
|
// autodetection if output support color
|
||||||
|
// go-isatty - dependency for go-colorable - dependency for ConsoleWriter
|
||||||
|
console.NoColor = !isatty.IsTerminal(writer.(*os.File).Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeFormat != "" {
|
||||||
|
console.TimeFormat = "15:04:05.000"
|
||||||
|
} else {
|
||||||
|
console.PartsOrder = []string{
|
||||||
|
zerolog.LevelFieldName,
|
||||||
|
zerolog.CallerFieldName,
|
||||||
|
zerolog.MessageFieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = console
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
||||||
|
} else {
|
||||||
|
writer = MemoryLog
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||||
|
Logger = zerolog.New(writer).Level(lvl)
|
||||||
|
|
||||||
|
if timeFormat != "" {
|
||||||
|
zerolog.TimeFieldFormat = timeFormat
|
||||||
|
Logger = Logger.With().Timestamp().Logger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Logger zerolog.Logger
|
||||||
|
|
||||||
// modules log levels
|
// modules log levels
|
||||||
var modules map[string]string
|
var modules = map[string]string{
|
||||||
|
"format": "", // useless, but anyway
|
||||||
|
"level": "info",
|
||||||
|
"output": "stdout", // TODO: change to stderr someday
|
||||||
|
"time": zerolog.TimeFormatUnixMs,
|
||||||
|
}
|
||||||
|
|
||||||
const chunkSize = 1 << 16
|
const chunkSize = 1 << 16
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func migrateStore() {
|
|
||||||
const name = "go2rtc.json"
|
|
||||||
|
|
||||||
data, _ := os.ReadFile(name)
|
|
||||||
if data == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var store struct {
|
|
||||||
Streams map[string]string `json:"streams"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &store); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for id, url := range store.Streams {
|
|
||||||
if err := PatchConfig(id, url, "streams"); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = os.Remove(name)
|
|
||||||
}
|
|
||||||
@@ -7,13 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("bubble", handle)
|
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
|
||||||
}
|
return bubble.Dial(source)
|
||||||
|
})
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn := bubble.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,8 @@ package debug
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/stack", stackHandler)
|
api.HandleFunc("api/stack", stackHandler)
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullHandler(string) (core.Producer, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-15
@@ -10,26 +10,16 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("dvrip", handle)
|
streams.HandleFunc("dvrip", dvrip.Dial)
|
||||||
|
|
||||||
// DVRIP client autodiscovery
|
// DVRIP client autodiscovery
|
||||||
api.HandleFunc("api/dvrip", apiDvrip)
|
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
|
const Port = 34569 // UDP port number for dvrip discovery
|
||||||
|
|
||||||
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -92,10 +82,7 @@ func sendBroadcasts(conn *net.UDPConn) {
|
|||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
_, _ = conn.WriteToUDP(data, addr)
|
||||||
if _, err = conn.WriteToUDP(data, addr); err != nil {
|
|
||||||
log.Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// closer support custom killsignal with custom killtimeout
|
||||||
|
type closer struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
query url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *closer) Close() (err error) {
|
||||||
|
sig := os.Kill
|
||||||
|
if s := c.query.Get("killsignal"); s != "" {
|
||||||
|
sig = syscall.Signal(core.Atoi(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[exec] kill with signal=%d", sig)
|
||||||
|
err = c.cmd.Process.Signal(sig)
|
||||||
|
|
||||||
|
if s := c.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)
|
||||||
|
_ = c.cmd.Process.Kill()
|
||||||
|
})
|
||||||
|
defer timer.Stop() // stop timer if Wait ends before timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(err, c.cmd.Wait())
|
||||||
|
}
|
||||||
+100
-43
@@ -1,6 +1,7 @@
|
|||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -49,8 +51,10 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execHandle(rawURL string) (core.Producer, error) {
|
func execHandle(rawURL string) (core.Producer, error) {
|
||||||
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
|
||||||
var path string
|
var path string
|
||||||
var query url.Values
|
|
||||||
|
|
||||||
// RTSP flow should have `{output}` inside URL
|
// RTSP flow should have `{output}` inside URL
|
||||||
// pipe flow may have `#{params}` inside URL
|
// pipe flow may have `#{params}` inside URL
|
||||||
@@ -62,60 +66,73 @@ func execHandle(rawURL string) (core.Producer, error) {
|
|||||||
sum := md5.Sum([]byte(rawURL))
|
sum := md5.Sum([]byte(rawURL))
|
||||||
path = "/" + hex.EncodeToString(sum[:])
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
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:`
|
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
if log.Debug().Enabled() {
|
cmd.Stderr = &logWriter{
|
||||||
cmd.Stderr = os.Stderr
|
buf: make([]byte, 512),
|
||||||
|
debug: log.Debug().Enabled(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
return handlePipe(rawURL, cmd, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleRTSP(rawURL, cmd, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
|
||||||
if query.Get("backchannel") == "1" {
|
if query.Get("backchannel") == "1" {
|
||||||
return stdin.NewClient(cmd)
|
return stdin.NewClient(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := PipeCloser(cmd, query)
|
cl := &closer{cmd: cmd, query: query}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return handlePipe(rawURL, cmd, cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleRTSP(rawURL, cmd, cl, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc := struct {
|
||||||
|
io.Reader
|
||||||
|
io.Closer
|
||||||
|
}{
|
||||||
|
// add buffer for pipe reader to reduce syscall
|
||||||
|
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||||
|
cl,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
if err = cmd.Start(); err != nil {
|
if err = cmd.Start(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prod, err := magic.Open(r)
|
prod, err := magic.Open(rc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = r.Close()
|
_ = rc.Close()
|
||||||
|
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return prod, err
|
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, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) {
|
||||||
stderr := limitBuffer{buf: make([]byte, 512)}
|
|
||||||
|
|
||||||
if cmd.Stderr != nil {
|
|
||||||
cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr)
|
|
||||||
} else {
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
waiter := make(chan core.Producer)
|
waiter := make(chan *pkg.Conn, 1)
|
||||||
|
|
||||||
waitersMu.Lock()
|
waitersMu.Lock()
|
||||||
waiters[path] = waiter
|
waiters[path] = waiter
|
||||||
@@ -127,12 +144,12 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
waitersMu.Unlock()
|
waitersMu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run")
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp")
|
||||||
|
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,15 +159,17 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 60):
|
case <-time.After(time.Minute):
|
||||||
_ = cmd.Process.Kill()
|
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
_ = cl.Close()
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("exec: timeout")
|
||||||
case <-done:
|
case <-done:
|
||||||
// limit message size
|
// limit message size
|
||||||
return nil, errors.New("exec: " + stderr.String())
|
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||||
case prod := <-waiter:
|
case prod := <-waiter:
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||||
|
setRemoteInfo(prod, source, cmd.Args)
|
||||||
|
prod.OnClose = cl.Close
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,25 +178,63 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
waiters = map[string]chan core.Producer{}
|
waiters = make(map[string]chan *pkg.Conn)
|
||||||
waitersMu sync.Mutex
|
waitersMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
type limitBuffer struct {
|
type logWriter struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
n int
|
debug bool
|
||||||
|
n int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *limitBuffer) String() string {
|
func (l *logWriter) String() string {
|
||||||
if l.n == len(l.buf) {
|
if l.n == len(l.buf) {
|
||||||
return string(l.buf) + "..."
|
return string(l.buf) + "..."
|
||||||
}
|
}
|
||||||
return string(l.buf[:l.n])
|
return string(l.buf[:l.n])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *limitBuffer) Write(p []byte) (int, error) {
|
func (l *logWriter) Write(p []byte) (n int, err error) {
|
||||||
if l.n < cap(l.buf) {
|
if l.n < cap(l.buf) {
|
||||||
l.n += copy(l.buf[l.n:], p)
|
l.n += copy(l.buf[l.n:], p)
|
||||||
}
|
}
|
||||||
return len(p), nil
|
n = len(p)
|
||||||
|
if l.debug {
|
||||||
|
if p = trimSpace(p); p != nil {
|
||||||
|
log.Debug().Msgf("[exec] %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpace(b []byte) []byte {
|
||||||
|
start := 0
|
||||||
|
stop := len(b)
|
||||||
|
for ; start < stop; start++ {
|
||||||
|
if b[start] >= ' ' {
|
||||||
|
break // trim all ASCII before 0x20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; ; stop-- {
|
||||||
|
if stop == start {
|
||||||
|
return nil // skip empty output
|
||||||
|
}
|
||||||
|
if b[stop-1] > ' ' {
|
||||||
|
break // trim all ASCII before 0x21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[start:stop]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRemoteInfo(info core.Info, source string, args []string) {
|
||||||
|
info.SetSource(source)
|
||||||
|
|
||||||
|
if i := slices.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()
|
|
||||||
}
|
|
||||||
@@ -45,6 +45,13 @@
|
|||||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TTS
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma
|
||||||
|
```
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
dst := query.Get("dst")
|
||||||
|
stream := streams.Get(dst)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var src string
|
||||||
|
if s := query.Get("file"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto#input=file"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("live"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("text"); s != "" {
|
||||||
|
if strings.IndexAny(s, `'"&%$`) < 0 {
|
||||||
|
src = "ffmpeg:tts?text=" + s
|
||||||
|
if s = query.Get("voice"); s != "" {
|
||||||
|
src += "&voice=" + s
|
||||||
|
}
|
||||||
|
src += "#audio=auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if src == "" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stream.Play(src); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build freebsd || netbsd || openbsd || dragonfly
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build darwin || ios
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
@@ -17,24 +15,15 @@ func Init(bin string) {
|
|||||||
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
func GetInput(src string) string {
|
||||||
i := strings.IndexByte(src, '?')
|
query, err := url.ParseQuery(src)
|
||||||
if i < 0 {
|
|
||||||
return "", errors.New("empty query: " + src)
|
|
||||||
}
|
|
||||||
|
|
||||||
query, err := url.ParseQuery(src[i+1:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
runonce.Do(initDevices)
|
runonce.Do(initDevices)
|
||||||
|
|
||||||
if input := queryToInput(query); input != "" {
|
return queryToInput(query)
|
||||||
return input, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("wrong query: " + src)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Bin string
|
var Bin string
|
||||||
|
|||||||
+76
-21
@@ -2,35 +2,58 @@ package ffmpeg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]string `yaml:"ffmpeg"`
|
Mod map[string]string `yaml:"ffmpeg"`
|
||||||
|
Log struct {
|
||||||
|
Level string `yaml:"ffmpeg"`
|
||||||
|
} `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Mod = defaults // will be overriden from yaml
|
cfg.Mod = defaults // will be overriden from yaml
|
||||||
|
cfg.Log.Level = "error"
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
log = app.GetLogger("ffmpeg")
|
||||||
defaults["global"] += " -v error"
|
|
||||||
|
// zerolog levels: trace debug info warn error fatal panic disabled
|
||||||
|
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
|
||||||
|
if cfg.Log.Level == "warn" {
|
||||||
|
cfg.Log.Level = "warning"
|
||||||
}
|
}
|
||||||
|
defaults["global"] += " -v " + cfg.Log.Level
|
||||||
|
|
||||||
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
||||||
|
if _, err := Version(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
args := parseArgs(url[7:])
|
args := parseArgs(url[7:])
|
||||||
|
if slices.Contains(args.Codecs, "auto") {
|
||||||
|
return "", nil // force call streams.HandleFunc("ffmpeg")
|
||||||
|
}
|
||||||
return "exec:" + args.String(), nil
|
return "exec:" + args.String(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("ffmpeg", NewProducer)
|
||||||
|
|
||||||
|
api.HandleFunc("api/ffmpeg", apiFFmpeg)
|
||||||
|
|
||||||
device.Init(defaults["bin"])
|
device.Init(defaults["bin"])
|
||||||
hardware.Init(defaults["bin"])
|
hardware.Init(defaults["bin"])
|
||||||
}
|
}
|
||||||
@@ -49,16 +72,25 @@ var defaults = map[string]string{
|
|||||||
// output
|
// output
|
||||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||||
"output/mjpeg": "-f mjpeg -",
|
"output/mjpeg": "-f mjpeg -",
|
||||||
|
"output/raw": "-f yuv4mpegpipe -",
|
||||||
|
"output/aac": "-f adts -",
|
||||||
|
"output/wav": "-f wav -",
|
||||||
|
|
||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||||
// `-tune zerolatency` - for minimal latency
|
// `-tune zerolatency` - for minimal latency
|
||||||
// `-profile high -level 4.1` - most used streaming profile
|
// `-profile high -level 4.1` - most used streaming profile
|
||||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||||
"mjpeg": "-c:v mjpeg",
|
"mjpeg": "-c:v mjpeg",
|
||||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||||
|
|
||||||
|
"raw": "-c:v rawvideo",
|
||||||
|
"raw/gray8": "-c:v rawvideo -pix_fmt:v gray8",
|
||||||
|
"raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p",
|
||||||
|
"raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p",
|
||||||
|
"raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p",
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||||
// https://github.com/pion/webrtc/issues/1514
|
// https://github.com/pion/webrtc/issues/1514
|
||||||
// https://ffmpeg.org/ffmpeg-resampler.html
|
// https://ffmpeg.org/ffmpeg-resampler.html
|
||||||
@@ -116,6 +148,8 @@ var defaults = map[string]string{
|
|||||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
// configTemplate - return template from config (defaults) if exist or return raw template
|
// configTemplate - return template from config (defaults) if exist or return raw template
|
||||||
func configTemplate(template string) string {
|
func configTemplate(template string) string {
|
||||||
if s := defaults[template]; s != "" {
|
if s := defaults[template]; s != "" {
|
||||||
@@ -140,9 +174,10 @@ func inputTemplate(name, s string, query url.Values) string {
|
|||||||
func parseArgs(s string) *ffmpeg.Args {
|
func parseArgs(s string) *ffmpeg.Args {
|
||||||
// init FFmpeg arguments
|
// init FFmpeg arguments
|
||||||
args := &ffmpeg.Args{
|
args := &ffmpeg.Args{
|
||||||
Bin: defaults["bin"],
|
Bin: defaults["bin"],
|
||||||
Global: defaults["global"],
|
Global: defaults["global"],
|
||||||
Output: defaults["output"],
|
Output: defaults["output"],
|
||||||
|
Version: verAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
var query url.Values
|
var query url.Values
|
||||||
@@ -188,16 +223,14 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
s += "?video&audio"
|
s += "?video&audio"
|
||||||
}
|
}
|
||||||
args.Input = inputTemplate("rtsp", s, query)
|
args.Input = inputTemplate("rtsp", s, query)
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
} else if i = strings.Index(s, "?"); i > 0 {
|
||||||
var err error
|
switch s[:i] {
|
||||||
args.Input, err = device.GetInput(s)
|
case "device":
|
||||||
if err != nil {
|
args.Input = device.GetInput(s[i+1:])
|
||||||
return nil
|
case "virtual":
|
||||||
}
|
args.Input = virtual.GetInput(s[i+1:])
|
||||||
} else if strings.HasPrefix(s, "virtual?") {
|
case "tts":
|
||||||
var err error
|
args.Input = virtual.GetInputTTS(s[i+1:])
|
||||||
if args.Input, err = virtual.GetInput(s[8:]); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args.Input = inputTemplate("file", s, query)
|
args.Input = inputTemplate("file", s, query)
|
||||||
@@ -280,6 +313,12 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query["bitrate"] != nil {
|
||||||
|
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||||
|
b := query["bitrate"][0]
|
||||||
|
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Process audio codecs
|
// 4. Process audio codecs
|
||||||
if args.Audio > 0 {
|
if args.Audio > 0 {
|
||||||
for _, audio := range query["audio"] {
|
for _, audio := range query["audio"] {
|
||||||
@@ -309,11 +348,27 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
args.AddCodec("-an")
|
args.AddCodec("-an")
|
||||||
}
|
}
|
||||||
|
|
||||||
// transcoding to only mjpeg
|
// change otput from RTSP to some other pipe format
|
||||||
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
|
switch {
|
||||||
// no transcoding from mjpeg input
|
case args.Video == 0 && args.Audio == 0:
|
||||||
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
|
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
|
||||||
args.Output = defaults["output/mjpeg"]
|
if strings.Contains(args.Input, " mjpeg ") {
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
}
|
||||||
|
case args.Video == 1 && args.Audio == 0:
|
||||||
|
switch core.Before(query.Get("video"), "/") {
|
||||||
|
case "mjpeg":
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
case "raw":
|
||||||
|
args.Output = defaults["output/raw"]
|
||||||
|
}
|
||||||
|
case args.Video == 0 && args.Audio == 1:
|
||||||
|
switch core.Before(query.Get("audio"), "/") {
|
||||||
|
case "aac":
|
||||||
|
args.Output = defaults["output/aac"]
|
||||||
|
case "pcma", "pcmu", "pcml":
|
||||||
|
args.Output = defaults["output/wav"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ffmpeg
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,3 +293,23 @@ func TestDrawText(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
verAV = ffmpeg.Version61
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: "/media/bbb.mp4",
|
||||||
|
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -152,7 +150,6 @@ var cache = map[string]string{}
|
|||||||
|
|
||||||
func run(bin string, args string) bool {
|
func run(bin string, args string) bool {
|
||||||
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
||||||
log.Printf("%v %v", args, err)
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build freebsd || netbsd || openbsd || dragonfly
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build darwin || ios
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import "github.com/AlexxIT/go2rtc/internal/api"
|
import "github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Producer struct {
|
||||||
|
core.Connection
|
||||||
|
url string
|
||||||
|
query url.Values
|
||||||
|
ffmpeg core.Producer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities
|
||||||
|
func NewProducer(url string) (core.Producer, error) {
|
||||||
|
p := &Producer{}
|
||||||
|
|
||||||
|
i := strings.IndexByte(url, '#')
|
||||||
|
p.url, p.query = url[:i], streams.ParseQuery(url[i+1:])
|
||||||
|
|
||||||
|
// ffmpeg.NewProducer support only one audio
|
||||||
|
if len(p.query["video"]) != 0 || len(p.query["audio"]) != 1 {
|
||||||
|
return nil, errors.New("ffmpeg: unsupported params: " + url[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
// codecs in order from best to worst
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
// OPUS will always marked as OPUS/48000/2
|
||||||
|
{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},
|
||||||
|
// AAC has unknown problems on Dahua two way
|
||||||
|
{Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) Start() error {
|
||||||
|
var err error
|
||||||
|
if p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, media := range p.ffmpeg.GetMedias() {
|
||||||
|
track, err := p.ffmpeg.GetTrack(media, media.Codecs[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Receivers[i].Replace(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ffmpeg.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) Stop() error {
|
||||||
|
if p.ffmpeg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.ffmpeg.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||||
|
if p.ffmpeg == nil {
|
||||||
|
return json.Marshal(p.Connection)
|
||||||
|
}
|
||||||
|
return json.Marshal(p.ffmpeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) newURL() string {
|
||||||
|
s := p.url
|
||||||
|
// rewrite codecs in url from auto to known presets from defaults
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
codec := receiver.Codec
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecOpus:
|
||||||
|
s += "#audio=opus"
|
||||||
|
case core.CodecAAC:
|
||||||
|
s += "#audio=aac/16000"
|
||||||
|
case core.CodecPCM:
|
||||||
|
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
case core.CodecPCMA:
|
||||||
|
s += "#audio=pcma/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
case core.CodecPCMU:
|
||||||
|
s += "#audio=pcmu/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add other params
|
||||||
|
for key, values := range p.query {
|
||||||
|
if key != "audio" {
|
||||||
|
for _, value := range values {
|
||||||
|
s += "#" + key + "=" + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var verMu sync.Mutex
|
||||||
|
var verErr error
|
||||||
|
var verFF string
|
||||||
|
var verAV string
|
||||||
|
|
||||||
|
func Version() (string, error) {
|
||||||
|
verMu.Lock()
|
||||||
|
defer verMu.Unlock()
|
||||||
|
|
||||||
|
if verFF != "" {
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(defaults["bin"], "-version")
|
||||||
|
b, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
verFF = "-"
|
||||||
|
verErr = err
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
|
|
||||||
|
verFF, verAV = ffmpeg.ParseVersion(b)
|
||||||
|
|
||||||
|
if verFF == "" {
|
||||||
|
verFF = "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
// better to compare libavformat, because nightly/master builds
|
||||||
|
if verAV != "" && verAV < ffmpeg.Version50 {
|
||||||
|
verErr = errors.New("ffmpeg: unsupported version: " + verFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin")
|
||||||
|
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
@@ -4,56 +4,76 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
func GetInput(src string) string {
|
||||||
query, err := url.ParseQuery(src)
|
query, err := url.ParseQuery(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// set defaults (using Add instead of Set)
|
input := "-re"
|
||||||
query.Add("source", "testsrc")
|
|
||||||
query.Add("size", "1920x1080")
|
|
||||||
query.Add("decimals", "2")
|
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-filters.html
|
for _, video := range query["video"] {
|
||||||
source := query.Get("source")
|
// https://ffmpeg.org/ffmpeg-filters.html
|
||||||
input := "-re -f lavfi -i " + source
|
sep := "=" // first separator
|
||||||
|
|
||||||
sep := "=" // first separator
|
if video == "" {
|
||||||
for key, values := range query {
|
video = "testsrc=decimals=2" // default video
|
||||||
value := values[0]
|
sep = ":"
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
|
||||||
switch key {
|
|
||||||
case "color", "rate", "duration", "sar":
|
|
||||||
case "size":
|
|
||||||
switch value {
|
|
||||||
case "720":
|
|
||||||
value = "1280x720"
|
|
||||||
case "1080":
|
|
||||||
value = "1920x1080"
|
|
||||||
case "2K":
|
|
||||||
value = "2560x1440"
|
|
||||||
case "4K":
|
|
||||||
value = "3840x2160"
|
|
||||||
case "8K":
|
|
||||||
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
|
||||||
}
|
|
||||||
case "decimals":
|
|
||||||
if source != "testsrc" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input += sep + key + "=" + value
|
input += " -f lavfi -i " + video
|
||||||
sep = ":" // next separator
|
|
||||||
|
// set defaults (using Add instead of Set)
|
||||||
|
query.Add("size", "1920x1080")
|
||||||
|
|
||||||
|
for key, values := range query {
|
||||||
|
value := values[0]
|
||||||
|
|
||||||
|
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
||||||
|
switch key {
|
||||||
|
case "color", "rate", "duration", "sar", "decimals":
|
||||||
|
case "size":
|
||||||
|
switch value {
|
||||||
|
case "720":
|
||||||
|
value = "1280x720" // crf=1 -> 12 Mbps
|
||||||
|
case "1080":
|
||||||
|
value = "1920x1080" // crf=1 -> 25 Mbps
|
||||||
|
case "2K":
|
||||||
|
value = "2560x1440" // crf=1 -> 43 Mbps
|
||||||
|
case "4K":
|
||||||
|
value = "3840x2160" // crf=1 -> 103 Mbps
|
||||||
|
case "8K":
|
||||||
|
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
input += sep + key + "=" + value
|
||||||
|
sep = ":" // next separator
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := query.Get("format"); s != "" {
|
||||||
|
input += ",format=" + s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := query.Get("format"); s != "" {
|
return input
|
||||||
input += ",format=" + s
|
}
|
||||||
}
|
|
||||||
|
func GetInputTTS(src string) string {
|
||||||
return input, nil
|
query, err := url.ParseQuery(src)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'`
|
||||||
|
|
||||||
|
// ffmpeg -f lavfi -i flite=list_voices=1
|
||||||
|
// awb, kal, kal16, rms, slt
|
||||||
|
if voice := query.Get("voice"); voice != "" {
|
||||||
|
input += ":voice" + voice
|
||||||
|
}
|
||||||
|
|
||||||
|
return input + `"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package virtual
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetInput(t *testing.T) {
|
||||||
|
s := GetInput("video")
|
||||||
|
require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s)
|
||||||
|
|
||||||
|
s = GetInput("video=testsrc2&size=4K")
|
||||||
|
require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInputTTS(t *testing.T) {
|
||||||
|
s := GetInputTTS("text=hello world&voice=slt")
|
||||||
|
require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s)
|
||||||
|
}
|
||||||
@@ -10,15 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("gopro", handleGoPro)
|
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
|
||||||
|
return gopro.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/gopro", apiGoPro)
|
api.HandleFunc("api/gopro", apiGoPro)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGoPro(rawURL string) (core.Producer, error) {
|
|
||||||
return gopro.Dial(rawURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||||
var items []*api.Source
|
var items []*api.Source
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
var conf struct {
|
var conf struct {
|
||||||
API struct {
|
API struct {
|
||||||
Listen string `json:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
} `yaml:"api"`
|
} `yaml:"api"`
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Config string `yaml:"config"`
|
Config string `yaml:"config"`
|
||||||
@@ -45,19 +45,14 @@ func Init() {
|
|||||||
return "", nil
|
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
|
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||||
client, err := hass.NewClient(url)
|
return hass.NewClient(source)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// load static entries from Hass config
|
// load static entries from Hass config
|
||||||
if err := importConfig(conf.Mod.Config); err != nil {
|
if err := importConfig(conf.Mod.Config); err != nil {
|
||||||
log.Debug().Msgf("[hass] can't import config: %s", err)
|
log.Trace().Msgf("[hass] can't import config: %s", err)
|
||||||
|
|
||||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
http.Error(w, "no hass config", http.StatusNotFound)
|
http.Error(w, "no hass config", http.StatusNotFound)
|
||||||
|
|||||||
+4
-7
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
medias := mp4.ParseQuery(r.URL.Query())
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
if medias != nil {
|
if medias != nil {
|
||||||
c := mp4.NewConsumer(medias)
|
c := mp4.NewConsumer(medias)
|
||||||
c.Type = "HLS/fMP4 consumer"
|
c.FormatName = "hls/fmp4"
|
||||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
c.WithRequest(r)
|
||||||
c.UserAgent = r.UserAgent()
|
|
||||||
cons = c
|
cons = c
|
||||||
} else {
|
} else {
|
||||||
c := mpegts.NewConsumer()
|
c := mpegts.NewConsumer()
|
||||||
c.Type = "HLS/TS consumer"
|
c.FormatName = "hls/mpegts"
|
||||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
c.WithRequest(r)
|
||||||
c.UserAgent = r.UserAgent()
|
|
||||||
cons = c
|
cons = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
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()
|
codecs := msg.String()
|
||||||
medias := mp4.ParseCodecs(codecs, true)
|
medias := mp4.ParseCodecs(codecs, true)
|
||||||
cons := mp4.NewConsumer(medias)
|
cons := mp4.NewConsumer(medias)
|
||||||
cons.Type = "HLS/fMP4 consumer"
|
cons.FormatName = "hls/fmp4"
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
cons.WithRequest(tr.Request)
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]struct {
|
Mod map[string]struct {
|
||||||
Pin string `json:"pin"`
|
Pin string `yaml:"pin"`
|
||||||
Name string `json:"name"`
|
Name string `yaml:"name"`
|
||||||
DeviceID string `json:"device_id"`
|
DeviceID string `yaml:"device_id"`
|
||||||
DevicePrivate string `json:"device_private"`
|
DevicePrivate string `yaml:"device_private"`
|
||||||
Pairings []string `json:"pairings"`
|
Pairings []string `yaml:"pairings"`
|
||||||
//Listen string `json:"listen"`
|
|
||||||
} `yaml:"homekit"`
|
} `yaml:"homekit"`
|
||||||
}
|
}
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
@@ -134,12 +133,19 @@ func Init() {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var servers map[string]*server
|
var servers map[string]*server
|
||||||
|
|
||||||
func streamHandler(url string) (core.Producer, error) {
|
func streamHandler(rawURL string) (core.Producer, error) {
|
||||||
if srtp.Server == nil {
|
if srtp.Server == nil {
|
||||||
return nil, errors.New("homekit: can't work without SRTP server")
|
return nil, errors.New("homekit: can't work without SRTP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
return homekit.Dial(url, srtp.Server)
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
client, err := homekit.Dial(rawURL, srtp.Server)
|
||||||
|
if client != nil && rawQuery != "" {
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
client.Bitrate = parseBitrate(query.Get("bitrate"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -200,3 +206,24 @@ func findHomeKitURL(stream *streams.Stream) string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBitrate(s string) int {
|
||||||
|
n := len(s)
|
||||||
|
if n == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var k int
|
||||||
|
switch n--; s[n] {
|
||||||
|
case 'K':
|
||||||
|
k = 1024
|
||||||
|
s = s[:n]
|
||||||
|
case 'M':
|
||||||
|
k = 1024 * 1024
|
||||||
|
s = s[:n]
|
||||||
|
default:
|
||||||
|
k = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return k * core.Atoi(s)
|
||||||
|
}
|
||||||
|
|||||||
+21
-8
@@ -11,9 +11,9 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hls"
|
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/image"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/multipart"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +45,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)
|
res, err := tcp.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,14 +81,12 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
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":
|
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
|
||||||
return hls.OpenURL(req.URL, res.Body)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return magic.Open(res.Body)
|
return magic.Open(res.Body)
|
||||||
|
|||||||
+3
-12
@@ -7,16 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("isapi", handle)
|
streams.HandleFunc("isapi", func(source string) (core.Producer, error) {
|
||||||
}
|
return isapi.Dial(source)
|
||||||
|
})
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,10 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("ivideon", func(url string) (core.Producer, error) {
|
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
|
||||||
id := strings.Replace(url[8:], "/", ":", 1)
|
return ivideon.Dial(source)
|
||||||
prod := ivideon.NewClient(id)
|
|
||||||
if err := prod.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return prod, nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## Stream as ASCII to Terminal
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
||||||
|
|
||||||
|
**Tips**
|
||||||
|
|
||||||
|
- this feature works only with MJPEG codec (use transcoding)
|
||||||
|
- choose a low frame rate (FPS)
|
||||||
|
- choose the width and height to fit in your terminal
|
||||||
|
- different terminals support different numbers of colours (8, 256, rgb)
|
||||||
|
- escape text param with urlencode
|
||||||
|
- you can stream any camera or file from a disc
|
||||||
|
|
||||||
|
**go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10
|
||||||
|
```
|
||||||
|
|
||||||
|
**API params**
|
||||||
|
|
||||||
|
- `color` - foreground color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||||
|
- example: `30` (black), `37` (white), `38;5;226` (yellow)
|
||||||
|
- `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||||
|
- example: `40` (black), `47` (white), `48;5;226` (yellow)
|
||||||
|
- `text` - character set, values: empty, one char, `block`, list of chars (in order of brightness)
|
||||||
|
- example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda"
|
||||||
|
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256"
|
||||||
|
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20"
|
||||||
|
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20"
|
||||||
|
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"
|
||||||
|
```
|
||||||
+50
-39
@@ -5,26 +5,36 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ascii"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/AlexxIT/go2rtc/pkg/y4m"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||||
|
api.HandleFunc("api/stream.ascii", handlerStream)
|
||||||
|
api.HandleFunc("api/stream.y4m", apiStreamY4M)
|
||||||
|
|
||||||
ws.HandleFunc("mjpeg", handlerWS)
|
ws.HandleFunc("mjpeg", handlerWS)
|
||||||
|
|
||||||
|
log = app.GetLogger("mjpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(src)
|
stream := streams.Get(src)
|
||||||
@@ -34,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := magic.NewKeyframe()
|
cons := magic.NewKeyframe()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -90,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mjpeg.NewConsumer()
|
cons := mjpeg.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||||
@@ -99,38 +107,22 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
|
||||||
h.Set("Cache-Control", "no-cache")
|
h.Set("Cache-Control", "no-cache")
|
||||||
h.Set("Connection", "close")
|
h.Set("Connection", "close")
|
||||||
h.Set("Pragma", "no-cache")
|
h.Set("Pragma", "no-cache")
|
||||||
|
|
||||||
wr := &writer{wr: w, buf: []byte(header)}
|
if strings.HasSuffix(r.URL.Path, "mjpeg") {
|
||||||
_, _ = cons.WriteTo(wr)
|
wr := mjpeg.NewWriter(w)
|
||||||
|
_, _ = cons.WriteTo(wr)
|
||||||
|
} else {
|
||||||
|
cons.FormatName = "ascii"
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
query := r.URL.Query()
|
||||||
}
|
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
|
||||||
|
_, _ = cons.WriteTo(wr)
|
||||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
|
||||||
|
|
||||||
type writer struct {
|
|
||||||
wr io.Writer
|
|
||||||
buf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *writer) Write(p []byte) (n int, err error) {
|
|
||||||
w.buf = w.buf[:len(header)]
|
|
||||||
w.buf = append(w.buf, strconv.Itoa(len(p))...)
|
|
||||||
w.buf = append(w.buf, "\r\n\r\n"...)
|
|
||||||
w.buf = append(w.buf, p...)
|
|
||||||
w.buf = append(w.buf, "\r\n"...)
|
|
||||||
|
|
||||||
// Chrome bug: mjpeg image always shows the second to last image
|
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
|
||||||
if n, err = w.wr.Write(w.buf); err == nil {
|
|
||||||
w.wr.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
stream.RemoveConsumer(cons)
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -141,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
|
prod, _ := mpjpeg.Open(r.Body)
|
||||||
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
|
prod.WithRequest(r)
|
||||||
|
|
||||||
client := mjpeg.NewClient(res)
|
stream.AddProducer(prod)
|
||||||
stream.AddProducer(client)
|
|
||||||
|
|
||||||
if err := client.Start(); err != nil && err != io.EOF {
|
if err := prod.Start(); err != nil && err != io.EOF {
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.Warn().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.RemoveProducer(client)
|
stream.RemoveProducer(prod)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||||
@@ -161,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mjpeg.NewConsumer()
|
cons := mjpeg.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
cons.WithRequest(tr.Request)
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
||||||
@@ -179,3 +169,24 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
|
||||||
|
src := r.URL.Query().Get("src")
|
||||||
|
stream := streams.Get(src)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := y4m.NewConsumer()
|
||||||
|
cons.WithRequest(r)
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}
|
||||||
|
|||||||
+17
-17
@@ -1,6 +1,7 @@
|
|||||||
package mp4
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,7 +13,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
medias := mp4.ParseQuery(r.URL.Query())
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
cons := mp4.NewConsumer(medias)
|
cons := mp4.NewConsumer(medias)
|
||||||
cons.Type = "MP4/HTTP active consumer"
|
cons.FormatName = "mp4"
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.Protocol = "http"
|
||||||
cons.UserAgent = r.UserAgent()
|
cons.WithRequest(r)
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -127,20 +127,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var duration *time.Timer
|
ctx := r.Context() // handle when the client drops the connection
|
||||||
if s := query.Get("duration"); s != "" {
|
|
||||||
if i, _ := strconv.Atoi(s); i > 0 {
|
if i := core.Atoi(query.Get("duration")); i > 0 {
|
||||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
timeout := time.Second * time.Duration(i)
|
||||||
_ = cons.Stop()
|
var cancel context.CancelFunc
|
||||||
})
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
}
|
defer cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = cons.Stop()
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}()
|
||||||
|
|
||||||
_, _ = cons.WriteTo(w)
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
if duration != nil {
|
|
||||||
duration.Stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-7
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
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 := mp4.NewConsumer(medias)
|
||||||
cons.Type = "MSE/WebSocket active consumer"
|
cons.FormatName = "mse/fmp4"
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
cons.WithRequest(tr.Request)
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Debug().Err(err).Msg("[mp4] add consumer")
|
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 := mp4.NewKeyframe(medias)
|
||||||
cons.Type = "MP4/WebSocket active consumer"
|
cons.WithRequest(tr.Request)
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -19,11 +17,9 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := aac.NewConsumer()
|
cons := aac.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -32,11 +30,9 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mpegts.NewConsumer()
|
cons := mpegts.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("nest", streamNest)
|
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
|
||||||
|
return nest.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/nest", apiNest)
|
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) {
|
func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
cliendID := query.Get("client_id")
|
cliendID := query.Get("client_id")
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func Init() {
|
|||||||
|
|
||||||
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
|
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
|
||||||
|
|
||||||
webrtc.AddCandidate(address, "tcp")
|
webrtc.AddCandidate("tcp", address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,22 +11,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("roborock", handle)
|
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
|
||||||
|
return roborock.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/roborock", apiHandle)
|
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 {
|
var Auth struct {
|
||||||
UserData *roborock.UserInfo `json:"user_data"`
|
UserData *roborock.UserInfo `json:"user_data"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,11 +127,7 @@ func tcpHandle(netConn net.Conn) error {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func streamsHandle(url string) (core.Producer, error) {
|
func streamsHandle(url string) (core.Producer, error) {
|
||||||
client, err := rtmp.DialPlay(url)
|
return rtmp.DialPlay(url)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||||
@@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := flv.NewConsumer()
|
cons := flv.NewConsumer()
|
||||||
cons.Type = "HTTP-FLV consumer"
|
cons.WithRequest(r)
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func Init() {
|
|||||||
Username string `yaml:"username" json:"-"`
|
Username string `yaml:"username" json:"-"`
|
||||||
Password string `yaml:"password" json:"-"`
|
Password string `yaml:"password" json:"-"`
|
||||||
DefaultQuery string `yaml:"default_query" json:"default_query"`
|
DefaultQuery string `yaml:"default_query" json:"default_query"`
|
||||||
PacketSize uint16 `yaml:"pkt_size"`
|
PacketSize uint16 `yaml:"pkt_size" json:"pkt_size,omitempty"`
|
||||||
} `yaml:"rtsp"`
|
} `yaml:"rtsp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +210,11 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query := conn.URL.Query()
|
||||||
|
if s := query.Get("timeout"); s != "" {
|
||||||
|
conn.Timeout = core.Atoi(s)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||||
|
|
||||||
stream.AddProducer(conn)
|
stream.AddProducer(conn)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## Testing notes
|
||||||
|
|
||||||
|
```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}
|
||||||
|
```
|
||||||
@@ -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, 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 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,175 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"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{}
|
var handlers = map[string]Handler{}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,18 +82,20 @@ func (s *Stream) Play(source string) error {
|
|||||||
s.AddInternalProducer(src)
|
s.AddInternalProducer(src)
|
||||||
s.AddInternalConsumer(cons)
|
s.AddInternalConsumer(cons)
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = src.Start()
|
|
||||||
_ = dst.Stop()
|
|
||||||
s.RemoveProducer(src)
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_ = dst.Start()
|
_ = dst.Start()
|
||||||
_ = src.Stop()
|
_ = src.Stop()
|
||||||
s.RemoveInternalConsumer(cons)
|
s.RemoveInternalConsumer(cons)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = src.Start()
|
||||||
|
// little timeout before stop dst, so the buffer can be transferred
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
_ = dst.Stop()
|
||||||
|
s.RemoveProducer(src)
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||||
if p.conn != nil {
|
if conn := p.conn; conn != nil {
|
||||||
return json.Marshal(p.conn)
|
return json.Marshal(conn)
|
||||||
}
|
}
|
||||||
|
info := map[string]string{"url": p.url}
|
||||||
info := core.Info{URL: p.url}
|
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +206,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
for _, media := range conn.GetMedias() {
|
for _, media := range conn.GetMedias() {
|
||||||
switch media.Direction {
|
switch media.Direction {
|
||||||
case core.DirectionRecvonly:
|
case core.DirectionRecvonly:
|
||||||
for _, receiver := range p.receivers {
|
for i, receiver := range p.receivers {
|
||||||
codec := media.MatchCodec(receiver.Codec)
|
codec := media.MatchCodec(receiver.Codec)
|
||||||
if codec == nil {
|
if codec == nil {
|
||||||
continue
|
continue
|
||||||
@@ -219,6 +218,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiver.Replace(track)
|
receiver.Replace(track)
|
||||||
|
p.receivers[i] = track
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +234,9 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop previous connection after moving tracks (fix ghost exec/ffmpeg)
|
||||||
|
_ = p.conn.Stop()
|
||||||
|
// swap connections
|
||||||
p.conn = conn
|
p.conn = conn
|
||||||
|
|
||||||
go p.worker(conn, workerID)
|
go p.worker(conn, workerID)
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ func (s *Stream) RemoveProducer(prod core.Producer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) stopProducers() {
|
func (s *Stream) stopProducers() {
|
||||||
|
if s.pending.Load() > 0 {
|
||||||
|
log.Trace().Msg("[streams] skip stop pending producer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
producers:
|
producers:
|
||||||
for _, producer := range s.producers {
|
for _, producer := range s.producers {
|
||||||
@@ -107,19 +112,12 @@ producers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||||
if !s.mu.TryLock() {
|
var info = struct {
|
||||||
log.Warn().Msgf("[streams] json locked")
|
|
||||||
return json.Marshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var info struct {
|
|
||||||
Producers []*Producer `json:"producers"`
|
Producers []*Producer `json:"producers"`
|
||||||
Consumers []core.Consumer `json:"consumers"`
|
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)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-75
@@ -1,7 +1,7 @@
|
|||||||
package streams
|
package streams
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -26,7 +26,8 @@ func Init() {
|
|||||||
streams[name] = NewStream(item)
|
streams[name] = NewStream(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.HandleFunc("api/streams", streamsHandler)
|
api.HandleFunc("api/streams", apiStreams)
|
||||||
|
api.HandleFunc("api/streams.dot", apiStreamsDOT)
|
||||||
|
|
||||||
if cfg.Publish == nil {
|
if cfg.Publish == nil {
|
||||||
return
|
return
|
||||||
@@ -47,9 +48,16 @@ func Get(name string) *Stream {
|
|||||||
|
|
||||||
var sanitize = regexp.MustCompile(`\s`)
|
var sanitize = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
func New(name string, source string) *Stream {
|
// Validate - not allow creating dynamic streams with spaces in the source
|
||||||
// not allow creating dynamic streams with spaces in the source
|
func Validate(source string) error {
|
||||||
if sanitize.MatchString(source) {
|
if sanitize.MatchString(source) {
|
||||||
|
return errors.New("streams: invalid dynamic source")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(name string, source string) *Stream {
|
||||||
|
if Validate(source) != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,77 +143,6 @@ func Delete(id string) {
|
|||||||
delete(streams, id)
|
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":
|
|
||||||
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 := 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 := 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var streams = map[string]*Stream{}
|
var streams = map[string]*Stream{}
|
||||||
var streamsMu sync.Mutex
|
var streamsMu sync.Mutex
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
|
streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
|
||||||
return kasa.Dial(url)
|
return kasa.Dial(source)
|
||||||
})
|
})
|
||||||
|
|
||||||
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
|
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
|
||||||
return tapo.Dial(url)
|
return tapo.Dial(source)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,105 @@
|
|||||||
|
What you should to know about WebRTC:
|
||||||
|
|
||||||
|
- It's almost always a **direct [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) connection** from your browser to go2rtc app
|
||||||
|
- When you use Home Assistant, Frigate, Nginx, Nabu Casa, Cloudflare and other software - they are only **involved in establishing** the connection, but they are **not involved in transferring** media data
|
||||||
|
- WebRTC media cannot be transferred inside an HTTP connection
|
||||||
|
- Usually, WebRTC uses random UDP ports on client and server side to establish a connection
|
||||||
|
- Usually, WebRTC uses public [STUN](https://en.wikipedia.org/wiki/STUN) servers to establish a connection outside LAN, such servers are only needed to establish a connection and are not involved in data transfer
|
||||||
|
- Usually, WebRTC will automatically discover all of your local addresses and all of your public addresses and try to establish a connection
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Default config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webrtc:
|
||||||
|
listen: ":8555/tcp"
|
||||||
|
ice_servers:
|
||||||
|
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||||
|
```
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
- supported TCP: fixed port (default), disabled
|
**Important!** This example is not for copypasting!
|
||||||
- supported UDP: random port (default), fixed port
|
|
||||||
|
|
||||||
| Config examples | TCP | UDP |
|
```yaml
|
||||||
|-----------------------|-------|--------|
|
webrtc:
|
||||||
| `listen: ":8555/tcp"` | fixed | random |
|
# fix local TCP or UDP or both ports for WebRTC media
|
||||||
| `listen: ":8555"` | fixed | fixed |
|
listen: ":8555/tcp" # address of your local server
|
||||||
| `listen: ""` | no | random |
|
|
||||||
|
# add additional host candidates manually
|
||||||
|
# order is important, the first will have a higher priority
|
||||||
|
candidates:
|
||||||
|
- 216.58.210.174:8555 # if you have static public IP-address
|
||||||
|
- stun:8555 # if you have dynamic public IP-address
|
||||||
|
- home.duckdns.org:8555 # if you have domain
|
||||||
|
|
||||||
|
# add custom STUN and TURN servers
|
||||||
|
# use `ice_servers: []` for remove defaults and leave empty
|
||||||
|
ice_servers:
|
||||||
|
- urls: [ stun:stun1.l.google.com:19302 ]
|
||||||
|
- urls: [ turn:123.123.123.123:3478 ]
|
||||||
|
username: your_user
|
||||||
|
credential: your_pass
|
||||||
|
|
||||||
|
# optional filter list for auto discovery logic
|
||||||
|
# some settings only make sense if you don't specify a fixed UDP port
|
||||||
|
filters:
|
||||||
|
# list of host candidates from auto discovery to be sent
|
||||||
|
# including candidates from the `listen` option
|
||||||
|
# use `candidates: []` to remove all auto discovery candidates
|
||||||
|
candidates: [ 192.168.1.123 ]
|
||||||
|
|
||||||
|
# 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
|
||||||
|
interfaces: [ eno1 ]
|
||||||
|
|
||||||
|
# list of host IP-addresses to be used for connection
|
||||||
|
# not related to the `listen` option
|
||||||
|
ips: [ 192.168.1.123 ]
|
||||||
|
|
||||||
|
# range for random UDP ports [min, max] to be used for connection
|
||||||
|
# not related to the `listen` option
|
||||||
|
udp_ports: [ 50000, 50100 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
By default go2rtc uses **fixed TCP** port and multiple **random UDP** ports for each WebRTC connection - `listen: ":8555/tcp"`.
|
||||||
|
|
||||||
|
You can set **fixed TCP** and **fixed UDP** port for all connections - `listen: ":8555"`. This may has lower performance, but it's your choice.
|
||||||
|
|
||||||
|
Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`.
|
||||||
|
|
||||||
|
## Config filters
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webrtc:
|
||||||
|
listen: ":8555/tcp" # use fixed TCP port and random UDP ports
|
||||||
|
filters:
|
||||||
|
ips: [ 192.168.1.2 ] # IP-address of your server
|
||||||
|
networks: [ udp4, tcp4 ] # skip IPv6, if it's not supported for you
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, go2rtc inside closed docker container (ex. [Frigate](https://frigate.video/)). You shouldn't filter docker interfaces, otherwise go2rtc will not be able to connect anywhere. But you can filter the docker candidates because no one can connect to them.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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
|
## Userful links
|
||||||
|
|
||||||
|
|||||||
@@ -2,57 +2,60 @@ package webrtc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
"github.com/pion/sdp/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Address struct {
|
type Address struct {
|
||||||
Host string
|
host string
|
||||||
Port string
|
Port string
|
||||||
Network string
|
Network string
|
||||||
Offset int
|
Priority uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Address) Marshal() string {
|
func (a *Address) Host() string {
|
||||||
host := a.Host
|
if a.host == "stun" {
|
||||||
if host == "stun" {
|
|
||||||
ip, err := webrtc.GetCachedPublicIP()
|
ip, err := webrtc.GetCachedPublicIP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
host = ip.String()
|
return ip.String()
|
||||||
}
|
}
|
||||||
|
return a.host
|
||||||
|
}
|
||||||
|
|
||||||
switch a.Network {
|
func (a *Address) Marshal() string {
|
||||||
case "udp":
|
if host := a.Host(); host != "" {
|
||||||
return webrtc.CandidateManualHostUDP(host, a.Port, a.Offset)
|
return webrtc.CandidateICE(a.Network, host, a.Port, a.Priority)
|
||||||
case "tcp":
|
|
||||||
return webrtc.CandidateManualHostTCPPassive(host, a.Port, a.Offset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var addresses []*Address
|
var addresses []*Address
|
||||||
|
var filters webrtc.Filters
|
||||||
|
|
||||||
|
func AddCandidate(network, address string) {
|
||||||
|
if network == "" {
|
||||||
|
AddCandidate("tcp", address)
|
||||||
|
AddCandidate("udp", address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func AddCandidate(address, network string) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
host, port, err := net.SplitHostPort(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := -1 - len(addresses) // every next candidate will have a lower priority
|
// start from 1, so manual candidates will be lower than built-in
|
||||||
|
// and every next candidate will have a lower priority
|
||||||
|
candidateIndex := 1 + len(addresses)
|
||||||
|
|
||||||
switch network {
|
priority := webrtc.CandidateHostPriority(network, candidateIndex)
|
||||||
case "tcp", "udp":
|
addresses = append(addresses, &Address{host, port, network, priority})
|
||||||
addresses = append(addresses, &Address{host, port, network, offset})
|
|
||||||
default:
|
|
||||||
addresses = append(
|
|
||||||
addresses, &Address{host, port, "udp", offset}, &Address{host, port, "tcp", offset},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCandidates() (candidates []string) {
|
func GetCandidates() (candidates []string) {
|
||||||
@@ -64,6 +67,38 @@ func GetCandidates() (candidates []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterCandidate return true if candidate passed the check
|
||||||
|
func FilterCandidate(candidate *pion.ICECandidate) bool {
|
||||||
|
if candidate == nil {
|
||||||
|
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) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.Networks != nil {
|
||||||
|
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
|
||||||
|
if !slices.Contains(filters.Networks, networkType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkType convert tcp/udp network to tcp4/tcp6/udp4/udp6
|
||||||
|
func NetworkType(network, host string) string {
|
||||||
|
if strings.IndexByte(host, ':') >= 0 {
|
||||||
|
return network + "6"
|
||||||
|
} else {
|
||||||
|
return network + "4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
|
func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
|
||||||
tr.WithContext(func(ctx map[any]any) {
|
tr.WithContext(func(ctx map[any]any) {
|
||||||
if candidates, ok := ctx["candidate"].([]string); ok {
|
if candidates, ok := ctx["candidate"].([]string); ok {
|
||||||
@@ -86,30 +121,6 @@ func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncCanditates(answer string) (string, error) {
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
return answer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sd := &sdp.SessionDescription{}
|
|
||||||
if err := sd.Unmarshal([]byte(answer)); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
md := sd.MediaDescriptions[0]
|
|
||||||
|
|
||||||
for _, candidate := range GetCandidates() {
|
|
||||||
md.WithPropertyAttribute(candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := sd.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func candidateHandler(tr *ws.Transport, msg *ws.Message) error {
|
func candidateHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||||
// process incoming candidate in sync function
|
// process incoming candidate in sync function
|
||||||
tr.WithContext(func(ctx map[any]any) {
|
tr.WithContext(func(ctx map[any]any) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
// https://aws.amazon.com/kinesis/video-streams/
|
// https://aws.amazon.com/kinesis/video-streams/
|
||||||
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
||||||
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
||||||
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
|
return kinesisClient(rawURL, query, "webrtc/kinesis")
|
||||||
} else if format == "openipc" {
|
} else if format == "openipc" {
|
||||||
return openIPCClient(rawURL, query)
|
return openIPCClient(rawURL, query)
|
||||||
} else {
|
} else {
|
||||||
@@ -77,17 +77,23 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
// 2. Create PeerConnection
|
// 2. Create PeerConnection
|
||||||
pc, err := PeerConnection(true)
|
pc, err := PeerConnection(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
_ = pc.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// waiter will wait PC error or WS error or nil (connection OK)
|
// waiter will wait PC error or WS error or nil (connection OK)
|
||||||
var connState core.Waiter
|
var connState core.Waiter
|
||||||
var connMu sync.Mutex
|
var connMu sync.Mutex
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WebSocket async"
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "ws"
|
||||||
|
prod.URL = url
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
@@ -132,7 +138,8 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if msg.Type != "webrtc/answer" {
|
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()
|
answer := msg.String()
|
||||||
@@ -180,8 +187,9 @@ func whepClient(url string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WHEP sync"
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
|
prod.URL = url
|
||||||
|
|
||||||
medias := []*core.Media{
|
medias := []*core.Media{
|
||||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (k kinesisResponse) String() string {
|
|||||||
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
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) (core.Producer, error) {
|
||||||
// 1. Connect to signalign server
|
// 1. Connect to signalign server
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,8 +79,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = desc
|
prod.FormatName = format
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "ws"
|
||||||
|
prod.URL = rawURL
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
@@ -216,5 +218,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
|
|||||||
"ice_servers": []string{string(kvs.Servers)},
|
"ice_servers": []string{string(kvs.Servers)},
|
||||||
}
|
}
|
||||||
|
|
||||||
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
|
return kinesisClient(kvs.URL, query, "webrtc/wyze")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/Milestone"
|
prod.FormatName = "webrtc/milestone"
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
|
prod.URL = rawURL
|
||||||
|
|
||||||
offer, err := mc.GetOffer()
|
offer, err := mc.GetOffer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
|||||||
var connState core.Waiter
|
var connState core.Waiter
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/OpenIPC"
|
prod.FormatName = "webrtc/openipc"
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "ws"
|
||||||
|
prod.URL = rawURL
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
url := r.URL.Query().Get("src")
|
url := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(url)
|
stream := streams.Get(url)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +101,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case "application/json":
|
case "application/json":
|
||||||
desc = "WebRTC/JSON sync"
|
desc = "webrtc/json"
|
||||||
case MimeSDP:
|
case MimeSDP:
|
||||||
desc = "WebRTC/WHEP sync"
|
desc = "webrtc/whep"
|
||||||
default:
|
default:
|
||||||
desc = "WebRTC/HTTP sync"
|
desc = "webrtc/post"
|
||||||
}
|
}
|
||||||
|
|
||||||
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
||||||
@@ -168,8 +169,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// create new webrtc instance
|
// create new webrtc instance
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WHIP sync"
|
|
||||||
prod.Mode = core.ModePassiveProducer
|
prod.Mode = core.ModePassiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
prod.UserAgent = r.UserAgent()
|
prod.UserAgent = r.UserAgent()
|
||||||
|
|
||||||
if err = prod.SetOffer(string(offer)); err != nil {
|
if err = prod.SetOffer(string(offer)); err != nil {
|
||||||
@@ -178,10 +179,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
answer, err := prod.GetCompleteAnswer()
|
answer, err := prod.GetCompleteAnswer(GetCandidates(), FilterCandidate)
|
||||||
if err == nil {
|
|
||||||
answer, err = syncCanditates(answer)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.Warn().Err(err).Caller().Send()
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
+13
-17
@@ -20,6 +20,7 @@ func Init() {
|
|||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
Candidates []string `yaml:"candidates"`
|
Candidates []string `yaml:"candidates"`
|
||||||
IceServers []pion.ICEServer `yaml:"ice_servers"`
|
IceServers []pion.ICEServer `yaml:"ice_servers"`
|
||||||
|
Filters webrtc.Filters `yaml:"filters"`
|
||||||
} `yaml:"webrtc"`
|
} `yaml:"webrtc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,20 +33,15 @@ func Init() {
|
|||||||
|
|
||||||
log = app.GetLogger("webrtc")
|
log = app.GetLogger("webrtc")
|
||||||
|
|
||||||
|
filters = cfg.Mod.Filters
|
||||||
|
|
||||||
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
|
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
|
||||||
|
|
||||||
var candidateHost []string
|
|
||||||
for _, candidate := range cfg.Mod.Candidates {
|
for _, candidate := range cfg.Mod.Candidates {
|
||||||
if strings.HasPrefix(candidate, "host:") {
|
AddCandidate(network, candidate)
|
||||||
candidateHost = append(candidateHost, candidate[5:])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
AddCandidate(candidate, network)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create pionAPI with custom codecs list and custom network settings
|
// create pionAPI with custom codecs list and custom network settings
|
||||||
serverAPI, err := webrtc.NewServerAPI(address, network, candidateHost)
|
serverAPI, err := webrtc.NewServerAPI(network, address, &filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
@@ -55,8 +51,7 @@ func Init() {
|
|||||||
clientAPI := serverAPI
|
clientAPI := serverAPI
|
||||||
|
|
||||||
if address != "" {
|
if address != "" {
|
||||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
||||||
|
|
||||||
clientAPI, _ = webrtc.NewAPI()
|
clientAPI, _ = webrtc.NewAPI()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +117,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
defer sendAnswer.Done(nil)
|
defer sendAnswer.Done(nil)
|
||||||
|
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = "WebRTC/WebSocket async"
|
|
||||||
conn.Mode = mode
|
conn.Mode = mode
|
||||||
|
conn.Protocol = "ws"
|
||||||
conn.UserAgent = tr.Request.UserAgent()
|
conn.UserAgent = tr.Request.UserAgent()
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -139,6 +134,9 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
|
if !FilterCandidate(msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
_ = sendAnswer.Wait()
|
_ = sendAnswer.Wait()
|
||||||
|
|
||||||
s := msg.ToJSON().Candidate
|
s := msg.ToJSON().Candidate
|
||||||
@@ -209,8 +207,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
|||||||
|
|
||||||
// create new webrtc instance
|
// create new webrtc instance
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = desc
|
conn.FormatName = desc
|
||||||
conn.UserAgent = userAgent
|
conn.UserAgent = userAgent
|
||||||
|
conn.Protocol = "http"
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case pion.PeerConnectionState:
|
case pion.PeerConnectionState:
|
||||||
@@ -248,10 +247,7 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
|||||||
stream.AddProducer(conn)
|
stream.AddProducer(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
answer, err = conn.GetCompleteAnswer()
|
answer, err = conn.GetCompleteAnswer(GetCandidates(), FilterCandidate)
|
||||||
if err == nil {
|
|
||||||
answer, err = syncCanditates(answer)
|
|
||||||
}
|
|
||||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func Init() {
|
|||||||
if stream == nil {
|
if stream == nil {
|
||||||
return "", errors.New(api.StreamNotFound)
|
return "", errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
|
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
app.Version = "1.9.4"
|
||||||
|
|
||||||
// 1. Core modules: app, api/ws, streams
|
// 1. Core modules: app, api/ws, streams
|
||||||
|
|
||||||
app.Init() // init config and logs
|
app.Init() // init config and logs
|
||||||
|
|||||||
@@ -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
|
## Useful links
|
||||||
|
|
||||||
- https://www.wowza.com/blog/streaming-protocols
|
- https://www.wowza.com/blog/streaming-protocols
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate u
|
|||||||
sampleFreqIdx = rd.ReadBits8(4)
|
sampleFreqIdx = rd.ReadBits8(4)
|
||||||
if sampleFreqIdx == 0b1111 {
|
if sampleFreqIdx == 0b1111 {
|
||||||
sampleRate = rd.ReadBits(24)
|
sampleRate = rd.ReadBits(24)
|
||||||
|
} else {
|
||||||
|
sampleRate = sampleRates[sampleFreqIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
channels = rd.ReadBits8(4)
|
channels = rd.ReadBits8(4)
|
||||||
|
|||||||
@@ -41,3 +41,12 @@ func TestADTS(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, src[:len(dst)], dst)
|
require.Equal(t, src[:len(dst)], dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodeConfig(t *testing.T) {
|
||||||
|
conf := EncodeConfig(TypeAACLC, 48000, 1, false)
|
||||||
|
require.Equal(t, "1188", hex.EncodeToString(conf))
|
||||||
|
conf = EncodeConfig(TypeAACLC, 16000, 1, false)
|
||||||
|
require.Equal(t, "1408", hex.EncodeToString(conf))
|
||||||
|
conf = EncodeConfig(TypeAACLC, 8000, 1, false)
|
||||||
|
require.Equal(t, "1588", hex.EncodeToString(conf))
|
||||||
|
}
|
||||||
|
|||||||
+12
-11
@@ -8,15 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
wr *core.WriteBuffer
|
wr *core.WriteBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumer() *Consumer {
|
func NewConsumer() *Consumer {
|
||||||
cons := &Consumer{
|
medias := []*core.Media{
|
||||||
wr: core.NewWriteBuffer(nil),
|
|
||||||
}
|
|
||||||
cons.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionSendonly,
|
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 {
|
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) {
|
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||||
return c.wr.WriteTo(wr)
|
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 {
|
type Producer struct {
|
||||||
core.SuperProducer
|
core.Connection
|
||||||
rd *bufio.Reader
|
rd *bufio.Reader
|
||||||
cl io.Closer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(r io.Reader) (*Producer, error) {
|
func Open(r io.Reader) (*Producer, error) {
|
||||||
@@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
codec := ADTSToCodec(b)
|
medias := []*core.Media{
|
||||||
|
|
||||||
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
|
||||||
prod.Type = "ADTS producer"
|
|
||||||
prod.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionRecvonly,
|
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 {
|
func (c *Producer) Start() error {
|
||||||
@@ -66,8 +69,3 @@ func (c *Producer) Start() error {
|
|||||||
c.Receivers[0].WriteRTP(pkt)
|
c.Receivers[0].WriteRTP(pkt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Producer) Stop() error {
|
|
||||||
_ = c.SuperProducer.Close()
|
|
||||||
return c.cl.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||||
|
- https://paulbourke.net/dataformats/asciiart/
|
||||||
|
- https://github.com/kutuluk/xterm-color-chart
|
||||||
|
- https://github.com/hugomd/parrot.live
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package ascii
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewWriter(w io.Writer, foreground, background, text string) io.Writer {
|
||||||
|
// once clear screen
|
||||||
|
_, _ = w.Write([]byte(csiClear))
|
||||||
|
|
||||||
|
// every frame - move to home
|
||||||
|
a := &writer{wr: w, buf: []byte(csiHome)}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||||
|
switch foreground {
|
||||||
|
case "":
|
||||||
|
case "8":
|
||||||
|
a.color = func(r, g, b uint8) {
|
||||||
|
idx := xterm256color(r, g, b, 8)
|
||||||
|
a.appendEsc(fmt.Sprintf("\033[%dm", 30+idx))
|
||||||
|
|
||||||
|
}
|
||||||
|
case "256":
|
||||||
|
a.color = func(r, g, b uint8) {
|
||||||
|
idx := xterm256color(r, g, b, 255)
|
||||||
|
a.appendEsc(fmt.Sprintf("\033[38;5;%dm", idx))
|
||||||
|
}
|
||||||
|
case "rgb":
|
||||||
|
a.color = func(r, g, b uint8) {
|
||||||
|
a.appendEsc(fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
a.buf = append(a.buf, "\033["+foreground+"m"...)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch background {
|
||||||
|
case "":
|
||||||
|
case "8":
|
||||||
|
a.color = func(r, g, b uint8) {
|
||||||
|
idx := xterm256color(r, g, b, 8)
|
||||||
|
a.appendEsc(fmt.Sprintf("\033[%dm", 40+idx))
|
||||||
|
}
|
||||||
|
case "256":
|
||||||
|
a.color = func(r, g, b uint8) {
|
||||||
|
idx := xterm256color(r, g, b, 255)
|
||||||
|
a.appendEsc(fmt.Sprintf("\033[48;5;%dm", idx))
|
||||||
|
}
|
||||||
|
case "rgb":
|
||||||
|
a.color = func(r, g, b uint8) {
|
||||||
|
a.appendEsc(fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
a.buf = append(a.buf, "\033["+background+"m"...)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.pre = len(a.buf) // save prefix size
|
||||||
|
|
||||||
|
if len(text) == 1 {
|
||||||
|
// fast 1 symbol version
|
||||||
|
a.text = func(_, _, _ uint32) {
|
||||||
|
a.buf = append(a.buf, text[0])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch text {
|
||||||
|
case "":
|
||||||
|
text = ` .::--~~==++**##%%$@` // default for empty text
|
||||||
|
case "block":
|
||||||
|
text = " ░░▒▒▓▓█" // https://en.wikipedia.org/wiki/Block_Elements
|
||||||
|
}
|
||||||
|
|
||||||
|
if runes := []rune(text); len(runes) != len(text) {
|
||||||
|
k := float32(len(runes)-1) / 255
|
||||||
|
a.text = func(r, g, b uint32) {
|
||||||
|
i := gray(r, g, b, k)
|
||||||
|
a.buf = utf8.AppendRune(a.buf, runes[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
k := float32(len(text)-1) / 255
|
||||||
|
a.text = func(r, g, b uint32) {
|
||||||
|
i := gray(r, g, b, k)
|
||||||
|
a.buf = append(a.buf, text[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
type writer struct {
|
||||||
|
wr io.Writer
|
||||||
|
buf []byte
|
||||||
|
pre int
|
||||||
|
esc string
|
||||||
|
color func(r, g, b uint8)
|
||||||
|
text func(r, g, b uint32)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/37774983/clearing-the-screen-by-printing-a-character
|
||||||
|
const csiClear = "\033[2J"
|
||||||
|
const csiHome = "\033[H"
|
||||||
|
|
||||||
|
func (a *writer) Write(p []byte) (n int, err error) {
|
||||||
|
img, err := jpeg.Decode(bytes.NewReader(p))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.buf = a.buf[:a.pre] // restore prefix
|
||||||
|
|
||||||
|
w := img.Bounds().Dx()
|
||||||
|
h := img.Bounds().Dy()
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
r, g, b, _ := img.At(x, y).RGBA()
|
||||||
|
if a.color != nil {
|
||||||
|
a.color(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||||
|
}
|
||||||
|
a.text(r, g, b)
|
||||||
|
}
|
||||||
|
a.buf = append(a.buf, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
a.appendEsc("\033[0m")
|
||||||
|
|
||||||
|
if _, err = a.wr.Write(a.buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.wr.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendEsc - append ESC code to buffer, and skip duplicates
|
||||||
|
func (a *writer) appendEsc(s string) {
|
||||||
|
if a.esc != s {
|
||||||
|
a.esc = s
|
||||||
|
a.buf = append(a.buf, s...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gray(r, g, b uint32, k float32) uint8 {
|
||||||
|
gr := (19595*r + 38470*g + 7471*b + 1<<15) >> 24 // uint8
|
||||||
|
return uint8(float32(gr) * k)
|
||||||
|
}
|
||||||
|
|
||||||
|
const x256r = "\x00\x80\x00\x80\x00\x80\x00\xc0\x80\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee"
|
||||||
|
const x256g = "\x00\x00\x80\x80\x00\x00\x80\xc0\x80\x00\xff\xff\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee"
|
||||||
|
const x256b = "\x00\x00\x00\x00\x80\x80\x80\xc0\x80\x00\x00\x00\xff\xff\xff\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee"
|
||||||
|
|
||||||
|
func xterm256color(r, g, b uint8, n int) (index uint8) {
|
||||||
|
best := uint16(0xFFFF)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
diff := sqDiff(r, x256r[i]) + sqDiff(g, x256g[i]) + sqDiff(b, x256b[i])
|
||||||
|
if diff < best {
|
||||||
|
best = diff
|
||||||
|
index = uint8(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqDiff - just like from image/color/color.go
|
||||||
|
func sqDiff(x, y uint8) uint16 {
|
||||||
|
d := uint16(x - y)
|
||||||
|
//return d
|
||||||
|
return (d * d) >> 2
|
||||||
|
}
|
||||||
@@ -131,3 +131,7 @@ func (r *Reader) ReadSEGolomb() int32 {
|
|||||||
func (r *Reader) Left() []byte {
|
func (r *Reader) Left() []byte {
|
||||||
return r.buf[r.pos:]
|
return r.buf[r.pos:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Pos() (int, byte) {
|
||||||
|
return r.pos - 1, r.bits
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated: should be rewritten to core.Connection
|
||||||
type Client struct {
|
type Client struct {
|
||||||
core.Listener
|
core.Listener
|
||||||
|
|
||||||
@@ -43,8 +44,12 @@ type Client struct {
|
|||||||
recv int
|
recv int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(url string) *Client {
|
func Dial(rawURL string) (*Client, error) {
|
||||||
return &Client{url: url}
|
client := &Client{url: rawURL}
|
||||||
|
if err := client.Dial(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+10
-5
@@ -65,11 +65,16 @@ func (c *Client) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
info := &core.Info{
|
info := &core.Connection{
|
||||||
Type: "Bubble active producer",
|
ID: core.ID(c),
|
||||||
Medias: c.medias,
|
FormatName: "bubble",
|
||||||
Recv: c.recv,
|
Protocol: "http",
|
||||||
Receivers: c.receivers,
|
Medias: c.medias,
|
||||||
|
Recv: c.recv,
|
||||||
|
Receivers: c.receivers,
|
||||||
|
}
|
||||||
|
if c.conn != nil {
|
||||||
|
info.RemoteAddr = c.conn.RemoteAddr().String()
|
||||||
}
|
}
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-20
@@ -2,8 +2,8 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -18,34 +18,76 @@ type Codec struct {
|
|||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) String() string {
|
// MarshalJSON - return FFprobe compatible output
|
||||||
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
|
func (c *Codec) MarshalJSON() ([]byte, error) {
|
||||||
|
info := map[string]any{}
|
||||||
|
if name := FFmpegCodecName(c.Name); name != "" {
|
||||||
|
info["codec_name"] = name
|
||||||
|
info["codec_type"] = c.Kind()
|
||||||
|
}
|
||||||
|
if c.Name == CodecH264 {
|
||||||
|
profile, level := DecodeH264(c.FmtpLine)
|
||||||
|
if profile != "" {
|
||||||
|
info["profile"] = profile
|
||||||
|
info["level"] = level
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||||
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
|
info["sample_rate"] = c.ClockRate
|
||||||
}
|
}
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
info["channels"] = c.Channels
|
||||||
}
|
}
|
||||||
return s
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Text() string {
|
func FFmpegCodecName(name string) string {
|
||||||
switch c.Name {
|
switch name {
|
||||||
case CodecH264:
|
case CodecH264:
|
||||||
if profile := DecodeH264(c.FmtpLine); profile != "" {
|
return "h264"
|
||||||
return "H.264 " + profile
|
case CodecH265:
|
||||||
}
|
return "hevc"
|
||||||
return c.Name
|
case CodecJPEG:
|
||||||
|
return "mjpeg"
|
||||||
|
case CodecRAW:
|
||||||
|
return "rawvideo"
|
||||||
|
case CodecPCMA:
|
||||||
|
return "pcm_alaw"
|
||||||
|
case CodecPCMU:
|
||||||
|
return "pcm_mulaw"
|
||||||
|
case CodecPCM:
|
||||||
|
return "pcm_s16be"
|
||||||
|
case CodecPCML:
|
||||||
|
return "pcm_s16le"
|
||||||
|
case CodecAAC:
|
||||||
|
return "aac"
|
||||||
|
case CodecOpus:
|
||||||
|
return "opus"
|
||||||
|
case CodecVP8:
|
||||||
|
return "vp8"
|
||||||
|
case CodecVP9:
|
||||||
|
return "vp9"
|
||||||
|
case CodecAV1:
|
||||||
|
return "av1"
|
||||||
|
case CodecELD:
|
||||||
|
return "aac/eld"
|
||||||
|
case CodecFLAC:
|
||||||
|
return "flac"
|
||||||
|
case CodecMP3:
|
||||||
|
return "mp3"
|
||||||
}
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
s := c.Name
|
func (c *Codec) String() (s string) {
|
||||||
|
s = c.Name
|
||||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||||
s += "/" + strconv.Itoa(int(c.ClockRate))
|
s += fmt.Sprintf("/%d", c.ClockRate)
|
||||||
}
|
}
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
s += "/" + strconv.Itoa(int(c.Channels))
|
s += fmt.Sprintf("/%d", c.Channels)
|
||||||
}
|
}
|
||||||
return s
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) IsRTP() bool {
|
func (c *Codec) IsRTP() bool {
|
||||||
@@ -181,10 +223,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeH264(fmtp string) string {
|
func DecodeH264(fmtp string) (profile string, level byte) {
|
||||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||||
var profile string
|
|
||||||
switch sps[1] {
|
switch sps[1] {
|
||||||
case 0x42:
|
case 0x42:
|
||||||
profile = "Baseline"
|
profile = "Baseline"
|
||||||
@@ -198,8 +239,8 @@ func DecodeH264(fmtp string) string {
|
|||||||
profile = fmt.Sprintf("0x%02X", sps[1])
|
profile = fmt.Sprintf("0x%02X", sps[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10)
|
level = sps[3]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
+5
-85
@@ -1,5 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DirectionRecvonly = "recvonly"
|
DirectionRecvonly = "recvonly"
|
||||||
DirectionSendonly = "sendonly"
|
DirectionSendonly = "sendonly"
|
||||||
@@ -18,6 +20,7 @@ const (
|
|||||||
CodecVP9 = "VP9"
|
CodecVP9 = "VP9"
|
||||||
CodecAV1 = "AV1"
|
CodecAV1 = "AV1"
|
||||||
CodecJPEG = "JPEG" // payloadType: 26
|
CodecJPEG = "JPEG" // payloadType: 26
|
||||||
|
CodecRAW = "RAW"
|
||||||
|
|
||||||
CodecPCMU = "PCMU" // payloadType: 0
|
CodecPCMU = "PCMU" // payloadType: 0
|
||||||
CodecPCMA = "PCMA" // payloadType: 8
|
CodecPCMA = "PCMA" // payloadType: 8
|
||||||
@@ -89,89 +92,6 @@ func (m Mode) String() string {
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
func (m Mode) MarshalJSON() ([]byte, error) {
|
||||||
Type string `json:"type,omitempty"`
|
return json.Marshal(m.String())
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type producer struct {
|
||||||
|
Medias []*Media
|
||||||
|
Receivers []*Receiver
|
||||||
|
|
||||||
|
id byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) GetMedias() []*Media {
|
||||||
|
return p.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) {
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
if receiver.Codec == codec {
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiver := NewReceiver(nil, codec)
|
||||||
|
p.Receivers = append(p.Receivers, receiver)
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) Start() error {
|
||||||
|
pkt := &Packet{Payload: []byte{p.id}}
|
||||||
|
p.Receivers[0].Input(pkt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) Stop() error {
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
receiver.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type consumer struct {
|
||||||
|
Medias []*Media
|
||||||
|
Senders []*Sender
|
||||||
|
|
||||||
|
cache chan byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) GetMedias() []*Media {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error {
|
||||||
|
c.cache = make(chan byte, 1)
|
||||||
|
sender := NewSender(nil, track.Codec)
|
||||||
|
sender.Output = func(packet *Packet) {
|
||||||
|
c.cache <- packet.Payload[0]
|
||||||
|
}
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
c.Senders = append(c.Senders, sender)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) Stop() error {
|
||||||
|
for _, sender := range c.Senders {
|
||||||
|
sender.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) read() byte {
|
||||||
|
return <-c.cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestName(t *testing.T) {
|
||||||
|
GetProducer := func(b byte) Producer {
|
||||||
|
return &producer{
|
||||||
|
Medias: []*Media{
|
||||||
|
{
|
||||||
|
Kind: KindVideo,
|
||||||
|
Direction: DirectionRecvonly,
|
||||||
|
Codecs: []*Codec{
|
||||||
|
{Name: CodecH264},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage1
|
||||||
|
prod1 := GetProducer(1)
|
||||||
|
cons2 := &consumer{}
|
||||||
|
|
||||||
|
media1 := prod1.GetMedias()[0]
|
||||||
|
track1, _ := prod1.GetTrack(media1, media1.Codecs[0])
|
||||||
|
|
||||||
|
_ = cons2.AddTrack(nil, nil, track1)
|
||||||
|
|
||||||
|
_ = prod1.Start()
|
||||||
|
require.Equal(t, byte(1), cons2.read())
|
||||||
|
|
||||||
|
// stage2
|
||||||
|
prod2 := GetProducer(2)
|
||||||
|
media2 := prod2.GetMedias()[0]
|
||||||
|
require.NotEqual(t, fmt.Sprintf("%p", media1), fmt.Sprintf("%p", media2))
|
||||||
|
track2, _ := prod2.GetTrack(media2, media2.Codecs[0])
|
||||||
|
track1.Replace(track2)
|
||||||
|
|
||||||
|
_ = prod1.Stop()
|
||||||
|
|
||||||
|
_ = prod2.Start()
|
||||||
|
require.Equal(t, byte(2), cons2.read())
|
||||||
|
|
||||||
|
// stage3
|
||||||
|
_ = prod2.Stop()
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ func RandString(size, base byte) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Before(s, sep string) string {
|
||||||
|
if i := strings.Index(s, sep); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func Between(s, sub1, sub2 string) string {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ type Media struct {
|
|||||||
func (m *Media) String() string {
|
func (m *Media) String() string {
|
||||||
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
||||||
for _, codec := range m.Codecs {
|
for _, codec := range m.Codecs {
|
||||||
name := codec.Text()
|
name := codec.String()
|
||||||
|
|
||||||
if strings.Contains(s, name) {
|
if strings.Contains(s, name) {
|
||||||
continue
|
continue
|
||||||
@@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool {
|
|||||||
|
|
||||||
func GetKind(name string) string {
|
func GetKind(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:
|
||||||
return KindVideo
|
return KindVideo
|
||||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
//type Packet struct {
|
||||||
|
// Payload []byte
|
||||||
|
// Timestamp uint32 // PTS if DTS == 0 else DTS
|
||||||
|
// Composition uint32 // CTS = PTS-DTS (for support B-frames)
|
||||||
|
// Sequence uint16
|
||||||
|
//}
|
||||||
|
|
||||||
|
type Packet = rtp.Packet
|
||||||
|
|
||||||
|
// HandlerFunc - process input packets (just like http.HandlerFunc)
|
||||||
|
type HandlerFunc func(packet *Packet)
|
||||||
|
|
||||||
|
// Filter - a decorator for any HandlerFunc
|
||||||
|
type Filter func(handler HandlerFunc) HandlerFunc
|
||||||
|
|
||||||
|
// Node - Receiver or Sender or Filter (transform)
|
||||||
|
type Node struct {
|
||||||
|
Codec *Codec
|
||||||
|
Input HandlerFunc
|
||||||
|
Output HandlerFunc
|
||||||
|
|
||||||
|
id uint32
|
||||||
|
childs []*Node
|
||||||
|
parent *Node
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) WithParent(parent *Node) *Node {
|
||||||
|
parent.AppendChild(n)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) AppendChild(child *Node) {
|
||||||
|
n.mu.Lock()
|
||||||
|
n.childs = append(n.childs, child)
|
||||||
|
n.mu.Unlock()
|
||||||
|
|
||||||
|
child.parent = n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) RemoveChild(child *Node) {
|
||||||
|
n.mu.Lock()
|
||||||
|
for i, ch := range n.childs {
|
||||||
|
if ch == child {
|
||||||
|
n.childs = append(n.childs[:i], n.childs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Close() {
|
||||||
|
if parent := n.parent; parent != nil {
|
||||||
|
parent.RemoveChild(n)
|
||||||
|
|
||||||
|
if len(parent.childs) == 0 {
|
||||||
|
parent.Close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, childs := range n.childs {
|
||||||
|
childs.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveNode(dst, src *Node) {
|
||||||
|
src.mu.Lock()
|
||||||
|
childs := src.childs
|
||||||
|
src.childs = nil
|
||||||
|
src.mu.Unlock()
|
||||||
|
|
||||||
|
dst.mu.Lock()
|
||||||
|
dst.childs = childs
|
||||||
|
dst.mu.Unlock()
|
||||||
|
|
||||||
|
for _, child := range childs {
|
||||||
|
child.parent = dst
|
||||||
|
}
|
||||||
|
}
|
||||||
+156
-145
@@ -3,201 +3,212 @@ package core
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Packet struct {
|
|
||||||
PayloadType uint8
|
|
||||||
Sequence uint16
|
|
||||||
Timestamp uint32 // PTS if DTS == 0 else DTS
|
|
||||||
Composition uint32 // CTS = PTS-DTS (for support B-frames)
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrCantGetTrack = errors.New("can't get track")
|
var ErrCantGetTrack = errors.New("can't get track")
|
||||||
|
|
||||||
type Receiver struct {
|
type Receiver struct {
|
||||||
Codec *Codec
|
Node
|
||||||
Media *Media
|
|
||||||
|
|
||||||
ID byte // Channel for RTSP, PayloadType for MPEG-TS
|
// Deprecated: should be removed
|
||||||
|
Media *Media `json:"-"`
|
||||||
|
// Deprecated: should be removed
|
||||||
|
ID byte `json:"-"` // Channel for RTSP, PayloadType for MPEG-TS
|
||||||
|
|
||||||
senders map[*Sender]chan *rtp.Packet
|
Bytes int `json:"bytes,omitempty"`
|
||||||
mu sync.RWMutex
|
Packets int `json:"packets,omitempty"`
|
||||||
bytes int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
||||||
Assert(codec != nil)
|
r := &Receiver{
|
||||||
return &Receiver{Codec: codec, Media: media}
|
Node: Node{id: NewID(), Codec: codec},
|
||||||
}
|
Media: media,
|
||||||
|
}
|
||||||
// WriteRTP - fast and non blocking write to all readers buffers
|
r.Input = func(packet *Packet) {
|
||||||
func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
r.Bytes += len(packet.Payload)
|
||||||
t.mu.Lock()
|
r.Packets++
|
||||||
t.bytes += len(packet.Payload)
|
for _, child := range r.childs {
|
||||||
for sender, buffer := range t.senders {
|
child.Input(packet)
|
||||||
select {
|
|
||||||
case buffer <- packet:
|
|
||||||
default:
|
|
||||||
sender.overflow++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.mu.Unlock()
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Receiver) Senders() (senders []*Sender) {
|
// Deprecated: should be removed
|
||||||
t.mu.RLock()
|
func (r *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||||
for sender := range t.senders {
|
r.Input(packet)
|
||||||
senders = append(senders, sender)
|
}
|
||||||
|
|
||||||
|
// Deprecated: should be removed
|
||||||
|
func (r *Receiver) Senders() []*Sender {
|
||||||
|
if len(r.childs) > 0 {
|
||||||
|
return []*Sender{{}}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
t.mu.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Receiver) Close() {
|
// Deprecated: should be removed
|
||||||
t.mu.Lock()
|
func (r *Receiver) Replace(target *Receiver) {
|
||||||
// close all sender channel buffers and erase senders list
|
MoveNode(&target.Node, &r.Node)
|
||||||
for _, buffer := range t.senders {
|
|
||||||
close(buffer)
|
|
||||||
}
|
|
||||||
t.senders = nil
|
|
||||||
t.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Receiver) Replace(target *Receiver) {
|
func (r *Receiver) Close() {
|
||||||
// move this receiver senders to new receiver
|
r.Node.Close()
|
||||||
t.mu.Lock()
|
|
||||||
senders := t.senders
|
|
||||||
t.mu.Unlock()
|
|
||||||
|
|
||||||
target.mu.Lock()
|
|
||||||
target.senders = senders
|
|
||||||
target.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Receiver) String() string {
|
|
||||||
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
|
|
||||||
t.mu.RLock()
|
|
||||||
s += fmt.Sprintf(", senders=%d", len(t.senders))
|
|
||||||
t.mu.RUnlock()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Receiver) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(t.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sender struct {
|
type Sender struct {
|
||||||
Codec *Codec
|
Node
|
||||||
Media *Media
|
|
||||||
|
|
||||||
Handler HandlerFunc
|
// Deprecated:
|
||||||
|
Media *Media `json:"-"`
|
||||||
|
// Deprecated:
|
||||||
|
Handler HandlerFunc `json:"-"`
|
||||||
|
|
||||||
receivers []*Receiver
|
Bytes int `json:"bytes,omitempty"`
|
||||||
mu sync.RWMutex
|
Packets int `json:"packets,omitempty"`
|
||||||
bytes int
|
Drops int `json:"drops,omitempty"`
|
||||||
|
|
||||||
overflow int
|
buf chan *Packet
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(media *Media, codec *Codec) *Sender {
|
func NewSender(media *Media, codec *Codec) *Sender {
|
||||||
return &Sender{Codec: codec, Media: media}
|
var bufSize uint16
|
||||||
}
|
|
||||||
|
|
||||||
// HandlerFunc like http.HandlerFunc
|
if GetKind(codec.Name) == KindVideo {
|
||||||
type HandlerFunc func(packet *rtp.Packet)
|
if codec.IsRTP() {
|
||||||
|
|
||||||
func (s *Sender) HandleRTP(track *Receiver) {
|
|
||||||
bufferSize := 100
|
|
||||||
|
|
||||||
if GetKind(track.Codec.Name) == KindVideo {
|
|
||||||
if track.Codec.IsRTP() {
|
|
||||||
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
||||||
// for the h264.RTPDepay => RTPPay queue
|
// for the h264.RTPDepay => RTPPay queue
|
||||||
bufferSize = 5000
|
bufSize = 4096
|
||||||
} else {
|
} else {
|
||||||
bufferSize = 50
|
bufSize = 64
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bufSize = 128
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make(chan *rtp.Packet, bufferSize)
|
buf := make(chan *Packet, bufSize)
|
||||||
|
s := &Sender{
|
||||||
track.mu.Lock()
|
Node: Node{id: NewID(), Codec: codec},
|
||||||
if track.senders == nil {
|
Media: media,
|
||||||
track.senders = map[*Sender]chan *rtp.Packet{}
|
buf: buf,
|
||||||
}
|
}
|
||||||
track.senders[s] = buffer
|
s.Input = func(packet *Packet) {
|
||||||
track.mu.Unlock()
|
// writing to nil chan - OK, writing to closed chan - panic
|
||||||
s.mu.Lock()
|
|
||||||
s.receivers = append(s.receivers, track)
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// read packets from buffer channel until it will be closed
|
|
||||||
for packet := range buffer {
|
|
||||||
s.bytes += len(packet.Payload)
|
|
||||||
s.Handler(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove current receiver from list
|
|
||||||
// it can only happen when receiver close buffer channel
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
for i, receiver := range s.receivers {
|
select {
|
||||||
if receiver == track {
|
case s.buf <- packet:
|
||||||
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
|
s.Bytes += len(packet.Payload)
|
||||||
break
|
s.Packets++
|
||||||
}
|
default:
|
||||||
|
s.Drops++
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
s.Output = func(packet *Packet) {
|
||||||
|
s.Handler(packet)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: should be removed
|
||||||
|
func (s *Sender) HandleRTP(parent *Receiver) {
|
||||||
|
s.WithParent(parent)
|
||||||
|
s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: should be removed
|
||||||
|
func (s *Sender) Bind(parent *Receiver) {
|
||||||
|
s.WithParent(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) WithParent(parent *Receiver) *Sender {
|
||||||
|
s.Node.WithParent(&parent.Node)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) Start() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.buf == nil || s.done != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.done = make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for packet := range s.buf {
|
||||||
|
s.Output(packet)
|
||||||
|
}
|
||||||
|
close(s.done)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) Close() {
|
func (s *Sender) Wait() {
|
||||||
s.mu.Lock()
|
if done := s.done; s.done != nil {
|
||||||
// remove this sender from all receivers list
|
<-done
|
||||||
for _, receiver := range s.receivers {
|
|
||||||
receiver.mu.Lock()
|
|
||||||
if buffer := receiver.senders[s]; buffer != nil {
|
|
||||||
// remove channel from list
|
|
||||||
delete(receiver.senders, s)
|
|
||||||
// close channel
|
|
||||||
close(buffer)
|
|
||||||
}
|
|
||||||
receiver.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
s.receivers = nil
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) String() string {
|
func (s *Sender) State() string {
|
||||||
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
|
if s.buf == nil {
|
||||||
s.mu.RLock()
|
return "closed"
|
||||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
|
||||||
s.mu.RUnlock()
|
|
||||||
if s.overflow > 0 {
|
|
||||||
info += ", overflow=" + strconv.Itoa(s.overflow)
|
|
||||||
}
|
}
|
||||||
return info
|
if s.done == nil {
|
||||||
|
return "new"
|
||||||
|
}
|
||||||
|
return "connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) Close() {
|
||||||
|
// close buffer if exists
|
||||||
|
if buf := s.buf; buf != nil {
|
||||||
|
s.buf = nil
|
||||||
|
defer close(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
func (s *Sender) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(s.String())
|
v := struct {
|
||||||
}
|
ID uint32 `json:"id"`
|
||||||
|
Codec *Codec `json:"codec"`
|
||||||
// VA - helper, for extract video and audio receivers from list
|
Parent uint32 `json:"parent,omitempty"`
|
||||||
func VA(receivers []*Receiver) (video, audio *Receiver) {
|
Bytes int `json:"bytes,omitempty"`
|
||||||
for _, receiver := range receivers {
|
Packets int `json:"packets,omitempty"`
|
||||||
switch GetKind(receiver.Codec.Name) {
|
Drops int `json:"drops,omitempty"`
|
||||||
case KindVideo:
|
}{
|
||||||
video = receiver
|
ID: s.Node.id,
|
||||||
case KindAudio:
|
Codec: s.Node.Codec,
|
||||||
audio = receiver
|
Bytes: s.Bytes,
|
||||||
}
|
Packets: s.Packets,
|
||||||
|
Drops: s.Drops,
|
||||||
}
|
}
|
||||||
return
|
if s.parent != nil {
|
||||||
|
v.Parent = s.parent.id
|
||||||
|
}
|
||||||
|
return json.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"%s: size:%6d, ts:%10d, type:%2d, ssrc:%d, seq:%5d, mark:%t, dts:%4d, dtime:%3d\n",
|
"%s: size=%6d ts=%10d type=%2d ssrc=%d seq=%5d mark=%t dts=%4d dtime=%3dms\n",
|
||||||
now.Format("15:04:05.000"),
|
now.Format("15:04:05.000"),
|
||||||
len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
||||||
packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),
|
packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),
|
||||||
@@ -41,7 +41,7 @@ func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {
|
|||||||
|
|
||||||
if dt := now.Sub(secTime); dt > time.Second {
|
if dt := now.Sub(secTime); dt > time.Second {
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"%s: size:%6d, cnt:%d, dts: %d, dtime:%d\n",
|
"%s: size=%6d cnt=%d dts=%d dtime=%3dms\n",
|
||||||
now.Format("15:04:05.000"),
|
now.Format("15:04:05.000"),
|
||||||
secSize, secCnt, lastTS-secTS, dt.Milliseconds(),
|
secSize, secCnt, lastTS-secTS, dt.Milliseconds(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Backchannel struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
client *Client
|
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
|
return nil, core.ErrCantGetTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Start() error {
|
func (c *Backchannel) Start() error {
|
||||||
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -30,12 +30,7 @@ func (c *Consumer) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Stop() error {
|
func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||||
_ = c.SuperConsumer.Close()
|
|
||||||
return c.client.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
|
||||||
if err := c.client.Talk(); err != nil {
|
if err := c.client.Talk(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
+1
-1
@@ -114,7 +114,7 @@ func (c *Client) Play() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Talk() error {
|
func (c *Client) Talk() error {
|
||||||
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s","AudioFormat":{"EncodeType":"G711_ALAW"}}}` + "\x0A\x00"
|
||||||
|
|
||||||
data := fmt.Sprintf(format, c.session, "Claim")
|
data := fmt.Sprintf(format, c.session, "Claim")
|
||||||
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user