mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-26 20:31:11 +08:00
Merge branch 'master' into feat-logging-to-file
This commit is contained in:
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with: { go-version: '1.21' }
|
||||
with: { go-version: '1.22' }
|
||||
|
||||
- name: Build go2rtc_win64
|
||||
env: { GOOS: windows, GOARCH: amd64 }
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win32
|
||||
env: { GOOS: windows, GOARCH: 386 }
|
||||
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win32
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_amd64
|
||||
env: { GOOS: darwin, GOARCH: amd64 }
|
||||
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_amd64
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -102,14 +102,14 @@ jobs:
|
||||
env: { GOOS: freebsd, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_amd64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_freebsd_arm64
|
||||
env: { GOOS: freebsd, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
|
||||
|
||||
docker-master:
|
||||
@@ -123,7 +123,9 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ github.repository }}
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}},enable=false
|
||||
@@ -142,6 +144,14 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
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
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -149,6 +159,7 @@ jobs:
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/386
|
||||
linux/arm/v6
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
@@ -168,7 +179,9 @@ jobs:
|
||||
id: meta-hw
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ github.repository }}
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-hardware,onlatest=true
|
||||
latest=auto
|
||||
@@ -189,6 +202,14 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
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
|
||||
uses: docker/build-push-action@v5
|
||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Build Go binary
|
||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
go2rtc.yaml
|
||||
go2rtc.json
|
||||
|
||||
go2rtc_freebsd*
|
||||
go2rtc_linux*
|
||||
go2rtc_mac*
|
||||
go2rtc_win*
|
||||
|
31
Dockerfile
31
Dockerfile
@@ -2,14 +2,19 @@
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.21"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
||||
ARG GO_VERSION="1.22"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
# 1. Download ngrok binary (for support arm/v6)
|
||||
FROM alpine AS ngrok
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz /
|
||||
RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin
|
||||
|
||||
|
||||
# 2. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
@@ -30,21 +35,14 @@ COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||
# font-droid for FFmpeg drawtext filter (+2MB)
|
||||
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
|
||||
RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
|
||||
|
||||
# Hardware Acceleration for Intel CPU (+50MB)
|
||||
ARG TARGETARCH
|
||||
@@ -56,7 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
||||
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||
|
||||
COPY --from=rootfs / /
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
VOLUME /config
|
||||
|
19
README.md
19
README.md
@@ -115,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
|
||||
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
||||
|
||||
- `go2rtc_win64.zip` - Windows 64-bit
|
||||
- `go2rtc_win32.zip` - Windows 32-bit
|
||||
- `go2rtc_win64.zip` - Windows 10+ 64-bit
|
||||
- `go2rtc_win32.zip` - Windows 7+ 32-bit
|
||||
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
||||
- `go2rtc_linux_amd64` - Linux 64-bit
|
||||
- `go2rtc_linux_i386` - Linux 32-bit
|
||||
@@ -124,8 +124,10 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
||||
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
||||
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
|
||||
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
|
||||
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
|
||||
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
||||
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
|
||||
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
|
||||
- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit
|
||||
- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit
|
||||
|
||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
@@ -170,7 +172,7 @@ Available modules:
|
||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||
- [webrtc](#module-webrtc) - WebRTC Server
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server
|
||||
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||
@@ -648,10 +650,11 @@ This source type support Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||
- Roborock S7 MaxV - video and two way audio
|
||||
- Roborock Qrevo MaxV - video and two way audio
|
||||
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link.
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
@@ -880,7 +883,7 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format.
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
|
26
examples/go2rtc_mjpeg/main.go
Normal file
26
examples/go2rtc_mjpeg/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Init()
|
||||
streams.Init()
|
||||
|
||||
api.Init()
|
||||
ws.Init()
|
||||
|
||||
ffmpeg.Init()
|
||||
mjpeg.Init()
|
||||
v4l2.Init()
|
||||
|
||||
shell.RunUntilSignal()
|
||||
}
|
72
examples/onvif_client/main.go
Normal file
72
examples/onvif_client/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/onvif"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var rawURL = os.Args[1]
|
||||
var operation = os.Args[2]
|
||||
var token string
|
||||
if len(os.Args) > 3 {
|
||||
token = os.Args[3]
|
||||
}
|
||||
|
||||
client, err := onvif.NewClient(rawURL)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
switch operation {
|
||||
case onvif.ServiceGetServiceCapabilities:
|
||||
b, err = client.MediaRequest(operation)
|
||||
case onvif.DeviceGetCapabilities,
|
||||
onvif.DeviceGetDeviceInformation,
|
||||
onvif.DeviceGetDiscoveryMode,
|
||||
onvif.DeviceGetDNS,
|
||||
onvif.DeviceGetHostname,
|
||||
onvif.DeviceGetNetworkDefaultGateway,
|
||||
onvif.DeviceGetNetworkInterfaces,
|
||||
onvif.DeviceGetNetworkProtocols,
|
||||
onvif.DeviceGetNTP,
|
||||
onvif.DeviceGetScopes,
|
||||
onvif.DeviceGetServices,
|
||||
onvif.DeviceGetSystemDateAndTime,
|
||||
onvif.DeviceSystemReboot:
|
||||
b, err = client.DeviceRequest(operation)
|
||||
case onvif.MediaGetProfiles, onvif.MediaGetVideoSources:
|
||||
b, err = client.MediaRequest(operation)
|
||||
case onvif.MediaGetProfile:
|
||||
b, err = client.GetProfile(token)
|
||||
case onvif.MediaGetVideoSourceConfiguration:
|
||||
b, err = client.GetVideoSourceConfiguration(token)
|
||||
case onvif.MediaGetStreamUri:
|
||||
b, err = client.GetStreamUri(token)
|
||||
case onvif.MediaGetSnapshotUri:
|
||||
b, err = client.GetSnapshotUri(token)
|
||||
default:
|
||||
log.Printf("unknown action\n")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(u.Host)
|
||||
|
||||
if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
}
|
39
examples/rtsp_client/main.go
Normal file
39
examples/rtsp_client/main.go
Normal file
@@ -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()
|
||||
}
|
52
go.mod
52
go.mod
@@ -1,48 +1,50 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.21
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.16.5
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/miekg/dns v1.1.59
|
||||
github.com/pion/ice/v2 v2.3.24
|
||||
github.com/pion/interceptor v0.1.29
|
||||
github.com/pion/rtcp v1.2.14
|
||||
github.com/pion/rtp v1.8.6
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/pion/ice/v2 v2.3.37
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.10
|
||||
github.com/pion/sdp/v3 v3.0.9
|
||||
github.com/pion/srtp/v2 v2.0.18
|
||||
github.com/pion/srtp/v2 v2.0.20
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.2.40
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/pion/webrtc/v3 v3.3.5
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/asticode/go-astikit v0.45.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // 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/dtls/v2 v2.2.11 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.16 // indirect
|
||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||
github.com/pion/sctp v1.8.35 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/tools v0.20.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
)
|
||||
|
138
go.sum
138
go.sum
@@ -1,46 +1,45 @@
|
||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw=
|
||||
github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
||||
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
|
||||
github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
|
||||
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
|
||||
github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI=
|
||||
github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
|
||||
github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
@@ -48,43 +47,42 @@ github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYF
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
|
||||
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
||||
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||
github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
|
||||
github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
|
||||
github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
|
||||
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o=
|
||||
github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA=
|
||||
github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU=
|
||||
github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
|
||||
github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
|
||||
github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
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=
|
||||
@@ -98,47 +96,42 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -150,40 +143,37 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
@@ -4,7 +4,7 @@
|
||||
# only debian 13 (trixie) has latest ffmpeg
|
||||
# https://packages.debian.org/trixie/ffmpeg
|
||||
ARG DEBIAN_VERSION="trixie-slim"
|
||||
ARG GO_VERSION="1.21-bookworm"
|
||||
ARG GO_VERSION="1.22-bookworm"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM debian:${DEBIAN_VERSION} AS base
|
||||
|
@@ -69,6 +69,8 @@ func Init() {
|
||||
}
|
||||
|
||||
if cfg.Mod.Listen != "" {
|
||||
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||
Port, _ = strconv.Atoi(port)
|
||||
go listen("tcp", cfg.Mod.Listen)
|
||||
}
|
||||
|
||||
@@ -92,10 +94,6 @@ func listen(network, address string) {
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] listen")
|
||||
|
||||
if network == "tcp" {
|
||||
Port = ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
Handler: Handler,
|
||||
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -38,20 +39,19 @@ type Message struct {
|
||||
Value any `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
func (m *Message) String() (value string) {
|
||||
if s, ok := m.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Message) GetString(key string) string {
|
||||
if v, ok := m.Value.(map[string]any); ok {
|
||||
if s, ok := v[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
func (m *Message) Unmarshal(v any) error {
|
||||
b, err := json.Marshal(m.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ""
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
|
@@ -1,6 +1,10 @@
|
||||
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
|
||||
- go2rtc support multiple config files
|
||||
- go2rtc support inline config as `YAML`, `JSON` or `key=value` format from command line
|
||||
- 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)
|
||||
|
||||
```
|
||||
@@ -15,21 +19,30 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local
|
||||
|
||||
## Environment variables
|
||||
|
||||
Also go2rtc support templates for using environment variables in any part of config:
|
||||
There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||
|
||||
${LOGS:} # empty default value
|
||||
|
||||
rtsp:
|
||||
username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
||||
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set
|
||||
```
|
||||
|
||||
## JSON Schema
|
||||
|
||||
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"
|
||||
@@ -38,7 +51,10 @@ ffmpeg:
|
||||
bin: "ffmpeg"
|
||||
|
||||
log:
|
||||
format: "color"
|
||||
level: "info"
|
||||
output: "stdout"
|
||||
time: "UNIXMS"
|
||||
|
||||
rtsp:
|
||||
listen: ":8554"
|
||||
@@ -51,4 +67,4 @@ webrtc:
|
||||
listen: ":8555/tcp"
|
||||
ice_servers:
|
||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||
```
|
||||
```
|
||||
|
@@ -1,29 +1,20 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Version = "1.9.2"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
var Info = map[string]any{
|
||||
"version": Version,
|
||||
}
|
||||
var (
|
||||
Version string
|
||||
UserAgent string
|
||||
ConfigPath string
|
||||
Info = make(map[string]any)
|
||||
)
|
||||
|
||||
const usage = `Usage of go2rtc:
|
||||
|
||||
@@ -33,12 +24,12 @@ const usage = `Usage of go2rtc:
|
||||
`
|
||||
|
||||
func Init() {
|
||||
var confs Config
|
||||
var config flagConfig
|
||||
var daemon bool
|
||||
var version bool
|
||||
|
||||
flag.Var(&confs, "config", "")
|
||||
flag.Var(&confs, "c", "")
|
||||
flag.Var(&config, "config", "")
|
||||
flag.Var(&config, "c", "")
|
||||
flag.BoolVar(&daemon, "daemon", false, "")
|
||||
flag.BoolVar(&daemon, "d", false, "")
|
||||
flag.BoolVar(&version, "version", false, "")
|
||||
@@ -54,133 +45,39 @@ func Init() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if daemon {
|
||||
if daemon && os.Getppid() != 1 {
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Println("Daemon not supported on Windows")
|
||||
fmt.Println("Daemon mode is not supported on Windows")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
for i, arg := range args {
|
||||
if arg == "-daemon" {
|
||||
args[i] = ""
|
||||
}
|
||||
}
|
||||
// Re-run the program in background and exit
|
||||
cmd := exec.Command(os.Args[0], args...)
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
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)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
UserAgent = "go2rtc/" + Version
|
||||
|
||||
Info["version"] = Version
|
||||
Info["revision"] = revision
|
||||
|
||||
var cfg struct {
|
||||
Mod map[string]string `yaml:"log"`
|
||||
}
|
||||
|
||||
cfg.Mod = map[string]string{
|
||||
"format": "", // useless, but anyway
|
||||
"level": "info",
|
||||
"output": "stdout", // TODO: change to stderr someday
|
||||
"time": zerolog.TimeFormatUnixMs,
|
||||
}
|
||||
|
||||
LoadConfig(&cfg)
|
||||
|
||||
log.Logger = NewLogger(cfg.Mod)
|
||||
|
||||
modules = cfg.Mod
|
||||
initConfig(config)
|
||||
initLogger()
|
||||
|
||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
log.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
|
||||
log.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
|
||||
Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
|
||||
Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
|
||||
|
||||
if ConfigPath != "" {
|
||||
log.Info().Str("path", ConfigPath).Msg("config")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
Logger.Info().Str("path", ConfigPath).Msg("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
|
||||
|
||||
func readRevisionTime() (revision, vcsTime string) {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
@@ -202,25 +99,3 @@ func readRevisionTime() (revision, vcsTime string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
109
internal/app/config.go
Normal file
109
internal/app/config.go
Normal file
@@ -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)
|
||||
}
|
@@ -7,20 +7,39 @@ import (
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var MemoryLog = newBuffer(16)
|
||||
|
||||
// NewLogger support:
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err == nil {
|
||||
return Logger.Level(lvl)
|
||||
}
|
||||
Logger.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
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 NewLogger(config map[string]string) zerolog.Logger {
|
||||
func initLogger() {
|
||||
var cfg struct {
|
||||
Mod map[string]string `yaml:"log"`
|
||||
}
|
||||
|
||||
cfg.Mod = modules // defaults
|
||||
|
||||
LoadConfig(&cfg)
|
||||
|
||||
var writer io.Writer
|
||||
|
||||
switch output, path, _ := strings.Cut(config["output"], ":"); output {
|
||||
switch output, path, _ := strings.Cut(modules["output"], ":"); output {
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "stdout":
|
||||
@@ -33,10 +52,10 @@ func NewLogger(config map[string]string) zerolog.Logger {
|
||||
writer, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
timeFormat := config["time"]
|
||||
timeFormat := modules["time"]
|
||||
|
||||
if writer != nil {
|
||||
if format := config["format"]; format != "json" {
|
||||
if format := modules["format"]; format != "json" {
|
||||
console := &zerolog.ConsoleWriter{Out: writer}
|
||||
|
||||
switch format {
|
||||
@@ -68,31 +87,24 @@ func NewLogger(config map[string]string) zerolog.Logger {
|
||||
writer = MemoryLog
|
||||
}
|
||||
|
||||
logger := zerolog.New(writer)
|
||||
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||
Logger = zerolog.New(writer).Level(lvl)
|
||||
|
||||
if timeFormat != "" {
|
||||
zerolog.TimeFieldFormat = timeFormat
|
||||
logger = logger.With().Timestamp().Logger()
|
||||
Logger = Logger.With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
lvl, _ := zerolog.ParseLevel(config["level"])
|
||||
return logger.Level(lvl)
|
||||
}
|
||||
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err == nil {
|
||||
return log.Level(lvl)
|
||||
}
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
return log.Logger
|
||||
}
|
||||
var Logger zerolog.Logger
|
||||
|
||||
// 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
|
||||
|
||||
|
@@ -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() {
|
||||
streams.HandleFunc("bubble", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := bubble.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
|
||||
return bubble.Dial(source)
|
||||
})
|
||||
}
|
||||
|
@@ -2,16 +2,8 @@ package debug
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
api.HandleFunc("api/stack", stackHandler)
|
||||
|
||||
streams.HandleFunc("null", nullHandler)
|
||||
}
|
||||
|
||||
func nullHandler(string) (core.Producer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
36
internal/doorbird/doorbird.go
Normal file
36
internal/doorbird/doorbird.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package doorbird
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/doorbird"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.RedirectFunc("doorbird", func(rawURL string) (string, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// https://www.doorbird.com/downloads/api_lan.pdf
|
||||
switch u.Query().Get("media") {
|
||||
case "video":
|
||||
u.Path = "/bha-api/video.cgi"
|
||||
case "audio":
|
||||
u.Path = "/bha-api/audio-receive.cgi"
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
|
||||
u.Scheme = "http"
|
||||
|
||||
return u.String(), nil
|
||||
})
|
||||
|
||||
streams.HandleFunc("doorbird", func(source string) (core.Producer, error) {
|
||||
return doorbird.Dial(source)
|
||||
})
|
||||
}
|
@@ -10,26 +10,16 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("dvrip", handle)
|
||||
streams.HandleFunc("dvrip", dvrip.Dial)
|
||||
|
||||
// DVRIP client autodiscovery
|
||||
api.HandleFunc("api/dvrip", apiDvrip)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
client, err := dvrip.Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const Port = 34569 // UDP port number for dvrip discovery
|
||||
|
||||
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -92,10 +82,7 @@ func sendBroadcasts(conn *net.UDPConn) {
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if _, err = conn.WriteToUDP(data, addr); err != nil {
|
||||
log.Err(err).Caller().Send()
|
||||
}
|
||||
_, _ = conn.WriteToUDP(data, addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,17 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -47,9 +49,11 @@ func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
func execHandle(rawURL string) (core.Producer, error) {
|
||||
func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
|
||||
var path string
|
||||
var query url.Values
|
||||
|
||||
// RTSP flow should have `{output}` inside URL
|
||||
// pipe flow may have `#{params}` inside URL
|
||||
@@ -61,35 +65,59 @@ func execHandle(rawURL string) (core.Producer, error) {
|
||||
sum := md5.Sum([]byte(rawURL))
|
||||
path = "/" + hex.EncodeToString(sum[:])
|
||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
|
||||
query = streams.ParseQuery(rawURL[i+1:])
|
||||
rawURL = rawURL[:i]
|
||||
}
|
||||
|
||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
|
||||
cmd.Stderr = &logWriter{
|
||||
buf: make([]byte, 512),
|
||||
debug: log.Debug().Enabled(),
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return handlePipe(rawURL, cmd, query)
|
||||
if s := query.Get("killsignal"); s != "" {
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
cmd.Cancel = func() error {
|
||||
log.Debug().Msgf("[exec] kill with signal=%d", sig)
|
||||
return cmd.Process.Signal(sig)
|
||||
}
|
||||
}
|
||||
|
||||
return handleRTSP(rawURL, cmd, path)
|
||||
}
|
||||
if s := query.Get("killtimeout"); s != "" {
|
||||
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
|
||||
}
|
||||
|
||||
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
||||
if query.Get("backchannel") == "1" {
|
||||
return stdin.NewClient(cmd)
|
||||
}
|
||||
|
||||
r, err := PipeCloser(cmd, query)
|
||||
if path == "" {
|
||||
prod, err = handlePipe(rawURL, cmd)
|
||||
} else {
|
||||
prod, err = handleRTSP(rawURL, cmd, path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = cmd.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd := struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
// add buffer for pipe reader to reduce syscall
|
||||
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||
// stop cmd on close pipe call
|
||||
cmd,
|
||||
}
|
||||
|
||||
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||
|
||||
ts := time.Now()
|
||||
@@ -98,22 +126,27 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod, err := magic.Open(r)
|
||||
prod, err := magic.Open(rd)
|
||||
if err != nil {
|
||||
_ = r.Close()
|
||||
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||
}
|
||||
|
||||
if info, ok := prod.(core.Info); ok {
|
||||
info.SetProtocol("pipe")
|
||||
setRemoteInfo(info, source, cmd.Args)
|
||||
}
|
||||
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
|
||||
|
||||
return prod, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||
func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) {
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
waiter := make(chan core.Producer)
|
||||
waiter := make(chan *pkg.Conn, 1)
|
||||
|
||||
waitersMu.Lock()
|
||||
waiters[path] = waiter
|
||||
@@ -130,25 +163,26 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||
ts := time.Now()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("[exec]")
|
||||
log.Error().Err(err).Str("source", source).Msg("[exec]")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
timeout := time.NewTimer(30 * time.Second)
|
||||
defer timeout.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 60):
|
||||
_ = cmd.Process.Kill()
|
||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||
case <-timeout.C:
|
||||
// haven't received data from app in timeout
|
||||
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||
return nil, errors.New("exec: timeout")
|
||||
case <-done:
|
||||
// limit message size
|
||||
case <-cmd.Done():
|
||||
// app fail before we receive any data
|
||||
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||
case prod := <-waiter:
|
||||
// app started successfully
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||
setRemoteInfo(prod, source, cmd.Args)
|
||||
prod.OnClose = cmd.Close
|
||||
return prod, nil
|
||||
}
|
||||
}
|
||||
@@ -157,7 +191,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||
|
||||
var (
|
||||
log zerolog.Logger
|
||||
waiters = map[string]chan core.Producer{}
|
||||
waiters = make(map[string]chan *pkg.Conn)
|
||||
waitersMu sync.Mutex
|
||||
)
|
||||
|
||||
@@ -205,3 +239,15 @@ func trimSpace(b []byte) []byte {
|
||||
}
|
||||
return b[start:stop]
|
||||
}
|
||||
|
||||
func setRemoteInfo(info core.Info, source string, args []string) {
|
||||
info.SetSource(source)
|
||||
|
||||
if i := core.Index(args, "-i"); i > 0 && i < len(args)-1 {
|
||||
rawURL := args[i+1]
|
||||
if u, err := url.Parse(rawURL); err == nil && u.Host != "" {
|
||||
info.SetRemoteAddr(u.Host)
|
||||
info.SetURL(rawURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,56 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
||||
func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add buffer for pipe reader to reduce syscall
|
||||
return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil
|
||||
}
|
||||
|
||||
type pipeCloser struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
cmd *exec.Cmd
|
||||
query url.Values
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Close() error {
|
||||
return errors.Join(p.Closer.Close(), p.Kill(), p.Wait())
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Kill() error {
|
||||
if s := p.query.Get("killsignal"); s != "" {
|
||||
log.Trace().Msgf("[exec] kill with custom sig=%s", s)
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
return p.cmd.Process.Signal(sig)
|
||||
}
|
||||
return p.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Wait() error {
|
||||
if s := p.query.Get("killtimeout"); s != "" {
|
||||
timeout := time.Duration(core.Atoi(s)) * time.Second
|
||||
timer := time.AfterFunc(timeout, func() {
|
||||
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
||||
_ = p.cmd.Process.Kill()
|
||||
})
|
||||
defer timer.Stop() // stop timer if Wait ends before timeout
|
||||
}
|
||||
return p.cmd.Wait()
|
||||
}
|
@@ -2,7 +2,6 @@ package ffmpeg
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -29,6 +29,8 @@ func Init() {
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("ffmpeg")
|
||||
|
||||
// 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" {
|
||||
@@ -41,7 +43,7 @@ func Init() {
|
||||
return "", err
|
||||
}
|
||||
args := parseArgs(url[7:])
|
||||
if slices.Contains(args.Codecs, "auto") {
|
||||
if core.Contains(args.Codecs, "auto") {
|
||||
return "", nil // force call streams.HandleFunc("ffmpeg")
|
||||
}
|
||||
return "exec:" + args.String(), nil
|
||||
@@ -145,6 +147,8 @@ var defaults = map[string]string{
|
||||
"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
|
||||
func configTemplate(template string) string {
|
||||
if s := defaults[template]; s != "" {
|
||||
@@ -175,6 +179,7 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
Version: verAV,
|
||||
}
|
||||
|
||||
var source = s
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||
query = streams.ParseQuery(s[i+1:])
|
||||
@@ -217,6 +222,10 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
default:
|
||||
s += "?video&audio"
|
||||
}
|
||||
s += "&source=ffmpeg:" + url.QueryEscape(source)
|
||||
for _, v := range query["query"] {
|
||||
s += "&" + v
|
||||
}
|
||||
args.Input = inputTemplate("rtsp", s, query)
|
||||
} else if i = strings.Index(s, "?"); i > 0 {
|
||||
switch s[:i] {
|
||||
|
@@ -31,7 +31,7 @@ func TestParseArgsFile(t *testing.T) {
|
||||
{
|
||||
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
||||
source: "/media/bbb.mp4#video=h265#rotate=-90",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||
@@ -53,85 +53,143 @@ func TestParseArgsFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseArgsDevice(t *testing.T) {
|
||||
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
||||
args := parseArgs("device?video=0&video_size=1920x1080")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
||||
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
||||
args = parseArgs("device?video=0&framerate=20#video=h265")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
|
||||
source: "device?video=0&video_size=1920x1080",
|
||||
expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped",
|
||||
source: "device?video=0&framerate=20#video=h265",
|
||||
expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[DEVICE] video/audio",
|
||||
source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)",
|
||||
expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsIpCam(t *testing.T) {
|
||||
// [HTTP] video will be copied
|
||||
args := parseArgs("http://example.com")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args = parseArgs("http://example.com#video=h264")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [HLS] video will be copied, audio will be skipped
|
||||
args = parseArgs("https://example.com#video=copy")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video will be copied without transcoding codecs
|
||||
args = parseArgs("rtsp://example.com")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
||||
args = parseArgs("rtsp://example.com#input=rtsp/udp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
||||
args = parseArgs("rtmp://example.com#input=rtsp/udp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[HTTP] video will be copied",
|
||||
source: "http://example.com",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||
source: "http://example.com#video=h264",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[HLS] video will be copied, audio will be skipped",
|
||||
source: "https://example.com#video=copy",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video will be copied without transcoding codecs",
|
||||
source: "rtsp://example.com",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
|
||||
source: "rtsp://example.com#video=h265#width=1280#height=720",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
|
||||
source: "rtsp://example.com#input=rtsp/udp",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
|
||||
source: "rtmp://example.com#input=rtsp/udp",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsAudio(t *testing.T) {
|
||||
// [AUDIO] audio will be transcoded to AAC, video will be skipped
|
||||
args := parseArgs("rtsp:///example.com#audio=aac")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to AAC/16000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=aac/16000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMA, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcma")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcma/16000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcma/48000")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
|
||||
source: "rtsp://example.com#audio=aac",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=aac/16000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
|
||||
source: "rtsp://example.com#audio=opus",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcmu",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcmu/16000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcmu/48000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcma",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcma/16000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
{
|
||||
name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped",
|
||||
source: "rtsp://example.com#audio=pcma/48000",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsHwVaapi(t *testing.T) {
|
||||
|
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -152,7 +150,6 @@ var cache = map[string]string{}
|
||||
|
||||
func run(bin string, args string) bool {
|
||||
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
||||
log.Printf("%v %v", args, err)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
|
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
url string
|
||||
query url.Values
|
||||
ffmpeg core.Producer
|
||||
@@ -31,7 +31,8 @@ func NewProducer(url string) (core.Producer, error) {
|
||||
return nil, errors.New("ffmpeg: unsupported params: " + url[i:])
|
||||
}
|
||||
|
||||
p.Type = "FFmpeg producer"
|
||||
p.ID = core.NewID()
|
||||
p.FormatName = "ffmpeg"
|
||||
p.Medias = []*core.Media{
|
||||
{
|
||||
// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG
|
||||
@@ -81,7 +82,7 @@ func (p *Producer) Stop() error {
|
||||
|
||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if p.ffmpeg == nil {
|
||||
return json.Marshal(p.SuperProducer)
|
||||
return json.Marshal(p.Connection)
|
||||
}
|
||||
return json.Marshal(p.ffmpeg)
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var verMu sync.Mutex
|
||||
|
@@ -10,15 +10,13 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("gopro", handleGoPro)
|
||||
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
|
||||
return gopro.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/gopro", apiGoPro)
|
||||
}
|
||||
|
||||
func handleGoPro(rawURL string) (core.Producer, error) {
|
||||
return gopro.Dial(rawURL)
|
||||
}
|
||||
|
||||
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||
var items []*api.Source
|
||||
|
||||
|
@@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@@ -45,14 +45,9 @@ func Init() {
|
||||
return "", nil
|
||||
})
|
||||
|
||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
||||
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
|
||||
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||
client, err := hass.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
return hass.NewClient(source)
|
||||
})
|
||||
|
||||
// load static entries from Hass config
|
||||
|
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
medias := mp4.ParseQuery(r.URL.Query())
|
||||
if medias != nil {
|
||||
c := mp4.NewConsumer(medias)
|
||||
c.Type = "HLS/fMP4 consumer"
|
||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
||||
c.UserAgent = r.UserAgent()
|
||||
c.FormatName = "hls/fmp4"
|
||||
c.WithRequest(r)
|
||||
cons = c
|
||||
} else {
|
||||
c := mpegts.NewConsumer()
|
||||
c.Type = "HLS/TS consumer"
|
||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
||||
c.UserAgent = r.UserAgent()
|
||||
c.FormatName = "hls/mpegts"
|
||||
c.WithRequest(r)
|
||||
cons = c
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||
@@ -20,9 +19,8 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||
codecs := msg.String()
|
||||
medias := mp4.ParseCodecs(codecs, true)
|
||||
cons := mp4.NewConsumer(medias)
|
||||
cons.Type = "HLS/fMP4 consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.FormatName = "hls/fmp4"
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
||||
|
||||
|
@@ -133,12 +133,19 @@ func Init() {
|
||||
var log zerolog.Logger
|
||||
var servers map[string]*server
|
||||
|
||||
func streamHandler(url string) (core.Producer, error) {
|
||||
func streamHandler(rawURL string) (core.Producer, error) {
|
||||
if srtp.Server == nil {
|
||||
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) {
|
||||
@@ -199,3 +206,24 @@ func findHomeKitURL(stream *streams.Stream) string {
|
||||
|
||||
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)
|
||||
}
|
||||
|
@@ -11,9 +11,10 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||
"github.com/AlexxIT/go2rtc/pkg/image"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/multipart"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,21 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
prod, err := do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info, ok := prod.(core.Info); ok {
|
||||
info.SetProtocol("http")
|
||||
info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn
|
||||
info.SetURL(rawURL)
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func do(req *http.Request) (core.Producer, error) {
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,14 +82,15 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case ct == "image/jpeg":
|
||||
return mjpeg.NewClient(res), nil
|
||||
|
||||
case ct == "multipart/x-mixed-replace":
|
||||
return multipart.Open(res.Body)
|
||||
|
||||
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
|
||||
return hls.OpenURL(req.URL, res.Body)
|
||||
case ct == "image/jpeg":
|
||||
return image.Open(res)
|
||||
case ct == "multipart/x-mixed-replace":
|
||||
return mpjpeg.Open(res.Body)
|
||||
//https://www.iana.org/assignments/media-types/audio/basic
|
||||
case ct == "audio/basic":
|
||||
return pcm.Open(res.Body)
|
||||
}
|
||||
|
||||
return magic.Open(res.Body)
|
||||
|
@@ -7,16 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("isapi", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn, err := isapi.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
streams.HandleFunc("isapi", func(source string) (core.Producer, error) {
|
||||
return isapi.Dial(source)
|
||||
})
|
||||
}
|
||||
|
@@ -4,16 +4,10 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ivideon", func(url string) (core.Producer, error) {
|
||||
id := strings.Replace(url[8:], "/", ":", 1)
|
||||
prod := ivideon.NewClient(id)
|
||||
if err := prod.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
|
||||
return ivideon.Dial(source)
|
||||
})
|
||||
}
|
||||
|
@@ -10,15 +10,16 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ascii"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/y4m"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -28,8 +29,12 @@ func Init() {
|
||||
api.HandleFunc("api/stream.y4m", apiStreamY4M)
|
||||
|
||||
ws.HandleFunc("mjpeg", handlerWS)
|
||||
|
||||
log = app.GetLogger("mjpeg")
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
@@ -39,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := magic.NewKeyframe()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
@@ -95,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := mjpeg.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||
@@ -112,7 +115,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
wr := mjpeg.NewWriter(w)
|
||||
_, _ = cons.WriteTo(wr)
|
||||
} else {
|
||||
cons.Type = "ASCII passive consumer "
|
||||
cons.FormatName = "ascii"
|
||||
|
||||
query := r.URL.Query()
|
||||
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
|
||||
@@ -130,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
|
||||
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
|
||||
prod, _ := mpjpeg.Open(r.Body)
|
||||
prod.WithRequest(r)
|
||||
|
||||
client := mjpeg.NewClient(res)
|
||||
stream.AddProducer(client)
|
||||
stream.AddProducer(prod)
|
||||
|
||||
if err := client.Start(); err != nil && err != io.EOF {
|
||||
if err := prod.Start(); err != nil && err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
stream.RemoveProducer(client)
|
||||
stream.RemoveProducer(prod)
|
||||
}
|
||||
|
||||
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
@@ -150,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
}
|
||||
|
||||
cons := mjpeg.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
||||
@@ -178,8 +179,7 @@ func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := y4m.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -99,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
medias := mp4.ParseQuery(r.URL.Query())
|
||||
cons := mp4.NewConsumer(medias)
|
||||
cons.Type = "MP4/HTTP active consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.FormatName = "mp4"
|
||||
cons.Protocol = "http"
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
@@ -127,20 +127,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
}
|
||||
|
||||
var duration *time.Timer
|
||||
if s := query.Get("duration"); s != "" {
|
||||
if i, _ := strconv.Atoi(s); i > 0 {
|
||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||
_ = cons.Stop()
|
||||
})
|
||||
}
|
||||
ctx := r.Context() // handle when the client drops the connection
|
||||
|
||||
if i := core.Atoi(query.Get("duration")); i > 0 {
|
||||
timeout := time.Second * time.Duration(i)
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = cons.Stop()
|
||||
stream.RemoveConsumer(cons)
|
||||
}()
|
||||
|
||||
_, _ = cons.WriteTo(w)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
if duration != nil {
|
||||
duration.Stop()
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
@@ -24,9 +23,8 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
cons := mp4.NewConsumer(medias)
|
||||
cons.Type = "MSE/WebSocket active consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.FormatName = "mse/fmp4"
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Debug().Err(err).Msg("[mp4] add consumer")
|
||||
@@ -57,9 +55,7 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
cons := mp4.NewKeyframe(medias)
|
||||
cons.Type = "MP4/WebSocket active consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.WithRequest(tr.Request)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -19,11 +17,9 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := aac.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -32,11 +30,9 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := mpegts.NewConsumer()
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package nest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
@@ -10,19 +11,13 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("nest", streamNest)
|
||||
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
|
||||
return nest.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/nest", apiNest)
|
||||
}
|
||||
|
||||
func streamNest(url string) (core.Producer, error) {
|
||||
client, err := nest.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
cliendID := query.Get("client_id")
|
||||
@@ -44,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var items []*api.Source
|
||||
|
||||
for name, deviceID := range devices {
|
||||
query.Set("device_id", deviceID)
|
||||
for _, device := range devices {
|
||||
query.Set("device_id", device.DeviceID)
|
||||
query.Set("protocols", strings.Join(device.Protocols, ","))
|
||||
|
||||
items = append(items, &api.Source{
|
||||
Name: name, URL: "nest:?" + query.Encode(),
|
||||
Name: device.Name, URL: "nest:?" + query.Encode(),
|
||||
})
|
||||
}
|
||||
|
||||
|
25
internal/onvif/README.md
Normal file
25
internal/onvif/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# ONVIF
|
||||
|
||||
A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
|
||||
|
||||
Go2rtc has one video source and one profile per stream.
|
||||
|
||||
## Tested clients
|
||||
|
||||
Go2rtc works as ONVIF server:
|
||||
|
||||
- Happytime onvif client (windows)
|
||||
- Home Assistant ONVIF integration (linux)
|
||||
- Onvier (android)
|
||||
- ONVIF Device Manager (windows)
|
||||
|
||||
PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
|
||||
|
||||
## Tested cameras
|
||||
|
||||
Go2rtc works as ONVIF client:
|
||||
|
||||
- Dahua IPC-K42
|
||||
- OpenIPC
|
||||
- Reolink RLC-520A
|
||||
- TP-Link Tapo TC60
|
@@ -55,49 +55,65 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
action := onvif.GetRequestAction(b)
|
||||
if action == "" {
|
||||
operation := onvif.GetRequestAction(b)
|
||||
if operation == "" {
|
||||
http.Error(w, "malformed request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] %s", action)
|
||||
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
|
||||
|
||||
var res string
|
||||
switch operation {
|
||||
case onvif.DeviceGetNetworkInterfaces, // important for Hass
|
||||
onvif.DeviceGetSystemDateAndTime, // important for Hass
|
||||
onvif.DeviceGetDiscoveryMode,
|
||||
onvif.DeviceGetDNS,
|
||||
onvif.DeviceGetHostname,
|
||||
onvif.DeviceGetNetworkDefaultGateway,
|
||||
onvif.DeviceGetNetworkProtocols,
|
||||
onvif.DeviceGetNTP,
|
||||
onvif.DeviceGetScopes:
|
||||
b = onvif.StaticResponse(operation)
|
||||
|
||||
switch action {
|
||||
case onvif.ActionGetCapabilities:
|
||||
case onvif.DeviceGetCapabilities:
|
||||
// important for Hass: Media section
|
||||
res = onvif.GetCapabilitiesResponse(r.Host)
|
||||
b = onvif.GetCapabilitiesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetSystemDateAndTime:
|
||||
// important for Hass
|
||||
res = onvif.GetSystemDateAndTimeResponse()
|
||||
case onvif.DeviceGetServices:
|
||||
b = onvif.GetServicesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetNetworkInterfaces:
|
||||
// important for Hass: none
|
||||
res = onvif.GetNetworkInterfacesResponse()
|
||||
|
||||
case onvif.ActionGetDeviceInformation:
|
||||
case onvif.DeviceGetDeviceInformation:
|
||||
// important for Hass: SerialNumber (unique server ID)
|
||||
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
|
||||
case onvif.ActionGetServiceCapabilities:
|
||||
case onvif.ServiceGetServiceCapabilities:
|
||||
// important for Hass
|
||||
res = onvif.GetServiceCapabilitiesResponse()
|
||||
// TODO: check path links to media
|
||||
b = onvif.GetMediaServiceCapabilitiesResponse()
|
||||
|
||||
case onvif.ActionSystemReboot:
|
||||
res = onvif.SystemRebootResponse()
|
||||
case onvif.DeviceSystemReboot:
|
||||
b = onvif.StaticResponse(operation)
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
case onvif.ActionGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
res = onvif.GetProfilesResponse(streams.GetAll())
|
||||
case onvif.MediaGetVideoSources:
|
||||
b = onvif.GetVideoSourcesResponse(streams.GetAll())
|
||||
|
||||
case onvif.ActionGetStreamUri:
|
||||
case onvif.MediaGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
b = onvif.GetProfilesResponse(streams.GetAll())
|
||||
|
||||
case onvif.MediaGetProfile:
|
||||
token := onvif.FindTagValue(b, "ProfileToken")
|
||||
b = onvif.GetProfileResponse(token)
|
||||
|
||||
case onvif.MediaGetVideoSourceConfiguration:
|
||||
token := onvif.FindTagValue(b, "ConfigurationToken")
|
||||
b = onvif.GetVideoSourceConfigurationResponse(token)
|
||||
|
||||
case onvif.MediaGetStreamUri:
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -105,16 +121,22 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
|
||||
res = onvif.GetStreamUriResponse(uri)
|
||||
b = onvif.GetStreamUriResponse(uri)
|
||||
|
||||
case onvif.MediaGetSnapshotUri:
|
||||
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
|
||||
b = onvif.GetSnapshotUriResponse(uri)
|
||||
|
||||
default:
|
||||
http.Error(w, "unsupported action", http.StatusBadRequest)
|
||||
http.Error(w, "unsupported operation", http.StatusBadRequest)
|
||||
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] server response:\n%s", b)
|
||||
|
||||
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
||||
if _, err = w.Write([]byte(res)); err != nil {
|
||||
if _, err = w.Write(b); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
@@ -160,7 +182,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if l := log.Trace(); l.Enabled() {
|
||||
b, _ := client.GetProfiles()
|
||||
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
|
||||
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
|
||||
}
|
||||
|
102
internal/ring/ring.go
Normal file
102
internal/ring/ring.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ring"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ring", func(source string) (core.Producer, error) {
|
||||
return ring.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/ring", apiRing)
|
||||
}
|
||||
|
||||
func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
var ringAPI *ring.RingRestClient
|
||||
var err error
|
||||
|
||||
// Check auth method
|
||||
if email := query.Get("email"); email != "" {
|
||||
// Email/Password Flow
|
||||
password := query.Get("password")
|
||||
code := query.Get("code")
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Try authentication (this will trigger 2FA if needed)
|
||||
if _, err = ringAPI.GetAuth(code); err != nil {
|
||||
if ringAPI.Using2FA {
|
||||
// Return 2FA prompt
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"needs_2fa": true,
|
||||
"prompt": ringAPI.PromptFor2FA,
|
||||
})
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Refresh Token Flow
|
||||
refreshToken := query.Get("refresh_token")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
|
||||
RefreshToken: refreshToken,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch devices
|
||||
devices, err := ringAPI.FetchRingDevices()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create clean query with only required parameters
|
||||
cleanQuery := url.Values{}
|
||||
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
||||
|
||||
var items []*api.Source
|
||||
for _, camera := range devices.AllCameras {
|
||||
cleanQuery.Set("device_id", camera.DeviceID)
|
||||
|
||||
// Stream source
|
||||
items = append(items, &api.Source{
|
||||
Name: camera.Description,
|
||||
URL: "ring:?" + cleanQuery.Encode(),
|
||||
})
|
||||
|
||||
// Snapshot source
|
||||
items = append(items, &api.Source{
|
||||
Name: camera.Description + " Snapshot",
|
||||
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
|
||||
})
|
||||
}
|
||||
|
||||
api.ResponseSources(w, items)
|
||||
}
|
@@ -11,22 +11,13 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("roborock", handle)
|
||||
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
|
||||
return roborock.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/roborock", apiHandle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := roborock.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
var Auth struct {
|
||||
UserData *roborock.UserInfo `json:"user_data"`
|
||||
BaseURL string `json:"base_url"`
|
||||
|
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -128,17 +127,13 @@ func tcpHandle(netConn net.Conn) error {
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamsHandle(url string) (core.Producer, error) {
|
||||
client, err := rtmp.DialPlay(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
return rtmp.DialPlay(url)
|
||||
}
|
||||
|
||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||
cons := flv.NewConsumer()
|
||||
run := func() {
|
||||
wr, err := rtmp.DialPublish(url)
|
||||
wr, err := rtmp.DialPublish(url, cons)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
cons.Type = "HTTP-FLV consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -147,6 +148,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
var closer func()
|
||||
|
||||
trace := log.Trace().Enabled()
|
||||
level := zerolog.WarnLevel
|
||||
|
||||
conn.Listen(func(msg any) {
|
||||
if trace {
|
||||
@@ -184,12 +186,38 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
if query.Get("backchannel") == "1" {
|
||||
conn.Medias = append(conn.Medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||
{Name: core.CodecPCM, ClockRate: 16000},
|
||||
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||
{Name: core.CodecPCM, ClockRate: 8000},
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if s := query.Get("pkt_size"); s != "" {
|
||||
conn.PacketSize = uint16(core.Atoi(s))
|
||||
}
|
||||
|
||||
// param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html
|
||||
if s := query.Get("log_level"); s != "" {
|
||||
if lvl, err := zerolog.ParseLevel(s); err == nil {
|
||||
level = lvl
|
||||
}
|
||||
}
|
||||
|
||||
// will help to protect looping requests to same source
|
||||
conn.Connection.Source = query.Get("source")
|
||||
|
||||
if err := stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,8 +254,10 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
})
|
||||
|
||||
if err := conn.Accept(); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
if errors.Is(err, rtsp.FailedAuth) {
|
||||
log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication")
|
||||
} else if err != io.EOF {
|
||||
log.WithLevel(level).Err(err).Caller().Send()
|
||||
}
|
||||
if closer != nil {
|
||||
closer()
|
||||
|
8
internal/streams/README.md
Normal file
8
internal/streams/README.md
Normal file
@@ -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}
|
||||
```
|
@@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
producers:
|
||||
for prodN, prod := range s.producers {
|
||||
// check for loop request, ex. `camera1: ffmpeg:camera1`
|
||||
if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
}
|
||||
|
||||
if prodErrors[prodN] != nil {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
@@ -129,7 +135,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
for _, media := range prodMedias {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
for _, codec := range media.Codecs {
|
||||
prod = appendString(prod, codec.PrintName())
|
||||
prod = appendString(prod, media.Kind+":"+codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +143,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
for _, media := range consMedias {
|
||||
if media.Direction == core.DirectionSendonly {
|
||||
for _, codec := range media.Codecs {
|
||||
cons = appendString(cons, codec.PrintName())
|
||||
cons = appendString(cons, media.Kind+":"+codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
124
internal/streams/api.go
Normal file
124
internal/streams/api.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
)
|
||||
|
||||
func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
// without source - return all streams list
|
||||
if src == "" && r.Method != "POST" {
|
||||
api.ResponseJSON(w, streams)
|
||||
return
|
||||
}
|
||||
|
||||
// Not sure about all this API. Should be rewrited...
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
stream := Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.WithRequest(r)
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
api.ResponsePrettyJSON(w, stream)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, streams[src])
|
||||
}
|
||||
|
||||
case "PUT":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
name = src
|
||||
}
|
||||
|
||||
if New(name, query["src"]...) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig(name, query["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")
|
||||
}
|
175
internal/streams/dot.go
Normal file
175
internal/streams/dot.go
Normal file
@@ -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"
|
||||
)
|
||||
|
||||
type Handler func(url string) (core.Producer, error)
|
||||
type Handler func(source string) (core.Producer, error)
|
||||
|
||||
var handlers = map[string]Handler{}
|
||||
|
||||
|
@@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error {
|
||||
}
|
||||
|
||||
func (s *Stream) AddInternalProducer(conn core.Producer) {
|
||||
producer := &Producer{conn: conn, state: stateInternal}
|
||||
producer := &Producer{conn: conn, state: stateInternal, url: "internal"}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
|
@@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
||||
}
|
||||
|
||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if p.conn != nil {
|
||||
return json.Marshal(p.conn)
|
||||
if conn := p.conn; conn != nil {
|
||||
return json.Marshal(conn)
|
||||
}
|
||||
|
||||
info := core.Info{URL: p.url}
|
||||
info := map[string]string{"url": p.url}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -207,7 +206,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
||||
for _, media := range conn.GetMedias() {
|
||||
switch media.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
for _, receiver := range p.receivers {
|
||||
for i, receiver := range p.receivers {
|
||||
codec := media.MatchCodec(receiver.Codec)
|
||||
if codec == nil {
|
||||
continue
|
||||
@@ -219,6 +218,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
||||
}
|
||||
|
||||
receiver.Replace(track)
|
||||
p.receivers[i] = track
|
||||
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
|
||||
|
||||
go p.worker(conn, workerID)
|
||||
|
@@ -21,6 +21,12 @@ func NewStream(source any) *Stream {
|
||||
return &Stream{
|
||||
producers: []*Producer{NewProducer(source)},
|
||||
}
|
||||
case []string:
|
||||
s := new(Stream)
|
||||
for _, str := range source {
|
||||
s.producers = append(s.producers, NewProducer(str))
|
||||
}
|
||||
return s
|
||||
case []any:
|
||||
s := new(Stream)
|
||||
for _, src := range source {
|
||||
@@ -70,7 +76,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
||||
}
|
||||
|
||||
func (s *Stream) AddProducer(prod core.Producer) {
|
||||
producer := &Producer{conn: prod, state: stateExternal}
|
||||
producer := &Producer{conn: prod, state: stateExternal, url: "external"}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
@@ -112,19 +118,12 @@ producers:
|
||||
}
|
||||
|
||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
if !s.mu.TryLock() {
|
||||
log.Warn().Msgf("[streams] json locked")
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
var info struct {
|
||||
var info = struct {
|
||||
Producers []*Producer `json:"producers"`
|
||||
Consumers []core.Consumer `json:"consumers"`
|
||||
}{
|
||||
Producers: s.producers,
|
||||
Consumers: s.consumers,
|
||||
}
|
||||
info.Producers = s.producers
|
||||
info.Consumers = s.consumers
|
||||
|
||||
s.mu.Unlock()
|
||||
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -29,7 +26,8 @@ func Init() {
|
||||
streams[name] = NewStream(item)
|
||||
}
|
||||
|
||||
api.HandleFunc("api/streams", streamsHandler)
|
||||
api.HandleFunc("api/streams", apiStreams)
|
||||
api.HandleFunc("api/streams.dot", apiStreamsDOT)
|
||||
|
||||
if cfg.Publish == nil {
|
||||
return
|
||||
@@ -58,13 +56,18 @@ func Validate(source string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(name string, source string) *Stream {
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
func New(name string, sources ...string) *Stream {
|
||||
for _, source := range sources {
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
stream := NewStream(source)
|
||||
stream := NewStream(sources)
|
||||
|
||||
streamsMu.Lock()
|
||||
streams[name] = stream
|
||||
streamsMu.Unlock()
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -97,6 +100,10 @@ func Patch(name string, source string) *Stream {
|
||||
return nil
|
||||
}
|
||||
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check an existing stream with this name
|
||||
if stream, ok := streams[name]; ok {
|
||||
stream.SetSource(source)
|
||||
@@ -104,7 +111,9 @@ func Patch(name string, source string) *Stream {
|
||||
}
|
||||
|
||||
// create new stream with this name
|
||||
return New(name, source)
|
||||
stream := NewStream(source)
|
||||
streams[name] = stream
|
||||
return stream
|
||||
}
|
||||
|
||||
func GetOrPatch(query url.Values) *Stream {
|
||||
@@ -145,101 +154,6 @@ func Delete(id string) {
|
||||
delete(streams, id)
|
||||
}
|
||||
|
||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
// without source - return all streams list
|
||||
if src == "" && r.Method != "POST" {
|
||||
api.ResponseJSON(w, streams)
|
||||
return
|
||||
}
|
||||
|
||||
// Not sure about all this API. Should be rewrited...
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
stream := Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
api.ResponsePrettyJSON(w, stream)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, streams[src])
|
||||
}
|
||||
|
||||
case "PUT":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
name = src
|
||||
}
|
||||
|
||||
if New(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig(name, src, "streams"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "PATCH":
|
||||
name := query.Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
|
||||
if Patch(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "POST":
|
||||
// with dst - redirect source to dst
|
||||
if dst := query.Get("dst"); dst != "" {
|
||||
if stream := Get(dst); stream != nil {
|
||||
if err := Validate(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if err = stream.Play(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
api.ResponseJSON(w, stream)
|
||||
}
|
||||
} else if stream = Get(src); stream != nil {
|
||||
if err := Validate(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if err = stream.Publish(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
delete(streams, src)
|
||||
|
||||
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
var streams = map[string]*Stream{}
|
||||
var streamsMu sync.Mutex
|
||||
|
@@ -8,11 +8,15 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
|
||||
return kasa.Dial(url)
|
||||
streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
|
||||
return kasa.Dial(source)
|
||||
})
|
||||
|
||||
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
|
||||
return tapo.Dial(url)
|
||||
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
|
||||
return tapo.Dial(source)
|
||||
})
|
||||
|
||||
streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
|
||||
return tapo.Dial(source)
|
||||
})
|
||||
}
|
||||
|
39
internal/v4l2/README.md
Normal file
39
internal/v4l2/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# V4L2
|
||||
|
||||
What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux):
|
||||
|
||||
- V4L2 (Video for Linux API version 2) works only in Linux
|
||||
- supports USB cameras and other similar devices
|
||||
- one device can only be connected to one software simultaneously
|
||||
- cameras support a fixed list of formats, resolutions and frame rates
|
||||
- basic cameras supports only RAW (non-compressed) pixel formats
|
||||
- regular cameras supports MJPEG format (series of JPEG frames)
|
||||
- advances cameras support H264 format (MSE/MP4, WebRTC compatible)
|
||||
- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage
|
||||
- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage
|
||||
- H265 (HEVC) format is also supported (if the camera supports it)
|
||||
|
||||
Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%.
|
||||
|
||||
Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**.
|
||||
|
||||
## RAW format
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10
|
||||
```
|
||||
|
||||
Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured.
|
||||
|
||||
```
|
||||
ffplay http://localhost:1984/api/stream.mjpeg?src=camera1
|
||||
```
|
||||
|
||||
**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth.
|
||||
|
||||
```
|
||||
ffplay http://localhost:1984/api/stream.y4m?src=camera1
|
||||
```
|
7
internal/v4l2/v4l2.go
Normal file
7
internal/v4l2/v4l2.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !linux
|
||||
|
||||
package v4l2
|
||||
|
||||
func Init() {
|
||||
// not supported
|
||||
}
|
89
internal/v4l2/v4l2_linux.go
Normal file
89
internal/v4l2/v4l2_linux.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package v4l2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/pkg/v4l2/device"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("v4l2", func(source string) (core.Producer, error) {
|
||||
return v4l2.Open(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/v4l2", apiV4L2)
|
||||
}
|
||||
|
||||
func apiV4L2(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []*api.Source
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
continue
|
||||
}
|
||||
|
||||
path := "/dev/" + file.Name()
|
||||
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
formats, _ := dev.ListFormats()
|
||||
for _, fourCC := range formats {
|
||||
name, ffmpeg := findFormat(fourCC)
|
||||
source := &api.Source{Name: name}
|
||||
|
||||
sizes, _ := dev.ListSizes(fourCC)
|
||||
for _, wh := range sizes {
|
||||
if source.Info != "" {
|
||||
source.Info += " "
|
||||
}
|
||||
|
||||
source.Info += fmt.Sprintf("%dx%d", wh[0], wh[1])
|
||||
|
||||
frameRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1])
|
||||
for _, fr := range frameRates {
|
||||
source.Info += fmt.Sprintf("@%d", fr)
|
||||
|
||||
if source.URL == "" && ffmpeg != "" {
|
||||
source.URL = fmt.Sprintf(
|
||||
"v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d",
|
||||
path, ffmpeg, wh[0], wh[1], fr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if source.Info != "" {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
}
|
||||
|
||||
func findFormat(fourCC uint32) (name, ffmpeg string) {
|
||||
for _, format := range device.Formats {
|
||||
if format.FourCC == fourCC {
|
||||
return format.Name, format.FFmpeg
|
||||
}
|
||||
}
|
||||
return string(binary.LittleEndian.AppendUint32(nil, fourCC)), ""
|
||||
}
|
@@ -2,10 +2,10 @@ package webrtc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
)
|
||||
@@ -75,14 +75,14 @@ func FilterCandidate(candidate *pion.ICECandidate) bool {
|
||||
|
||||
// host candidate should be in the hosts list
|
||||
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
||||
if !slices.Contains(filters.Candidates, candidate.Address) {
|
||||
if !core.Contains(filters.Candidates, candidate.Address) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if filters.Networks != nil {
|
||||
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
|
||||
if !slices.Contains(filters.Networks, networkType) {
|
||||
if !core.Contains(filters.Networks, networkType) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
||||
// https://aws.amazon.com/kinesis/video-streams/
|
||||
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
||||
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
||||
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
|
||||
return kinesisClient(rawURL, query, "webrtc/kinesis")
|
||||
} else if format == "openipc" {
|
||||
return openIPCClient(rawURL, query)
|
||||
} else {
|
||||
@@ -77,17 +77,23 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
// 2. Create PeerConnection
|
||||
pc, err := PeerConnection(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// waiter will wait PC error or WS error or nil (connection OK)
|
||||
var connState core.Waiter
|
||||
var connMu sync.Mutex
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WebSocket async"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "ws"
|
||||
prod.URL = url
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
@@ -132,7 +138,8 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
if msg.Type != "webrtc/answer" {
|
||||
return nil, errors.New("wrong answer: " + msg.Type)
|
||||
err = errors.New("wrong answer: " + msg.String())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answer := msg.String()
|
||||
@@ -180,8 +187,9 @@ func whepClient(url string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WHEP sync"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.URL = url
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
|
@@ -34,7 +34,7 @@ func (k kinesisResponse) String() string {
|
||||
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
||||
}
|
||||
|
||||
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
|
||||
func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) {
|
||||
// 1. Connect to signalign server
|
||||
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||
if err != nil {
|
||||
@@ -79,8 +79,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = desc
|
||||
prod.FormatName = format
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "ws"
|
||||
prod.URL = rawURL
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
@@ -216,5 +218,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
|
||||
"ice_servers": []string{string(kvs.Servers)},
|
||||
}
|
||||
|
||||
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
|
||||
return kinesisClient(kvs.URL, query, "webrtc/wyze")
|
||||
}
|
||||
|
@@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/Milestone"
|
||||
prod.FormatName = "webrtc/milestone"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.URL = rawURL
|
||||
|
||||
offer, err := mc.GetOffer()
|
||||
if err != nil {
|
||||
|
@@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
var connState core.Waiter
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/OpenIPC"
|
||||
prod.FormatName = "webrtc/openipc"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "ws"
|
||||
prod.URL = rawURL
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
|
@@ -65,6 +65,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("src")
|
||||
stream := streams.Get(url)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,11 +101,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
desc = "WebRTC/JSON sync"
|
||||
desc = "webrtc/json"
|
||||
case MimeSDP:
|
||||
desc = "WebRTC/WHEP sync"
|
||||
desc = "webrtc/whep"
|
||||
default:
|
||||
desc = "WebRTC/HTTP sync"
|
||||
desc = "webrtc/post"
|
||||
}
|
||||
|
||||
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
||||
@@ -168,8 +169,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// create new webrtc instance
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WHIP sync"
|
||||
prod.Mode = core.ModePassiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.UserAgent = r.UserAgent()
|
||||
|
||||
if err = prod.SetOffer(string(offer)); err != nil {
|
||||
|
@@ -40,15 +40,17 @@ func Init() {
|
||||
AddCandidate(network, candidate)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// create pionAPI with custom codecs list and custom network settings
|
||||
serverAPI, err := webrtc.NewServerAPI(network, address, &filters)
|
||||
serverAPI, err = webrtc.NewServerAPI(network, address, &filters)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
// use same API for WebRTC server and client if no address
|
||||
clientAPI := serverAPI
|
||||
clientAPI = serverAPI
|
||||
|
||||
if address != "" {
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
||||
@@ -81,11 +83,13 @@ func Init() {
|
||||
streams.HandleFunc("webrtc", streamsHandler)
|
||||
}
|
||||
|
||||
var serverAPI, clientAPI *pion.API
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
||||
|
||||
func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {
|
||||
var stream *streams.Stream
|
||||
var mode core.Mode
|
||||
|
||||
@@ -104,8 +108,30 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
var offer struct {
|
||||
Type string `json:"type"`
|
||||
SDP string `json:"sdp"`
|
||||
ICEServers []pion.ICEServer `json:"ice_servers"`
|
||||
}
|
||||
|
||||
// V2 - json/object exchange, V1 - raw SDP exchange
|
||||
apiV2 := msg.Type == "webrtc"
|
||||
|
||||
if apiV2 {
|
||||
if err = msg.Unmarshal(&offer); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
offer.SDP = msg.String()
|
||||
}
|
||||
|
||||
// create new PeerConnection instance
|
||||
pc, err := PeerConnection(false)
|
||||
var pc *pion.PeerConnection
|
||||
if offer.ICEServers == nil {
|
||||
pc, err = PeerConnection(false)
|
||||
} else {
|
||||
pc, err = serverAPI.NewPeerConnection(pion.Configuration{ICEServers: offer.ICEServers})
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
@@ -117,8 +143,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
defer sendAnswer.Done(nil)
|
||||
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = "WebRTC/WebSocket async"
|
||||
conn.Mode = mode
|
||||
conn.Protocol = "ws"
|
||||
conn.UserAgent = tr.Request.UserAgent()
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
@@ -145,20 +171,10 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
})
|
||||
|
||||
// V2 - json/object exchange, V1 - raw SDP exchange
|
||||
apiV2 := msg.Type == "webrtc"
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer.SDP)
|
||||
|
||||
// 1. SetOffer, so we can get remote client codecs
|
||||
var offer string
|
||||
if apiV2 {
|
||||
offer = msg.GetString("sdp")
|
||||
} else {
|
||||
offer = msg.String()
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
if err = conn.SetOffer(offer.SDP); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
@@ -207,8 +223,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
||||
|
||||
// create new webrtc instance
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = desc
|
||||
conn.FormatName = desc
|
||||
conn.UserAgent = userAgent
|
||||
conn.Protocol = "http"
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
|
38
internal/webrtc/webrtc_test.go
Normal file
38
internal/webrtc/webrtc_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebRTCAPIv1(t *testing.T) {
|
||||
raw := `{"type":"webrtc/offer","value":"v=0\n..."}`
|
||||
msg := new(ws.Message)
|
||||
err := json.Unmarshal([]byte(raw), msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "v=0\n...", msg.String())
|
||||
}
|
||||
|
||||
func TestWebRTCAPIv2(t *testing.T) {
|
||||
raw := `{"type":"webrtc","value":{"type":"offer","sdp":"v=0\n...","ice_servers":[{"urls":["stun:stun.l.google.com:19302"]}]}}`
|
||||
msg := new(ws.Message)
|
||||
err := json.Unmarshal([]byte(raw), msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
var offer struct {
|
||||
Type string `json:"type"`
|
||||
SDP string `json:"sdp"`
|
||||
ICEServers []pion.ICEServer `json:"ice_servers"`
|
||||
}
|
||||
err = msg.Unmarshal(&offer)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "offer", offer.Type)
|
||||
require.Equal(t, "v=0\n...", offer.SDP)
|
||||
require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0])
|
||||
}
|
@@ -47,7 +47,7 @@ func Init() {
|
||||
if stream == nil {
|
||||
return "", errors.New(api.StreamNotFound)
|
||||
}
|
||||
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
|
||||
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
|
||||
},
|
||||
}
|
||||
|
||||
|
8
main.go
8
main.go
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/bubble"
|
||||
"github.com/AlexxIT/go2rtc/internal/debug"
|
||||
"github.com/AlexxIT/go2rtc/internal/doorbird"
|
||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
@@ -24,18 +25,22 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/nest"
|
||||
"github.com/AlexxIT/go2rtc/internal/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/internal/onvif"
|
||||
"github.com/AlexxIT/go2rtc/internal/ring"
|
||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/tapo"
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/webtorrent"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.9.8"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
|
||||
app.Init() // init config and logs
|
||||
@@ -76,10 +81,13 @@ func main() {
|
||||
mpegts.Init() // mpegts passive source
|
||||
roborock.Init() // roborock source
|
||||
homekit.Init() // homekit source
|
||||
ring.Init() // ring source
|
||||
nest.Init() // nest source
|
||||
bubble.Init() // bubble source
|
||||
expr.Init() // expr source
|
||||
gopro.Init() // gopro source
|
||||
doorbird.Init() // doorbird source
|
||||
v4l2.Init() // v4l2 source
|
||||
|
||||
// 6. Helper modules
|
||||
|
||||
|
@@ -1,3 +1,85 @@
|
||||
# Notes
|
||||
|
||||
go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.
|
||||
Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.
|
||||
|
||||
## Producers (input)
|
||||
|
||||
- The initiator of the connection can be go2rtc - **Source protocols**
|
||||
- The initiator of the connection can be an external program - **Ingress protocols**
|
||||
- Codecs can be incoming - **Recevers codecs**
|
||||
- Codecs can be outgoing (two way audio) - **Senders codecs**
|
||||
|
||||
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|
||||
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
|
||||
| adts | http,tcp,pipe | http | aac | | `http:` |
|
||||
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
|
||||
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
|
||||
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
|
||||
| gopro | http+udp | | TODO | | `gopro:` |
|
||||
| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` |
|
||||
| hls/mpegts | http | | h264,h265,aac,opus | | `http:` |
|
||||
| homekit | homekit+udp | | h264,eld* | | `homekit:` |
|
||||
| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` |
|
||||
| ivideon | ws | | h264 | | `ivideon:` |
|
||||
| kasa | http | | h264,pcm_mulaw | | `kasa:` |
|
||||
| h264 | http,tcp,pipe | http | h264 | | `http:` |
|
||||
| hevc | http,tcp,pipe | http | hevc | | `http:` |
|
||||
| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||
| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||
| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` |
|
||||
| nest/webrtc | http+udp | | TODO | | `nest:` |
|
||||
| roborock | mqtt+udp | | h264,opus | opus | `roborock:` |
|
||||
| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` |
|
||||
| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` |
|
||||
| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` |
|
||||
| tapo | http | | h264,pcma | pcm_alaw | `tapo:` |
|
||||
| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` |
|
||||
| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` |
|
||||
| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` |
|
||||
| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` |
|
||||
|
||||
- **eld** - rare variant of aac codec
|
||||
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||
- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep
|
||||
|
||||
## Consumers (output)
|
||||
|
||||
| Format | Protocol | Send codecs | Recv codecs | Example |
|
||||
|--------------|-------------|------------------------------|-------------------------|---------------------------------------|
|
||||
| adts | http | aac | | `GET /api/stream.adts` |
|
||||
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
|
||||
| flv | http | h264,aac | | `GET /api/stream.flv` |
|
||||
| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` |
|
||||
| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` |
|
||||
| homekit | homekit+udp | h264,opus | | Apple HomeKit app |
|
||||
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
|
||||
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
|
||||
| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` |
|
||||
| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` |
|
||||
| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` |
|
||||
| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` |
|
||||
| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` |
|
||||
| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` |
|
||||
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
|
||||
|
||||
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||
|
||||
## Snapshots
|
||||
|
||||
| Format | Protocol | Send codecs | Example |
|
||||
|--------|----------|-------------|-----------------------|
|
||||
| jpeg | http | mjpeg | `GET /api/frame.jpeg` |
|
||||
| mp4 | http | h264,hevc | `GET /api/frame.mp4` |
|
||||
|
||||
## Developers
|
||||
|
||||
File naming:
|
||||
|
||||
- `pkg/{format}/producer.go` - producer for this format (also if support backchannel)
|
||||
- `pkg/{format}/consumer.go` - consumer for this format
|
||||
- `pkg/{format}/backchanel.go` - producer with only backchannel func
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://www.wowza.com/blog/streaming-protocols
|
||||
|
@@ -9,7 +9,6 @@ import (
|
||||
)
|
||||
|
||||
func IsADTS(b []byte) bool {
|
||||
_ = b[1]
|
||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||
}
|
||||
|
||||
|
@@ -8,15 +8,12 @@ import (
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
core.Connection
|
||||
wr *core.WriteBuffer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
cons := &Consumer{
|
||||
wr: core.NewWriteBuffer(nil),
|
||||
}
|
||||
cons.Medias = []*core.Media{
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
@@ -25,7 +22,16 @@ func NewConsumer() *Consumer {
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons
|
||||
wr := core.NewWriteBuffer(nil)
|
||||
return &Consumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "adts",
|
||||
Medias: medias,
|
||||
Transport: wr,
|
||||
},
|
||||
wr: wr,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
@@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.wr.Close()
|
||||
}
|
||||
|
@@ -10,9 +10,8 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
rd *bufio.Reader
|
||||
cl io.Closer
|
||||
}
|
||||
|
||||
func Open(r io.Reader) (*Producer, error) {
|
||||
@@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codec := ADTSToCodec(b)
|
||||
|
||||
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
||||
prod.Type = "ADTS producer"
|
||||
prod.Medias = []*core.Media{
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
Codecs: []*core.Codec{ADTSToCodec(b)},
|
||||
},
|
||||
}
|
||||
return prod, nil
|
||||
return &Producer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "adts",
|
||||
Medias: medias,
|
||||
Transport: r,
|
||||
},
|
||||
rd: rd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Producer) Start() error {
|
||||
@@ -66,8 +69,3 @@ func (c *Producer) Start() error {
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
_ = c.SuperProducer.Close()
|
||||
return c.cl.Close()
|
||||
}
|
||||
|
@@ -22,6 +22,15 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||
|
||||
if len(packet.Payload) < int(2+headersSize) {
|
||||
// In very rare cases noname cameras may send data not according to the standard
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1328
|
||||
if IsADTS(packet.Payload) {
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAAC
|
||||
clone.Timestamp = timestamp
|
||||
clone.Payload = clone.Payload[ADTSHeaderSize:]
|
||||
handler(&clone)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
33
pkg/aac/rtp_test.go
Normal file
33
pkg/aac/rtp_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package aac
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuggy_RTSP_AAC(t *testing.T) {
|
||||
// https: //github.com/AlexxIT/go2rtc/issues/1328
|
||||
payload, _ := hex.DecodeString("fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0")
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: 36944,
|
||||
Timestamp: 4217191328,
|
||||
SSRC: 12892774,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
var size int
|
||||
|
||||
RTPDepay(func(packet *core.Packet) {
|
||||
size = len(packet.Payload)
|
||||
})(packet)
|
||||
|
||||
require.Equal(t, len(payload), size+ADTSHeaderSize)
|
||||
}
|
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
// Deprecated: should be rewritten to core.Connection
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
@@ -43,8 +44,12 @@ type Client struct {
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{url: url}
|
||||
func Dial(rawURL string) (*Client, error) {
|
||||
client := &Client{url: rawURL}
|
||||
if err := client.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -226,7 +231,7 @@ func (c *Client) Handle() error {
|
||||
Header: rtp.Header{
|
||||
Timestamp: core.Now90000(),
|
||||
},
|
||||
Payload: annexb.EncodeToAVCC(b[6:], false),
|
||||
Payload: annexb.EncodeToAVCC(b[6:]),
|
||||
}
|
||||
c.videoTrack.WriteRTP(pkt)
|
||||
} else {
|
||||
|
@@ -65,11 +65,16 @@ func (c *Client) Stop() error {
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "Bubble active producer",
|
||||
Medias: c.medias,
|
||||
Recv: c.recv,
|
||||
Receivers: c.receivers,
|
||||
info := &core.Connection{
|
||||
ID: core.ID(c),
|
||||
FormatName: "bubble",
|
||||
Protocol: "http",
|
||||
Medias: c.medias,
|
||||
Recv: c.recv,
|
||||
Receivers: c.receivers,
|
||||
}
|
||||
if c.conn != nil {
|
||||
info.RemoteAddr = c.conn.RemoteAddr().String()
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
@@ -2,8 +2,8 @@ package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -18,34 +18,76 @@ type Codec struct {
|
||||
PayloadType uint8
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
|
||||
// MarshalJSON - return FFprobe compatible output
|
||||
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 {
|
||||
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
|
||||
info["sample_rate"] = c.ClockRate
|
||||
}
|
||||
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 {
|
||||
switch c.Name {
|
||||
func FFmpegCodecName(name string) string {
|
||||
switch name {
|
||||
case CodecH264:
|
||||
if profile := DecodeH264(c.FmtpLine); profile != "" {
|
||||
return "H.264 " + profile
|
||||
}
|
||||
return c.Name
|
||||
return "h264"
|
||||
case CodecH265:
|
||||
return "hevc"
|
||||
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 {
|
||||
s += "/" + strconv.Itoa(int(c.ClockRate))
|
||||
s += fmt.Sprintf("/%d", c.ClockRate)
|
||||
}
|
||||
if c.Channels > 0 {
|
||||
s += "/" + strconv.Itoa(int(c.Channels))
|
||||
s += fmt.Sprintf("/%d", c.Channels)
|
||||
}
|
||||
return s
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Codec) IsRTP() bool {
|
||||
@@ -115,7 +157,12 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Name == "" {
|
||||
switch c.Name {
|
||||
case "PCM":
|
||||
// https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/
|
||||
// check pkg/rtsp/rtsp_test.go TestHikvisionPCM
|
||||
c.Name = CodecPCML
|
||||
case "":
|
||||
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||
switch payloadType {
|
||||
case "0":
|
||||
@@ -181,10 +228,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
return c
|
||||
}
|
||||
|
||||
func DecodeH264(fmtp string) string {
|
||||
func DecodeH264(fmtp string) (profile string, level byte) {
|
||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||
var profile string
|
||||
switch sps[1] {
|
||||
case 0x42:
|
||||
profile = "Baseline"
|
||||
@@ -198,8 +244,8 @@ func DecodeH264(fmtp string) string {
|
||||
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
|
||||
}
|
||||
|
144
pkg/core/connection.go
Normal file
144
pkg/core/connection.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func NewID() uint32 {
|
||||
return id.Add(1)
|
||||
}
|
||||
|
||||
// Deprecated: use NewID instead
|
||||
func ID(v any) uint32 {
|
||||
p := uintptr(reflect.ValueOf(v).UnsafePointer())
|
||||
return 0x8000_0000 | uint32(p)
|
||||
}
|
||||
|
||||
var id atomic.Uint32
|
||||
|
||||
type Info interface {
|
||||
SetProtocol(string)
|
||||
SetRemoteAddr(string)
|
||||
SetSource(string)
|
||||
SetURL(string)
|
||||
WithRequest(*http.Request)
|
||||
GetSource() string
|
||||
}
|
||||
|
||||
// Connection just like webrtc.PeerConnection
|
||||
// - ID and RemoteAddr used for building Connection(s) graph
|
||||
// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection
|
||||
// - FormatName and Protocol has FFmpeg compatible names
|
||||
// - Transport used for auto closing on Stop
|
||||
type Connection struct {
|
||||
ID uint32 `json:"id,omitempty"`
|
||||
FormatName string `json:"format_name,omitempty"` // rtsp, webrtc, mp4, mjpeg, mpjpeg...
|
||||
Protocol string `json:"protocol,omitempty"` // tcp, udp, http, ws, pipe...
|
||||
RemoteAddr string `json:"remote_addr,omitempty"` // host:port other info
|
||||
Source string `json:"source,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Recv int `json:"bytes_recv,omitempty"`
|
||||
Send int `json:"bytes_send,omitempty"`
|
||||
|
||||
Transport any `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Connection) GetMedias() []*Media {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
||||
for _, receiver := range c.Receivers {
|
||||
if receiver.Codec == codec {
|
||||
return receiver, nil
|
||||
}
|
||||
}
|
||||
receiver := NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, receiver)
|
||||
return receiver, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Stop() error {
|
||||
for _, receiver := range c.Receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
for _, sender := range c.Senders {
|
||||
sender.Close()
|
||||
}
|
||||
if closer, ok := c.Transport.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated:
|
||||
func (c *Connection) Codecs() []*Codec {
|
||||
codecs := make([]*Codec, len(c.Senders))
|
||||
for i, sender := range c.Senders {
|
||||
codecs[i] = sender.Codec
|
||||
}
|
||||
return codecs
|
||||
}
|
||||
|
||||
func (c *Connection) SetProtocol(s string) {
|
||||
c.Protocol = s
|
||||
}
|
||||
|
||||
func (c *Connection) SetRemoteAddr(s string) {
|
||||
if c.RemoteAddr == "" {
|
||||
c.RemoteAddr = s
|
||||
} else {
|
||||
c.RemoteAddr += " forwarded " + s
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connection) SetSource(s string) {
|
||||
c.Source = s
|
||||
}
|
||||
|
||||
func (c *Connection) SetURL(s string) {
|
||||
c.URL = s
|
||||
}
|
||||
|
||||
func (c *Connection) WithRequest(r *http.Request) {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
c.Protocol = "ws"
|
||||
} else {
|
||||
c.Protocol = "http"
|
||||
}
|
||||
|
||||
c.RemoteAddr = r.RemoteAddr
|
||||
if remote := r.Header.Get("X-Forwarded-For"); remote != "" {
|
||||
c.RemoteAddr += " forwarded " + remote
|
||||
}
|
||||
|
||||
c.UserAgent = r.UserAgent()
|
||||
}
|
||||
|
||||
func (c *Connection) GetSource() string {
|
||||
return c.Source
|
||||
}
|
||||
|
||||
// Create like os.Create, init Consumer with existing Transport
|
||||
func Create(w io.Writer) (*Connection, error) {
|
||||
return &Connection{Transport: w}, nil
|
||||
}
|
||||
|
||||
// Open like os.Open, init Producer from existing Transport
|
||||
func Open(r io.Reader) (*Connection, error) {
|
||||
return &Connection{Transport: r}, nil
|
||||
}
|
||||
|
||||
// Dial like net.Dial, init Producer via Dialing
|
||||
func Dial(rawURL string) (*Connection, error) {
|
||||
return &Connection{}, nil
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package core
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
DirectionRecvonly = "recvonly"
|
||||
DirectionSendonly = "sendonly"
|
||||
@@ -90,89 +92,6 @@ func (m Mode) String() string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Recv int `json:"recv,omitempty"`
|
||||
Send int `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
UnsupportedCodec = "unsupported codec"
|
||||
WrongMediaDirection = "wrong media direction"
|
||||
)
|
||||
|
||||
type SuperProducer struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Recv int `json:"recv,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SuperProducer) GetMedias() []*Media {
|
||||
return s.Medias
|
||||
}
|
||||
|
||||
func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
||||
for _, receiver := range s.Receivers {
|
||||
if receiver.Codec == codec {
|
||||
return receiver, nil
|
||||
}
|
||||
}
|
||||
receiver := NewReceiver(media, codec)
|
||||
s.Receivers = append(s.Receivers, receiver)
|
||||
return receiver, nil
|
||||
}
|
||||
|
||||
func (s *SuperProducer) Close() error {
|
||||
for _, receiver := range s.Receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SuperConsumer struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
SDP string `json:"sdp,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Send int `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SuperConsumer) GetMedias() []*Media {
|
||||
return s.Medias
|
||||
}
|
||||
|
||||
func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) {
|
||||
// return 0, nil
|
||||
//}
|
||||
|
||||
func (s *SuperConsumer) Close() error {
|
||||
for _, sender := range s.Senders {
|
||||
sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SuperConsumer) Codecs() []*Codec {
|
||||
codecs := make([]*Codec, len(s.Senders))
|
||||
for i, sender := range s.Senders {
|
||||
codecs[i] = sender.Codec
|
||||
}
|
||||
return codecs
|
||||
func (m Mode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.String())
|
||||
}
|
||||
|
120
pkg/core/core_test.go
Normal file
120
pkg/core/core_test.go
Normal file
@@ -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()
|
||||
}
|
@@ -22,7 +22,7 @@ type Media struct {
|
||||
func (m *Media) String() string {
|
||||
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
||||
for _, codec := range m.Codecs {
|
||||
name := codec.Text()
|
||||
name := codec.String()
|
||||
|
||||
if strings.Contains(s, name) {
|
||||
continue
|
||||
@@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool {
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
||||
return KindAudio
|
||||
@@ -124,9 +124,13 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
|
||||
codec := media.Codecs[0]
|
||||
|
||||
name := codec.Name
|
||||
if name == CodecELD {
|
||||
switch codec.Name {
|
||||
case CodecELD:
|
||||
name = CodecAAC
|
||||
case CodecPCML:
|
||||
name = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server
|
||||
default:
|
||||
name = codec.Name
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
@@ -137,6 +141,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
if media.Direction != "" {
|
||||
md.WithPropertyAttribute(media.Direction)
|
||||
}
|
||||
|
||||
if media.ID != "" {
|
||||
md.WithValueAttribute("control", media.ID)
|
||||
}
|
||||
|
88
pkg/core/node.go
Normal file
88
pkg/core/node.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
43
pkg/core/slices.go
Normal file
43
pkg/core/slices.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package core
|
||||
|
||||
// This code copied from go1.21 for backward support in go1.20.
|
||||
// We need to support go1.20 for Windows 7
|
||||
|
||||
// Index returns the index of the first occurrence of v in s,
|
||||
// or -1 if not present.
|
||||
func Index[S ~[]E, E comparable](s S, v E) int {
|
||||
for i := range s {
|
||||
if v == s[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Contains reports whether v is present in s.
|
||||
func Contains[S ~[]E, E comparable](s S, v E) bool {
|
||||
return Index(s, v) >= 0
|
||||
}
|
||||
|
||||
type Ordered interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
||||
~float32 | ~float64 |
|
||||
~string
|
||||
}
|
||||
|
||||
// Max returns the maximal value in x. It panics if x is empty.
|
||||
// For floating-point E, Max propagates NaNs (any NaN value in x
|
||||
// forces the output to be NaN).
|
||||
func Max[S ~[]E, E Ordered](x S) E {
|
||||
if len(x) < 1 {
|
||||
panic("slices.Max: empty list")
|
||||
}
|
||||
m := x[0]
|
||||
for i := 1; i < len(x); i++ {
|
||||
if x[i] > m {
|
||||
m = x[i]
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
@@ -3,223 +3,212 @@ package core
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"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")
|
||||
|
||||
type Receiver struct {
|
||||
Codec *Codec
|
||||
Media *Media
|
||||
Node
|
||||
|
||||
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
|
||||
mu sync.RWMutex
|
||||
bytes int
|
||||
Bytes int `json:"bytes,omitempty"`
|
||||
Packets int `json:"packets,omitempty"`
|
||||
}
|
||||
|
||||
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
||||
Assert(codec != nil)
|
||||
return &Receiver{Codec: codec, Media: media}
|
||||
}
|
||||
|
||||
// WriteRTP - fast and non blocking write to all readers buffers
|
||||
func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||
t.mu.Lock()
|
||||
t.bytes += len(packet.Payload)
|
||||
for sender, buffer := range t.senders {
|
||||
select {
|
||||
case buffer <- packet:
|
||||
default:
|
||||
sender.overflow++
|
||||
r := &Receiver{
|
||||
Node: Node{id: NewID(), Codec: codec},
|
||||
Media: media,
|
||||
}
|
||||
r.Input = func(packet *Packet) {
|
||||
r.Bytes += len(packet.Payload)
|
||||
r.Packets++
|
||||
for _, child := range r.childs {
|
||||
child.Input(packet)
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
func (t *Receiver) Senders() (senders []*Sender) {
|
||||
t.mu.RLock()
|
||||
for sender := range t.senders {
|
||||
senders = append(senders, sender)
|
||||
// Deprecated: should be removed
|
||||
func (r *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||
r.Input(packet)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
t.mu.Lock()
|
||||
// close all sender channel buffers and erase senders list
|
||||
for _, buffer := range t.senders {
|
||||
close(buffer)
|
||||
}
|
||||
t.senders = nil
|
||||
t.mu.Unlock()
|
||||
// Deprecated: should be removed
|
||||
func (r *Receiver) Replace(target *Receiver) {
|
||||
MoveNode(&target.Node, &r.Node)
|
||||
}
|
||||
|
||||
func (t *Receiver) Replace(target *Receiver) {
|
||||
// move this receiver senders to new receiver
|
||||
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())
|
||||
func (r *Receiver) Close() {
|
||||
r.Node.Close()
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
Codec *Codec
|
||||
Media *Media
|
||||
Node
|
||||
|
||||
Handler HandlerFunc
|
||||
// Deprecated:
|
||||
Media *Media `json:"-"`
|
||||
// Deprecated:
|
||||
Handler HandlerFunc `json:"-"`
|
||||
|
||||
receivers []*Receiver
|
||||
mu sync.RWMutex
|
||||
bytes int
|
||||
Bytes int `json:"bytes,omitempty"`
|
||||
Packets int `json:"packets,omitempty"`
|
||||
Drops int `json:"drops,omitempty"`
|
||||
|
||||
overflow int
|
||||
buf chan *Packet
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewSender(media *Media, codec *Codec) *Sender {
|
||||
return &Sender{Codec: codec, Media: media}
|
||||
}
|
||||
var bufSize uint16
|
||||
|
||||
// HandlerFunc like http.HandlerFunc
|
||||
type HandlerFunc func(packet *rtp.Packet)
|
||||
|
||||
func (s *Sender) HandleRTP(track *Receiver) {
|
||||
s.Bind(track)
|
||||
go s.worker(track)
|
||||
}
|
||||
|
||||
func (s *Sender) Bind(track *Receiver) {
|
||||
var bufferSize uint16
|
||||
|
||||
if GetKind(track.Codec.Name) == KindVideo {
|
||||
if track.Codec.IsRTP() {
|
||||
if GetKind(codec.Name) == KindVideo {
|
||||
if codec.IsRTP() {
|
||||
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
||||
// for the h264.RTPDepay => RTPPay queue
|
||||
bufferSize = 5000
|
||||
bufSize = 4096
|
||||
} else {
|
||||
bufferSize = 50
|
||||
bufSize = 64
|
||||
}
|
||||
} else {
|
||||
bufferSize = 100
|
||||
bufSize = 128
|
||||
}
|
||||
|
||||
buffer := make(chan *rtp.Packet, bufferSize)
|
||||
|
||||
track.mu.Lock()
|
||||
if track.senders == nil {
|
||||
track.senders = map[*Sender]chan *rtp.Packet{}
|
||||
buf := make(chan *Packet, bufSize)
|
||||
s := &Sender{
|
||||
Node: Node{id: NewID(), Codec: codec},
|
||||
Media: media,
|
||||
buf: buf,
|
||||
}
|
||||
track.senders[s] = buffer
|
||||
track.mu.Unlock()
|
||||
|
||||
s.mu.Lock()
|
||||
s.receivers = append(s.receivers, track)
|
||||
s.mu.Unlock()
|
||||
s.Input = func(packet *Packet) {
|
||||
// writing to nil chan - OK, writing to closed chan - panic
|
||||
s.mu.Lock()
|
||||
select {
|
||||
case s.buf <- packet:
|
||||
s.Bytes += len(packet.Payload)
|
||||
s.Packets++
|
||||
default:
|
||||
s.Drops++
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.Output = func(packet *Packet) {
|
||||
s.Handler(packet)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Sender) worker(track *Receiver) {
|
||||
track.mu.Lock()
|
||||
buffer := track.senders[s]
|
||||
track.mu.Unlock()
|
||||
// Deprecated: should be removed
|
||||
func (s *Sender) HandleRTP(parent *Receiver) {
|
||||
s.WithParent(parent)
|
||||
s.Start()
|
||||
}
|
||||
|
||||
// read packets from buffer channel until it will be closed
|
||||
if buffer != nil {
|
||||
for packet := range buffer {
|
||||
s.bytes += len(packet.Payload)
|
||||
s.Handler(packet)
|
||||
}
|
||||
}
|
||||
// Deprecated: should be removed
|
||||
func (s *Sender) Bind(parent *Receiver) {
|
||||
s.WithParent(parent)
|
||||
}
|
||||
|
||||
// remove current receiver from list
|
||||
// it can only happen when receiver close buffer channel
|
||||
s.mu.Lock()
|
||||
for i, receiver := range s.receivers {
|
||||
if receiver == track {
|
||||
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
func (s *Sender) WithParent(parent *Receiver) *Sender {
|
||||
s.Node.WithParent(&parent.Node)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Sender) Start() {
|
||||
s.mu.Lock()
|
||||
for _, track := range s.receivers {
|
||||
go s.worker(track)
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.buf == nil || s.done != nil {
|
||||
return
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.done = make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for packet := range s.buf {
|
||||
s.Output(packet)
|
||||
}
|
||||
close(s.done)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Sender) Wait() {
|
||||
if done := s.done; s.done != nil {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sender) State() string {
|
||||
if s.buf == nil {
|
||||
return "closed"
|
||||
}
|
||||
if s.done == nil {
|
||||
return "new"
|
||||
}
|
||||
return "connected"
|
||||
}
|
||||
|
||||
func (s *Sender) Close() {
|
||||
s.mu.Lock()
|
||||
// remove this sender from all receivers list
|
||||
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()
|
||||
// close buffer if exists
|
||||
if buf := s.buf; buf != nil {
|
||||
s.buf = nil
|
||||
defer close(buf)
|
||||
}
|
||||
s.receivers = nil
|
||||
s.mu.Unlock()
|
||||
|
||||
s.Node.Close()
|
||||
}
|
||||
|
||||
func (s *Sender) String() string {
|
||||
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
|
||||
s.mu.RLock()
|
||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
||||
s.mu.RUnlock()
|
||||
if s.overflow > 0 {
|
||||
info += ", overflow=" + strconv.Itoa(s.overflow)
|
||||
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,
|
||||
}
|
||||
return info
|
||||
for _, child := range r.childs {
|
||||
v.Childs = append(v.Childs, child.id)
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (s *Sender) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// VA - helper, for extract video and audio receivers from list
|
||||
func VA(receivers []*Receiver) (video, audio *Receiver) {
|
||||
for _, receiver := range receivers {
|
||||
switch GetKind(receiver.Codec.Name) {
|
||||
case KindVideo:
|
||||
video = receiver
|
||||
case KindAudio:
|
||||
audio = receiver
|
||||
}
|
||||
v := struct {
|
||||
ID uint32 `json:"id"`
|
||||
Codec *Codec `json:"codec"`
|
||||
Parent uint32 `json:"parent,omitempty"`
|
||||
Bytes int `json:"bytes,omitempty"`
|
||||
Packets int `json:"packets,omitempty"`
|
||||
Drops int `json:"drops,omitempty"`
|
||||
}{
|
||||
ID: s.Node.id,
|
||||
Codec: s.Node.Codec,
|
||||
Bytes: s.Bytes,
|
||||
Packets: s.Packets,
|
||||
Drops: s.Drops,
|
||||
}
|
||||
return
|
||||
if s.parent != nil {
|
||||
v.Parent = s.parent.id
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
93
pkg/doorbird/backchannel.go
Normal file
93
pkg/doorbird/backchannel.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package doorbird
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Connection
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func Dial(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
|
||||
if u.Port() == "" {
|
||||
u.Host += ":80"
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("POST /bha-api/audio-transmit.cgi?http-user=%s&http-password=%s HTTP/1.0\r\n", user, pass) +
|
||||
"Content-Type: audio/basic\r\n" +
|
||||
"Content-Length: 9999999\r\n" +
|
||||
"Connection: Keep-Alive\r\n" +
|
||||
"Cache-Control: no-cache\r\n" +
|
||||
"\r\n"
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))
|
||||
if _, err = conn.Write([]byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &Client{
|
||||
core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "doorbird",
|
||||
Protocol: "http",
|
||||
URL: rawURL,
|
||||
Medias: medias,
|
||||
Transport: conn,
|
||||
},
|
||||
conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))
|
||||
if n, err := c.conn.Write(pkt.Payload); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() (err error) {
|
||||
_, err = c.conn.Read(nil)
|
||||
return
|
||||
}
|
@@ -8,16 +8,16 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
type Backchannel struct {
|
||||
core.Connection
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() error {
|
||||
func (c *Backchannel) Start() error {
|
||||
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -30,12 +30,7 @@ func (c *Consumer) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if err := c.client.Talk(); err != nil {
|
||||
return err
|
||||
}
|
@@ -8,17 +8,22 @@ func Dial(url string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "dvrip",
|
||||
Protocol: "tcp",
|
||||
RemoteAddr: client.conn.RemoteAddr().String(),
|
||||
Transport: client.conn,
|
||||
}
|
||||
|
||||
if client.stream != "" {
|
||||
prod := &Producer{client: client}
|
||||
prod.Type = "DVRIP active producer"
|
||||
prod := &Producer{Connection: conn, client: client}
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
} else {
|
||||
cons := &Consumer{client: client}
|
||||
cons.Type = "DVRIP active consumer"
|
||||
cons.Medias = []*core.Media{
|
||||
conn.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
@@ -29,6 +34,6 @@ func Dial(url string) (core.Producer, error) {
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons, nil
|
||||
return &Backchannel{Connection: conn, client: client}, nil
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
|
||||
client *Client
|
||||
|
||||
@@ -53,7 +53,7 @@ func (c *Producer) Start() error {
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(payload, false),
|
||||
Payload: annexb.EncodeToAVCC(payload),
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
@@ -92,10 +92,6 @@ func (c *Producer) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.client.Play(); err != nil {
|
||||
return err
|
||||
@@ -150,7 +146,7 @@ func (c *Producer) probe() error {
|
||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||
c.videoDT = 90000 / uint32(fps)
|
||||
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
payload := annexb.EncodeToAVCC(b[16:])
|
||||
c.addVideoTrack(b[4], payload)
|
||||
|
||||
case 0xFA: // audio
|
||||
|
@@ -10,17 +10,13 @@ import (
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
core.Connection
|
||||
wr *core.WriteBuffer
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
c := &Consumer{
|
||||
wr: core.NewWriteBuffer(nil),
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
c.Medias = []*core.Media{
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
@@ -36,7 +32,17 @@ func NewConsumer() *Consumer {
|
||||
},
|
||||
},
|
||||
}
|
||||
return c
|
||||
wr := core.NewWriteBuffer(nil)
|
||||
return &Consumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "flv",
|
||||
Medias: medias,
|
||||
Transport: wr,
|
||||
},
|
||||
wr: wr,
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
@@ -86,8 +92,3 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
}
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.wr.Close()
|
||||
}
|
||||
|
@@ -15,18 +15,24 @@ import (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
core.Connection
|
||||
rd *core.ReadBuffer
|
||||
|
||||
video, audio *core.Receiver
|
||||
}
|
||||
|
||||
func Open(rd io.Reader) (*Producer, error) {
|
||||
prod := &Producer{rd: core.NewReadBuffer(rd)}
|
||||
prod := &Producer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "flv",
|
||||
Transport: rd,
|
||||
},
|
||||
rd: core.NewReadBuffer(rd),
|
||||
}
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prod.Type = "FLV producer"
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
@@ -57,7 +63,7 @@ const (
|
||||
)
|
||||
|
||||
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
receiver, _ := c.SuperProducer.GetTrack(media, codec)
|
||||
receiver, _ := c.Connection.GetTrack(media, codec)
|
||||
if media.Kind == core.KindVideo {
|
||||
c.video = receiver
|
||||
} else {
|
||||
@@ -117,11 +123,6 @@ func (c *Producer) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
_ = c.SuperProducer.Close()
|
||||
return c.rd.Close()
|
||||
}
|
||||
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.readHeader(); err != nil {
|
||||
return err
|
||||
@@ -139,23 +140,29 @@ func (c *Producer) probe() error {
|
||||
// 1. Empty video/audio flag
|
||||
// 2. MedaData without stereo key for AAC
|
||||
// 3. Audio header after Video keyframe tag
|
||||
waitType := []byte{TagData}
|
||||
timeout := time.Now().Add(core.ProbeTimeout)
|
||||
|
||||
for len(waitType) != 0 && time.Now().Before(timeout) {
|
||||
// OpenIPC camera (on old firmwares) sends:
|
||||
// 1. Empty video/audio flag
|
||||
// 2. No MetaData packet
|
||||
// 3. Sends a video packet in more than 3 seconds
|
||||
waitVideo := true
|
||||
waitAudio := true
|
||||
timeout := time.Now().Add(time.Second * 5)
|
||||
|
||||
for (waitVideo || waitAudio) && time.Now().Before(timeout) {
|
||||
pkt, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {
|
||||
continue
|
||||
} else {
|
||||
waitType = append(waitType[:i], waitType[i+1:]...)
|
||||
}
|
||||
//log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload)
|
||||
|
||||
switch pkt.PayloadType {
|
||||
case TagAudio:
|
||||
if !waitAudio {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = pkt.Payload[1] // bounds
|
||||
|
||||
codecID := pkt.Payload[0] >> 4 // SoundFormat
|
||||
@@ -178,8 +185,13 @@ func (c *Producer) probe() error {
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
waitAudio = false
|
||||
|
||||
case TagVideo:
|
||||
if !waitVideo {
|
||||
continue
|
||||
}
|
||||
|
||||
var codec *core.Codec
|
||||
|
||||
if isExHeader(pkt.Payload) {
|
||||
@@ -212,19 +224,20 @@ func (c *Producer) probe() error {
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
waitVideo = false
|
||||
|
||||
case TagData:
|
||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||
waitType = append(waitType, TagData)
|
||||
continue
|
||||
}
|
||||
// Dahua cameras doesn't send videocodecid
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("width")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitType = append(waitType, TagVideo)
|
||||
if !bytes.Contains(pkt.Payload, []byte("videocodecid")) &&
|
||||
!bytes.Contains(pkt.Payload, []byte("width")) &&
|
||||
!bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitVideo = false
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
waitType = append(waitType, TagAudio)
|
||||
if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
waitAudio = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,11 +8,10 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
)
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
func Dial(rawURL string) (*mpegts.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -32,7 +31,15 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mpegts.Open(r)
|
||||
prod, err := mpegts.Open(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod.FormatName = "gopro"
|
||||
prod.RemoteAddr = u.Host
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
type listener struct {
|
@@ -11,64 +11,60 @@ const startAUD = StartCode + "\x09\xF0"
|
||||
const startAUDstart = startAUD + StartCode
|
||||
|
||||
// EncodeToAVCC
|
||||
// will change original slice data!
|
||||
// safeAppend should be used if original slice has useful data after end (part of other slice)
|
||||
//
|
||||
// FFmpeg MPEG-TS: 00000001 AUD 00000001 SPS 00000001 PPS 000001 IFrame
|
||||
// FFmpeg H264: 00000001 SPS 00000001 PPS 000001 IFrame 00000001 PFrame
|
||||
func EncodeToAVCC(b []byte, safeAppend bool) []byte {
|
||||
const minSize = len(StartCode) + 1
|
||||
|
||||
// 1. Check frist "start code"
|
||||
if len(b) < len(startAUDstart) || string(b[:len(StartCode)]) != StartCode {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Skip Access unit delimiter (AUD) from FFmpeg
|
||||
if string(b[:len(startAUDstart)]) == startAUDstart {
|
||||
b = b[6:]
|
||||
}
|
||||
|
||||
// Reolink: 000001 AUD 000001 VPS 00000001 SPS 00000001 PPS 00000001 IDR 00000001 IDR
|
||||
func EncodeToAVCC(annexb []byte) (avc []byte) {
|
||||
var start int
|
||||
|
||||
for i, n := minSize, len(b)-minSize; i < n; {
|
||||
// 3. Check "start code" (first 2 bytes)
|
||||
if b[i] != 0 || b[i+1] != 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
avc = make([]byte, 0, len(annexb)+4) // init memory with little overhead
|
||||
|
||||
// 4. Check "start code" (3 bytes size or 4 bytes size)
|
||||
if b[i+2] == 1 {
|
||||
if safeAppend {
|
||||
// protect original slice from "damage"
|
||||
b = bytes.Clone(b)
|
||||
safeAppend = false
|
||||
for i := 0; ; i++ {
|
||||
var offset int
|
||||
|
||||
if i+3 < len(annexb) {
|
||||
// search next separator
|
||||
if annexb[i] == 0 && annexb[i+1] == 0 {
|
||||
if annexb[i+2] == 1 {
|
||||
offset = 3 // 00 00 01
|
||||
} else if annexb[i+2] == 0 && annexb[i+3] == 1 {
|
||||
offset = 4 // 00 00 00 01
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// convert start code from 3 bytes to 4 bytes
|
||||
b = append(b, 0)
|
||||
copy(b[i+1:], b[i:])
|
||||
n++
|
||||
} else if b[i+2] != 0 || b[i+3] != 1 {
|
||||
i++
|
||||
continue
|
||||
} else {
|
||||
i = len(annexb) // move i to data end
|
||||
}
|
||||
|
||||
// 5. Set size for previous AU
|
||||
size := uint32(i - start - len(StartCode))
|
||||
binary.BigEndian.PutUint32(b[start:], size)
|
||||
if start != 0 {
|
||||
size := uint32(i - start)
|
||||
avc = binary.BigEndian.AppendUint32(avc, size)
|
||||
avc = append(avc, annexb[start:i]...)
|
||||
}
|
||||
|
||||
start = i
|
||||
// sometimes FFmpeg put separator at the end
|
||||
if i += offset; i == len(annexb) {
|
||||
break
|
||||
}
|
||||
|
||||
i += minSize
|
||||
if isAUD(annexb[i]) {
|
||||
start = 0 // skip this NALU
|
||||
} else {
|
||||
start = i // save this position
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Set size for last AU
|
||||
size := uint32(len(b) - start - len(StartCode))
|
||||
binary.BigEndian.PutUint32(b[start:], size)
|
||||
return
|
||||
}
|
||||
|
||||
return b
|
||||
func isAUD(b byte) bool {
|
||||
const h264 = 9
|
||||
const h265 = 35 << 1
|
||||
return b&0b0001_1111 == h264 || b&0b0111_1110 == h265
|
||||
}
|
||||
|
||||
func DecodeAVCC(b []byte, safeClone bool) []byte {
|
||||
|
97
pkg/h264/annexb/annexb_test.go
Normal file
97
pkg/h264/annexb/annexb_test.go
Normal file
File diff suppressed because one or more lines are too long
@@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
payload, err := depack.Unmarshal(packet.Payload)
|
||||
if len(payload) == 0 || err != nil {
|
||||
@@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
|
||||
payload = payload[i:]
|
||||
continue
|
||||
case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass
|
||||
default:
|
||||
return // skip any unknown NAL unit type
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@@ -15,7 +15,8 @@ type Stream struct {
|
||||
}
|
||||
|
||||
func NewStream(
|
||||
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session,
|
||||
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec,
|
||||
videoSession, audioSession *srtp.Session, bitrate int,
|
||||
) (*Stream, error) {
|
||||
stream := &Stream{
|
||||
id: core.RandString(16, 0),
|
||||
@@ -30,11 +31,17 @@ func NewStream(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bitrate != 0 {
|
||||
bitrate /= 1024 // convert bps to kbps
|
||||
} else {
|
||||
bitrate = 4096 // default kbps for general FullHD camera
|
||||
}
|
||||
|
||||
videoCodec.RTPParams = []RTPParams{
|
||||
{
|
||||
PayloadType: 99,
|
||||
SSRC: videoSession.Local.SSRC,
|
||||
MaxBitrate: 299,
|
||||
MaxBitrate: uint16(bitrate), // iPhone query 299Kbps, iPad/AppleTV query 802Kbps
|
||||
RTCPInterval: 0.5,
|
||||
MaxMTU: []uint16{1378},
|
||||
},
|
||||
@@ -43,7 +50,7 @@ func NewStream(
|
||||
{
|
||||
PayloadType: 110,
|
||||
SSRC: audioSession.Local.SSRC,
|
||||
MaxBitrate: 24,
|
||||
MaxBitrate: 24, // any iDevice query 24Kbps (this is OK for 16KHz and 1 channel)
|
||||
RTCPInterval: 5,
|
||||
|
||||
ComfortNoisePayloadType: []uint8{13},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user