Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbe9e4aade | |||
| 715be4dad0 | |||
| 570b7d0d97 | |||
| 80ac0ab17f | |||
| 9ee8174d5f | |||
| 831aa03c9f | |||
| d372597bdb | |||
| 172437b6fc | |||
| 7640a42bfc | |||
| fde04bd625 | |||
| ad14a5ccba | |||
| 2348d12e9d | |||
| 5cafc05e13 | |||
| e982257271 | |||
| 340fd81778 | |||
| 2c34a17d88 | |||
| 6b005a666e | |||
| 1d1bcb0a63 | |||
| 3f5f1328e7 | |||
| 8cca8decde | |||
| be5bbd3b9b | |||
| 3f94a754e4 | |||
| 780f378fb1 | |||
| b874c17bcb | |||
| 16e4831499 | |||
| 9d709f0db8 | |||
| a8d394efd7 | |||
| 95a5283c86 | |||
| ef7d898747 | |||
| 388c408080 | |||
| 7b77e41253 | |||
| c0bfebf3a4 | |||
| 6f9f1c3a35 | |||
| 8128edad43 | |||
| eb8a13d8c2 | |||
| 8399edce6a | |||
| 2311d5eabe | |||
| afc8f4fdf6 | |||
| 66de2f91b6 | |||
| bd88695e59 | |||
| d559ec0208 | |||
| ed99025bd6 | |||
| 57d48f53e0 | |||
| 68fa42249e | |||
| c5bc761a52 | |||
| 3762bdbccd | |||
| eaae7aee39 | |||
| a4885c2c3a | |||
| f5aaee006e | |||
| db6745e8ff | |||
| ba34855602 | |||
| e6fa97c738 | |||
| 5b481a27c6 | |||
| bdc7ff1035 | |||
| da5f060741 | |||
| a56d335380 | |||
| d8aed552bc | |||
| d7286fa06e | |||
| 906f554d74 | |||
| cb44d5431a | |||
| a69eb8a66e | |||
| 1b411b1fed | |||
| 5d57959608 | |||
| 31e57c2ff8 | |||
| 734393d638 | |||
| 96504e2fb0 | |||
| ecfe802065 | |||
| 1ac9d54dab | |||
| 72d7e8aaaa | |||
| 0395696866 | |||
| 0667683e4d | |||
| aca0781c4b | |||
| b389d0eb9c | |||
| bf303ed471 | |||
| cd777ba2b4 | |||
| e3188a0a6d | |||
| 2bab0a014d | |||
| a01da18018 | |||
| 9d5a5c1e45 | |||
| 8377ad1d05 | |||
| ec33796bd3 | |||
| 31e4ba2722 | |||
| e0b1a50356 | |||
| 9bb36ebb6c | |||
| 756be9801e | |||
| bd73b07ed8 | |||
| df1d44d24e | |||
| 79245eeff4 | |||
| aa86c1ec25 | |||
| 2ab1d9d774 | |||
| a9e7a73cc8 | |||
| ea17b420d6 | |||
| 660979dfda | |||
| a6b9b4993f | |||
| cc74504ed8 | |||
| 791239be12 | |||
| a79061c7c2 | |||
| 50ad3b20c4 | |||
| 649de0131c | |||
| 8cb513cb89 | |||
| 3932dbaa84 | |||
| 4534b4d8ca | |||
| 8e571a66e3 | |||
| 0ccfcb0ec0 | |||
| 8bae4631d2 | |||
| 268629f551 | |||
| 0bd2fcde54 | |||
| 6f34cf0c95 | |||
| f8bc25d0ae | |||
| 8749562c96 | |||
| d9d2bdff44 | |||
| b3e9ed23ac | |||
| bf3f81ccac | |||
| ff39e2e496 | |||
| d2346a2aed | |||
| 8f57b1acb6 | |||
| 6fafd10482 | |||
| c726651b8b | |||
| 02af2e2849 | |||
| 6d9c7012b0 | |||
| 8a7712a4c8 | |||
| 82fa803a37 | |||
| 78a74da8d6 | |||
| 53242ea02f | |||
| af05083a1f | |||
| c41bddbbea | |||
| 54c8ca0112 | |||
| a518488289 | |||
| 99cc21aacb | |||
| bc8295baee | |||
| 50f9913c41 | |||
| 4c135b5a46 | |||
| 686fb374e9 | |||
| 2b3e6a2730 | |||
| 9143729042 | |||
| 3952f0ba0f | |||
| 7a131822db |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with: { go-version: '1.21' }
|
with: { go-version: '1.22' }
|
||||||
|
|
||||||
- name: Build go2rtc_win64
|
- name: Build go2rtc_win64
|
||||||
env: { GOOS: windows, GOARCH: amd64 }
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_win32
|
- 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
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win32
|
- name: Upload go2rtc_win32
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_mac_amd64
|
- 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
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_mac_amd64
|
- name: Upload go2rtc_mac_amd64
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -123,7 +123,9 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}},enable=false
|
type=semver,pattern={{version}},enable=false
|
||||||
@@ -142,6 +144,14 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -149,6 +159,7 @@ jobs:
|
|||||||
platforms: |
|
platforms: |
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/386
|
linux/386
|
||||||
|
linux/arm/v6
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
linux/arm64/v8
|
linux/arm64/v8
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
@@ -168,7 +179,9 @@ jobs:
|
|||||||
id: meta-hw
|
id: meta-hw
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
flavor: |
|
flavor: |
|
||||||
suffix=-hardware,onlatest=true
|
suffix=-hardware,onlatest=true
|
||||||
latest=auto
|
latest=auto
|
||||||
@@ -190,6 +203,14 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
|
go2rtc_linux*
|
||||||
|
go2rtc_mac*
|
||||||
|
go2rtc_win*
|
||||||
|
|
||||||
0_test.go
|
0_test.go
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+16
-15
@@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.11"
|
||||||
ARG GO_VERSION="1.21"
|
ARG GO_VERSION="1.22"
|
||||||
ARG NGROK_VERSION="3"
|
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
|
||||||
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
@@ -20,6 +25,8 @@ ENV GOARCH=${TARGETARCH}
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
# Cache dependencies
|
# Cache dependencies
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||||
@@ -28,15 +35,8 @@ COPY . .
|
|||||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
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
|
# 3. Final image
|
||||||
FROM base
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
|
|
||||||
# Install ffmpeg, tini (for signal handling),
|
# Install ffmpeg, tini (for signal handling),
|
||||||
# and other common tools for the echo source.
|
# and other common tools for the echo source.
|
||||||
@@ -54,7 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
|||||||
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
# 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", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
|
|
||||||
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
||||||
|
|
||||||
- `go2rtc_win64.zip` - Windows 64-bit
|
- `go2rtc_win64.zip` - Windows 10+ 64-bit
|
||||||
- `go2rtc_win32.zip` - Windows 32-bit
|
- `go2rtc_win32.zip` - Windows 7+ 32-bit
|
||||||
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
||||||
- `go2rtc_linux_amd64` - Linux 64-bit
|
- `go2rtc_linux_amd64` - Linux 64-bit
|
||||||
- `go2rtc_linux_i386` - Linux 32-bit
|
- `go2rtc_linux_i386` - Linux 32-bit
|
||||||
@@ -124,8 +124,8 @@ 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_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
||||||
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
|
- `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_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_amd64.zip` - macOS 10.13+ Intel 64-bit
|
||||||
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
|
||||||
|
|
||||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ Available modules:
|
|||||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||||
- [webrtc](#module-webrtc) - WebRTC Server
|
- [webrtc](#module-webrtc) - WebRTC Server
|
||||||
- [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
|
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
||||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||||
@@ -648,10 +648,11 @@ This source type support Roborock vacuums with cameras. Known working models:
|
|||||||
|
|
||||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||||
- Roborock S7 MaxV - video and two way audio
|
- 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
|
#### Source: WebRTC
|
||||||
|
|
||||||
@@ -779,7 +780,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
|||||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||||
|
|
||||||
- Supported codecs: H264 for video and AAC for audio
|
- Supported codecs: H264 for video and AAC for audio
|
||||||
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
|
- AAC audio is required for YouTube, videos without audio will not work
|
||||||
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||||
|
|
||||||
You can use API:
|
You can use API:
|
||||||
@@ -792,16 +793,19 @@ Or config file:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
publish:
|
publish:
|
||||||
# publish stream "tplink_tapo" to Telegram
|
# publish stream "video_audio_transcode" to Telegram
|
||||||
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
video_audio_transcode:
|
||||||
# publish stream "other_camera" to Telegram and YouTube
|
|
||||||
other_camera:
|
|
||||||
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
# publish stream "audio_transcode" to Telegram and YouTube
|
||||||
|
audio_transcode:
|
||||||
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
- rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||||
|
|
||||||
streams:
|
streams:
|
||||||
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
|
video_audio_transcode:
|
||||||
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
||||||
|
audio_transcode:
|
||||||
|
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := rtsp.NewClient(os.Args[1])
|
||||||
|
if err := client.Dial(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||||
|
},
|
||||||
|
ID: "streamid=0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.Announce(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if _, err := client.SetupMedia(client.Medias[0]); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if err := client.Record(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.RunUntilSignal()
|
||||||
|
}
|
||||||
@@ -1,48 +1,49 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.21
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astits v1.13.0
|
github.com/asticode/go-astits v1.13.0
|
||||||
github.com/expr-lang/expr v1.16.5
|
github.com/expr-lang/expr v1.16.9
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/miekg/dns v1.1.59
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/pion/ice/v2 v2.3.24
|
github.com/miekg/dns v1.1.62
|
||||||
github.com/pion/interceptor v0.1.29
|
github.com/pion/ice/v2 v2.3.36
|
||||||
|
github.com/pion/interceptor v0.1.37
|
||||||
github.com/pion/rtcp v1.2.14
|
github.com/pion/rtcp v1.2.14
|
||||||
github.com/pion/rtp v1.8.6
|
github.com/pion/rtp v1.8.9
|
||||||
github.com/pion/sdp/v3 v3.0.9
|
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/stun v0.6.1
|
||||||
github.com/pion/webrtc/v3 v3.2.40
|
github.com/pion/webrtc/v3 v3.3.4
|
||||||
github.com/rs/zerolog v1.32.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
golang.org/x/crypto v0.23.0
|
golang.org/x/crypto v0.28.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/pion/datachannel v1.5.9 // indirect
|
||||||
github.com/pion/datachannel v1.5.6 // indirect
|
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.11 // indirect
|
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.12 // indirect
|
github.com/pion/mdns v0.0.12 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.16 // indirect
|
github.com/pion/sctp v1.8.33 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/tools v0.20.0 // indirect
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.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 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||||
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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/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.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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
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.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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
||||||
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
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.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||||
github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||||
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
|
github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc=
|
||||||
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||||
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
|
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||||
github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||||
github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI=
|
|
||||||
github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
|
||||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
|
||||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/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/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||||
@@ -51,40 +50,38 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9
|
|||||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
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.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
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.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||||
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.9/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.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||||
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||||
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
|
||||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
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.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
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 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
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.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.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.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||||
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
|
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||||
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
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.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.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
|
||||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
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 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
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.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk=
|
||||||
github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA=
|
github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||||
github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
@@ -102,38 +99,31 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.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 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
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=
|
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-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.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.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.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.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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -150,40 +140,31 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.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.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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.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.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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.26.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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
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.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.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.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.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.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.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.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.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.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# only debian 13 (trixie) has latest ffmpeg
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
# https://packages.debian.org/trixie/ffmpeg
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
ARG DEBIAN_VERSION="trixie-slim"
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
ARG GO_VERSION="1.21-bookworm"
|
ARG GO_VERSION="1.22-bookworm"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -38,20 +39,19 @@ type Message struct {
|
|||||||
Value any `json:"value,omitempty"`
|
Value any `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) String() string {
|
func (m *Message) String() (value string) {
|
||||||
if s, ok := m.Value.(string); ok {
|
if s, ok := m.Value.(string); ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) GetString(key string) string {
|
func (m *Message) Unmarshal(v any) error {
|
||||||
if v, ok := m.Value.(map[string]any); ok {
|
b, err := json.Marshal(m.Value)
|
||||||
if s, ok := v[key].(string); ok {
|
if err != nil {
|
||||||
return s
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
return json.Unmarshal(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WSHandler func(tr *Transport, msg *Message) error
|
type WSHandler func(tr *Transport, msg *Message) error
|
||||||
|
|||||||
+23
-7
@@ -1,6 +1,10 @@
|
|||||||
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
|
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
|
||||||
- go2rtc support multiple config files
|
- go2rtc support multiple config files:
|
||||||
- go2rtc support inline config as `YAML`, `JSON` or `key=value` format from command line
|
- `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)
|
- 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
|
## 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
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||||
|
|
||||||
${LOGS:} # empty default value
|
|
||||||
|
|
||||||
rtsp:
|
rtsp:
|
||||||
username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set
|
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
|
||||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" 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
|
## 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
|
```yaml
|
||||||
api:
|
api:
|
||||||
listen: ":1984"
|
listen: ":1984"
|
||||||
@@ -38,7 +51,10 @@ ffmpeg:
|
|||||||
bin: "ffmpeg"
|
bin: "ffmpeg"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
|
format: "color"
|
||||||
level: "info"
|
level: "info"
|
||||||
|
output: "stdout"
|
||||||
|
time: "UNIXMS"
|
||||||
|
|
||||||
rtsp:
|
rtsp:
|
||||||
listen: ":8554"
|
listen: ":8554"
|
||||||
|
|||||||
+21
-138
@@ -1,28 +1,20 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.9.2"
|
var (
|
||||||
var UserAgent = "go2rtc/" + Version
|
Version string
|
||||||
|
UserAgent string
|
||||||
var ConfigPath string
|
ConfigPath string
|
||||||
var Info = map[string]any{
|
Info = make(map[string]any)
|
||||||
"version": Version,
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const usage = `Usage of go2rtc:
|
const usage = `Usage of go2rtc:
|
||||||
|
|
||||||
@@ -32,12 +24,12 @@ const usage = `Usage of go2rtc:
|
|||||||
`
|
`
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var confs Config
|
var config flagConfig
|
||||||
var daemon bool
|
var daemon bool
|
||||||
var version bool
|
var version bool
|
||||||
|
|
||||||
flag.Var(&confs, "config", "")
|
flag.Var(&config, "config", "")
|
||||||
flag.Var(&confs, "c", "")
|
flag.Var(&config, "c", "")
|
||||||
flag.BoolVar(&daemon, "daemon", false, "")
|
flag.BoolVar(&daemon, "daemon", false, "")
|
||||||
flag.BoolVar(&daemon, "d", false, "")
|
flag.BoolVar(&daemon, "d", false, "")
|
||||||
flag.BoolVar(&version, "version", false, "")
|
flag.BoolVar(&version, "version", false, "")
|
||||||
@@ -53,126 +45,39 @@ func Init() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if daemon {
|
if daemon && os.Getppid() != 1 {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
fmt.Println("Daemon not supported on Windows")
|
fmt.Println("Daemon mode is not supported on Windows")
|
||||||
os.Exit(1)
|
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
|
// Re-run the program in background and exit
|
||||||
cmd := exec.Command(os.Args[0], args...)
|
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Fatal().Err(err).Send()
|
fmt.Println("Failed to start daemon:", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if confs == nil {
|
UserAgent = "go2rtc/" + Version
|
||||||
confs = []string{"go2rtc.yaml"}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, conf := range confs {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Info["version"] = Version
|
||||||
Info["revision"] = revision
|
Info["revision"] = revision
|
||||||
|
|
||||||
var cfg struct {
|
initConfig(config)
|
||||||
Mod map[string]string `yaml:"log"`
|
initLogger()
|
||||||
}
|
|
||||||
|
|
||||||
LoadConfig(&cfg)
|
|
||||||
|
|
||||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
|
||||||
|
|
||||||
modules = cfg.Mod
|
|
||||||
|
|
||||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
log.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
|
Logger.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.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
|
||||||
|
|
||||||
if ConfigPath != "" {
|
if ConfigPath != "" {
|
||||||
log.Info().Str("path", ConfigPath).Msg("config")
|
Logger.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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func readRevisionTime() (revision, vcsTime string) {
|
||||||
if info, ok := debug.ReadBuildInfo(); ok {
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
for _, setting := range info.Settings {
|
for _, setting := range info.Settings {
|
||||||
@@ -194,25 +99,3 @@ func readRevisionTime() (revision, vcsTime string) {
|
|||||||
}
|
}
|
||||||
return
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadConfig(v any) {
|
||||||
|
for _, data := range configs {
|
||||||
|
if err := yaml.Unmarshal(data, v); err != nil {
|
||||||
|
Logger.Warn().Err(err).Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatchConfig(key string, value any, path ...string) error {
|
||||||
|
if ConfigPath == "" {
|
||||||
|
return errors.New("config file disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty config is OK
|
||||||
|
b, _ := os.ReadFile(ConfigPath)
|
||||||
|
|
||||||
|
b, err := yaml.Patch(b, key, value, path...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(ConfigPath, b, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
type flagConfig []string
|
||||||
|
|
||||||
|
func (c *flagConfig) String() string {
|
||||||
|
return strings.Join(*c, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flagConfig) Set(value string) error {
|
||||||
|
*c = append(*c, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs [][]byte
|
||||||
|
|
||||||
|
func initConfig(confs flagConfig) {
|
||||||
|
if confs == nil {
|
||||||
|
confs = []string{"go2rtc.yaml"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range confs {
|
||||||
|
if len(conf) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conf[0] == '{' {
|
||||||
|
// config as raw YAML or JSON
|
||||||
|
configs = append(configs, []byte(conf))
|
||||||
|
} else if data := parseConfString(conf); data != nil {
|
||||||
|
configs = append(configs, data)
|
||||||
|
} else {
|
||||||
|
// config as file
|
||||||
|
if ConfigPath == "" {
|
||||||
|
ConfigPath = conf
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, _ = os.ReadFile(conf); data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(shell.ReplaceEnvVars(string(data)))
|
||||||
|
configs = append(configs, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ConfigPath != "" {
|
||||||
|
if !filepath.IsAbs(ConfigPath) {
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
ConfigPath = filepath.Join(cwd, ConfigPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Info["config_path"] = ConfigPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfString(s string) []byte {
|
||||||
|
i := strings.IndexByte(s, '=')
|
||||||
|
if i < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
items := strings.Split(s[:i], ".")
|
||||||
|
if len(items) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// `log.level=trace` => `{log: {level: trace}}`
|
||||||
|
var pre string
|
||||||
|
var suf = s[i+1:]
|
||||||
|
for _, item := range items {
|
||||||
|
pre += "{" + item + ": "
|
||||||
|
suf += "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(pre + suf)
|
||||||
|
}
|
||||||
+80
-29
@@ -4,49 +4,100 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var MemoryLog *circularBuffer
|
var MemoryLog = newBuffer(16)
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
|
||||||
var writer io.Writer = os.Stdout
|
|
||||||
|
|
||||||
if format != "json" {
|
|
||||||
writer = zerolog.ConsoleWriter{
|
|
||||||
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MemoryLog = newBuffer(16)
|
|
||||||
|
|
||||||
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
|
||||||
lvl = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
func GetLogger(module string) zerolog.Logger {
|
||||||
if s, ok := modules[module]; ok {
|
if s, ok := modules[module]; ok {
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
lvl, err := zerolog.ParseLevel(s)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return log.Level(lvl)
|
return Logger.Level(lvl)
|
||||||
}
|
}
|
||||||
log.Warn().Err(err).Caller().Send()
|
Logger.Warn().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
return log.Logger
|
return Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initLogger support:
|
||||||
|
// - output: empty (only to memory), stderr, stdout
|
||||||
|
// - format: empty (autodetect color support), color, json, text
|
||||||
|
// - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO
|
||||||
|
// - level: disabled, trace, debug, info, warn, error...
|
||||||
|
func initLogger() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod map[string]string `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Mod = modules // defaults
|
||||||
|
|
||||||
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
|
var writer io.Writer
|
||||||
|
|
||||||
|
switch modules["output"] {
|
||||||
|
case "stderr":
|
||||||
|
writer = os.Stderr
|
||||||
|
case "stdout":
|
||||||
|
writer = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
timeFormat := modules["time"]
|
||||||
|
|
||||||
|
if writer != nil {
|
||||||
|
if format := modules["format"]; format != "json" {
|
||||||
|
console := &zerolog.ConsoleWriter{Out: writer}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "text":
|
||||||
|
console.NoColor = true
|
||||||
|
case "color":
|
||||||
|
console.NoColor = false // useless, but anyway
|
||||||
|
default:
|
||||||
|
// autodetection if output support color
|
||||||
|
// go-isatty - dependency for go-colorable - dependency for ConsoleWriter
|
||||||
|
console.NoColor = !isatty.IsTerminal(writer.(*os.File).Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeFormat != "" {
|
||||||
|
console.TimeFormat = "15:04:05.000"
|
||||||
|
} else {
|
||||||
|
console.PartsOrder = []string{
|
||||||
|
zerolog.LevelFieldName,
|
||||||
|
zerolog.CallerFieldName,
|
||||||
|
zerolog.MessageFieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = console
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
||||||
|
} else {
|
||||||
|
writer = MemoryLog
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||||
|
Logger = zerolog.New(writer).Level(lvl)
|
||||||
|
|
||||||
|
if timeFormat != "" {
|
||||||
|
zerolog.TimeFieldFormat = timeFormat
|
||||||
|
Logger = Logger.With().Timestamp().Logger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Logger zerolog.Logger
|
||||||
|
|
||||||
// modules log levels
|
// modules log levels
|
||||||
var modules map[string]string
|
var modules = map[string]string{
|
||||||
|
"format": "", // useless, but anyway
|
||||||
|
"level": "info",
|
||||||
|
"output": "stdout", // TODO: change to stderr someday
|
||||||
|
"time": zerolog.TimeFormatUnixMs,
|
||||||
|
}
|
||||||
|
|
||||||
const chunkSize = 1 << 16
|
const chunkSize = 1 << 16
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func migrateStore() {
|
|
||||||
const name = "go2rtc.json"
|
|
||||||
|
|
||||||
data, _ := os.ReadFile(name)
|
|
||||||
if data == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var store struct {
|
|
||||||
Streams map[string]string `json:"streams"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &store); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for id, url := range store.Streams {
|
|
||||||
if err := PatchConfig(id, url, "streams"); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = os.Remove(name)
|
|
||||||
}
|
|
||||||
@@ -7,13 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("bubble", handle)
|
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
|
||||||
}
|
return bubble.Dial(source)
|
||||||
|
})
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn := bubble.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,8 @@ package debug
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/stack", stackHandler)
|
api.HandleFunc("api/stack", stackHandler)
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullHandler(string) (core.Producer, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-15
@@ -10,26 +10,16 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("dvrip", handle)
|
streams.HandleFunc("dvrip", dvrip.Dial)
|
||||||
|
|
||||||
// DVRIP client autodiscovery
|
// DVRIP client autodiscovery
|
||||||
api.HandleFunc("api/dvrip", apiDvrip)
|
api.HandleFunc("api/dvrip", apiDvrip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
client, err := dvrip.Dial(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const Port = 34569 // UDP port number for dvrip discovery
|
const Port = 34569 // UDP port number for dvrip discovery
|
||||||
|
|
||||||
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -92,10 +82,7 @@ func sendBroadcasts(conn *net.UDPConn) {
|
|||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
_, _ = conn.WriteToUDP(data, addr)
|
||||||
if _, err = conn.WriteToUDP(data, addr); err != nil {
|
|
||||||
log.Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// closer support custom killsignal with custom killtimeout
|
||||||
|
type closer struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
query url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *closer) Close() (err error) {
|
||||||
|
sig := os.Kill
|
||||||
|
if s := c.query.Get("killsignal"); s != "" {
|
||||||
|
sig = syscall.Signal(core.Atoi(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[exec] kill with signal=%d", sig)
|
||||||
|
err = c.cmd.Process.Signal(sig)
|
||||||
|
|
||||||
|
if s := c.query.Get("killtimeout"); s != "" {
|
||||||
|
timeout := time.Duration(core.Atoi(s)) * time.Second
|
||||||
|
timer := time.AfterFunc(timeout, func() {
|
||||||
|
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
||||||
|
_ = c.cmd.Process.Kill()
|
||||||
|
})
|
||||||
|
defer timer.Stop() // stop timer if Wait ends before timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(err, c.cmd.Wait())
|
||||||
|
}
|
||||||
+99
-43
@@ -1,6 +1,7 @@
|
|||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -49,8 +50,10 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execHandle(rawURL string) (core.Producer, error) {
|
func execHandle(rawURL string) (core.Producer, error) {
|
||||||
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
|
||||||
var path string
|
var path string
|
||||||
var query url.Values
|
|
||||||
|
|
||||||
// RTSP flow should have `{output}` inside URL
|
// RTSP flow should have `{output}` inside URL
|
||||||
// pipe flow may have `#{params}` inside URL
|
// pipe flow may have `#{params}` inside URL
|
||||||
@@ -62,60 +65,73 @@ func execHandle(rawURL string) (core.Producer, error) {
|
|||||||
sum := md5.Sum([]byte(rawURL))
|
sum := md5.Sum([]byte(rawURL))
|
||||||
path = "/" + hex.EncodeToString(sum[:])
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||||
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
|
|
||||||
query = streams.ParseQuery(rawURL[i+1:])
|
|
||||||
rawURL = rawURL[:i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
if log.Debug().Enabled() {
|
cmd.Stderr = &logWriter{
|
||||||
cmd.Stderr = os.Stderr
|
buf: make([]byte, 512),
|
||||||
|
debug: log.Debug().Enabled(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
return handlePipe(rawURL, cmd, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleRTSP(rawURL, cmd, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
|
||||||
if query.Get("backchannel") == "1" {
|
if query.Get("backchannel") == "1" {
|
||||||
return stdin.NewClient(cmd)
|
return stdin.NewClient(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := PipeCloser(cmd, query)
|
cl := &closer{cmd: cmd, query: query}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return handlePipe(rawURL, cmd, cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleRTSP(rawURL, cmd, cl, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc := struct {
|
||||||
|
io.Reader
|
||||||
|
io.Closer
|
||||||
|
}{
|
||||||
|
// add buffer for pipe reader to reduce syscall
|
||||||
|
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||||
|
cl,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
if err = cmd.Start(); err != nil {
|
if err = cmd.Start(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prod, err := magic.Open(r)
|
prod, err := magic.Open(rc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = r.Close()
|
_ = rc.Close()
|
||||||
|
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return prod, err
|
if info, ok := prod.(core.Info); ok {
|
||||||
|
info.SetProtocol("pipe")
|
||||||
|
setRemoteInfo(info, source, cmd.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) {
|
||||||
stderr := limitBuffer{buf: make([]byte, 512)}
|
|
||||||
|
|
||||||
if cmd.Stderr != nil {
|
|
||||||
cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr)
|
|
||||||
} else {
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
waiter := make(chan core.Producer)
|
waiter := make(chan *pkg.Conn, 1)
|
||||||
|
|
||||||
waitersMu.Lock()
|
waitersMu.Lock()
|
||||||
waiters[path] = waiter
|
waiters[path] = waiter
|
||||||
@@ -127,12 +143,12 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
waitersMu.Unlock()
|
waitersMu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run")
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp")
|
||||||
|
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Error().Err(err).Str("url", url).Msg("[exec]")
|
log.Error().Err(err).Str("source", source).Msg("[exec]")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,15 +158,17 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 60):
|
case <-time.After(time.Minute):
|
||||||
_ = cmd.Process.Kill()
|
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
_ = cl.Close()
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("exec: timeout")
|
||||||
case <-done:
|
case <-done:
|
||||||
// limit message size
|
// limit message size
|
||||||
return nil, errors.New("exec: " + stderr.String())
|
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||||
case prod := <-waiter:
|
case prod := <-waiter:
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||||
|
setRemoteInfo(prod, source, cmd.Args)
|
||||||
|
prod.OnClose = cl.Close
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,25 +177,63 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
waiters = map[string]chan core.Producer{}
|
waiters = make(map[string]chan *pkg.Conn)
|
||||||
waitersMu sync.Mutex
|
waitersMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
type limitBuffer struct {
|
type logWriter struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
n int
|
debug bool
|
||||||
|
n int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *limitBuffer) String() string {
|
func (l *logWriter) String() string {
|
||||||
if l.n == len(l.buf) {
|
if l.n == len(l.buf) {
|
||||||
return string(l.buf) + "..."
|
return string(l.buf) + "..."
|
||||||
}
|
}
|
||||||
return string(l.buf[:l.n])
|
return string(l.buf[:l.n])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *limitBuffer) Write(p []byte) (int, error) {
|
func (l *logWriter) Write(p []byte) (n int, err error) {
|
||||||
if l.n < cap(l.buf) {
|
if l.n < cap(l.buf) {
|
||||||
l.n += copy(l.buf[l.n:], p)
|
l.n += copy(l.buf[l.n:], p)
|
||||||
}
|
}
|
||||||
return len(p), nil
|
n = len(p)
|
||||||
|
if l.debug {
|
||||||
|
if p = trimSpace(p); p != nil {
|
||||||
|
log.Debug().Msgf("[exec] %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpace(b []byte) []byte {
|
||||||
|
start := 0
|
||||||
|
stop := len(b)
|
||||||
|
for ; start < stop; start++ {
|
||||||
|
if b[start] >= ' ' {
|
||||||
|
break // trim all ASCII before 0x20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; ; stop-- {
|
||||||
|
if stop == start {
|
||||||
|
return nil // skip empty output
|
||||||
|
}
|
||||||
|
if b[stop-1] > ' ' {
|
||||||
|
break // trim all ASCII before 0x21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[start:stop]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRemoteInfo(info core.Info, source string, args []string) {
|
||||||
|
info.SetSource(source)
|
||||||
|
|
||||||
|
if i := 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()
|
|
||||||
}
|
|
||||||
@@ -45,6 +45,13 @@
|
|||||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TTS
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma
|
||||||
|
```
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
dst := query.Get("dst")
|
||||||
|
stream := streams.Get(dst)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var src string
|
||||||
|
if s := query.Get("file"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto#input=file"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("live"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("text"); s != "" {
|
||||||
|
if strings.IndexAny(s, `'"&%$`) < 0 {
|
||||||
|
src = "ffmpeg:tts?text=" + s
|
||||||
|
if s = query.Get("voice"); s != "" {
|
||||||
|
src += "&voice=" + s
|
||||||
|
}
|
||||||
|
src += "#audio=auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if src == "" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stream.Play(src); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
@@ -17,24 +15,15 @@ func Init(bin string) {
|
|||||||
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
func GetInput(src string) string {
|
||||||
i := strings.IndexByte(src, '?')
|
query, err := url.ParseQuery(src)
|
||||||
if i < 0 {
|
|
||||||
return "", errors.New("empty query: " + src)
|
|
||||||
}
|
|
||||||
|
|
||||||
query, err := url.ParseQuery(src[i+1:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
runonce.Do(initDevices)
|
runonce.Do(initDevices)
|
||||||
|
|
||||||
if input := queryToInput(query); input != "" {
|
return queryToInput(query)
|
||||||
return input, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("wrong query: " + src)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Bin string
|
var Bin string
|
||||||
|
|||||||
+73
-20
@@ -4,33 +4,55 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]string `yaml:"ffmpeg"`
|
Mod map[string]string `yaml:"ffmpeg"`
|
||||||
|
Log struct {
|
||||||
|
Level string `yaml:"ffmpeg"`
|
||||||
|
} `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Mod = defaults // will be overriden from yaml
|
cfg.Mod = defaults // will be overriden from yaml
|
||||||
|
cfg.Log.Level = "error"
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
log = app.GetLogger("ffmpeg")
|
||||||
defaults["global"] += " -v error"
|
|
||||||
|
// zerolog levels: trace debug info warn error fatal panic disabled
|
||||||
|
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
|
||||||
|
if cfg.Log.Level == "warn" {
|
||||||
|
cfg.Log.Level = "warning"
|
||||||
}
|
}
|
||||||
|
defaults["global"] += " -v " + cfg.Log.Level
|
||||||
|
|
||||||
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
||||||
|
if _, err := Version(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
args := parseArgs(url[7:])
|
args := parseArgs(url[7:])
|
||||||
|
if core.Contains(args.Codecs, "auto") {
|
||||||
|
return "", nil // force call streams.HandleFunc("ffmpeg")
|
||||||
|
}
|
||||||
return "exec:" + args.String(), nil
|
return "exec:" + args.String(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("ffmpeg", NewProducer)
|
||||||
|
|
||||||
|
api.HandleFunc("api/ffmpeg", apiFFmpeg)
|
||||||
|
|
||||||
device.Init(defaults["bin"])
|
device.Init(defaults["bin"])
|
||||||
hardware.Init(defaults["bin"])
|
hardware.Init(defaults["bin"])
|
||||||
}
|
}
|
||||||
@@ -49,6 +71,9 @@ var defaults = map[string]string{
|
|||||||
// output
|
// output
|
||||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||||
"output/mjpeg": "-f mjpeg -",
|
"output/mjpeg": "-f mjpeg -",
|
||||||
|
"output/raw": "-f yuv4mpegpipe -",
|
||||||
|
"output/aac": "-f adts -",
|
||||||
|
"output/wav": "-f wav -",
|
||||||
|
|
||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||||
// `-tune zerolatency` - for minimal latency
|
// `-tune zerolatency` - for minimal latency
|
||||||
@@ -59,6 +84,12 @@ var defaults = map[string]string{
|
|||||||
"mjpeg": "-c:v mjpeg",
|
"mjpeg": "-c:v mjpeg",
|
||||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||||
|
|
||||||
|
"raw": "-c:v rawvideo",
|
||||||
|
"raw/gray8": "-c:v rawvideo -pix_fmt:v gray8",
|
||||||
|
"raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p",
|
||||||
|
"raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p",
|
||||||
|
"raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p",
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||||
// https://github.com/pion/webrtc/issues/1514
|
// https://github.com/pion/webrtc/issues/1514
|
||||||
// https://ffmpeg.org/ffmpeg-resampler.html
|
// https://ffmpeg.org/ffmpeg-resampler.html
|
||||||
@@ -116,6 +147,8 @@ var defaults = map[string]string{
|
|||||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
// configTemplate - return template from config (defaults) if exist or return raw template
|
// configTemplate - return template from config (defaults) if exist or return raw template
|
||||||
func configTemplate(template string) string {
|
func configTemplate(template string) string {
|
||||||
if s := defaults[template]; s != "" {
|
if s := defaults[template]; s != "" {
|
||||||
@@ -140,11 +173,13 @@ func inputTemplate(name, s string, query url.Values) string {
|
|||||||
func parseArgs(s string) *ffmpeg.Args {
|
func parseArgs(s string) *ffmpeg.Args {
|
||||||
// init FFmpeg arguments
|
// init FFmpeg arguments
|
||||||
args := &ffmpeg.Args{
|
args := &ffmpeg.Args{
|
||||||
Bin: defaults["bin"],
|
Bin: defaults["bin"],
|
||||||
Global: defaults["global"],
|
Global: defaults["global"],
|
||||||
Output: defaults["output"],
|
Output: defaults["output"],
|
||||||
|
Version: verAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var source = s
|
||||||
var query url.Values
|
var query url.Values
|
||||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||||
query = streams.ParseQuery(s[i+1:])
|
query = streams.ParseQuery(s[i+1:])
|
||||||
@@ -187,17 +222,19 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
default:
|
default:
|
||||||
s += "?video&audio"
|
s += "?video&audio"
|
||||||
}
|
}
|
||||||
args.Input = inputTemplate("rtsp", s, query)
|
s += "&source=ffmpeg:" + url.QueryEscape(source)
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
for _, v := range query["query"] {
|
||||||
var err error
|
s += "&" + v
|
||||||
args.Input, err = device.GetInput(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(s, "virtual?") {
|
args.Input = inputTemplate("rtsp", s, query)
|
||||||
var err error
|
} else if i = strings.Index(s, "?"); i > 0 {
|
||||||
if args.Input, err = virtual.GetInput(s[8:]); err != nil {
|
switch s[:i] {
|
||||||
return nil
|
case "device":
|
||||||
|
args.Input = device.GetInput(s[i+1:])
|
||||||
|
case "virtual":
|
||||||
|
args.Input = virtual.GetInput(s[i+1:])
|
||||||
|
case "tts":
|
||||||
|
args.Input = virtual.GetInputTTS(s[i+1:])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args.Input = inputTemplate("file", s, query)
|
args.Input = inputTemplate("file", s, query)
|
||||||
@@ -315,11 +352,27 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
args.AddCodec("-an")
|
args.AddCodec("-an")
|
||||||
}
|
}
|
||||||
|
|
||||||
// transcoding to only mjpeg
|
// change otput from RTSP to some other pipe format
|
||||||
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
|
switch {
|
||||||
// no transcoding from mjpeg input
|
case args.Video == 0 && args.Audio == 0:
|
||||||
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
|
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
|
||||||
args.Output = defaults["output/mjpeg"]
|
if strings.Contains(args.Input, " mjpeg ") {
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
}
|
||||||
|
case args.Video == 1 && args.Audio == 0:
|
||||||
|
switch core.Before(query.Get("video"), "/") {
|
||||||
|
case "mjpeg":
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
case "raw":
|
||||||
|
args.Output = defaults["output/raw"]
|
||||||
|
}
|
||||||
|
case args.Video == 0 && args.Audio == 1:
|
||||||
|
switch core.Before(query.Get("audio"), "/") {
|
||||||
|
case "aac":
|
||||||
|
args.Output = defaults["output/aac"]
|
||||||
|
case "pcma", "pcmu", "pcml":
|
||||||
|
args.Output = defaults["output/wav"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|||||||
+153
-74
@@ -3,6 +3,7 @@ package ffmpeg
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ func TestParseArgsFile(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
||||||
source: "/media/bbb.mp4#video=h265#rotate=-90",
|
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",
|
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||||
@@ -52,85 +53,143 @@ func TestParseArgsFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsDevice(t *testing.T) {
|
func TestParseArgsDevice(t *testing.T) {
|
||||||
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
tests := []struct {
|
||||||
args := parseArgs("device?video=0&video_size=1920x1080")
|
name string
|
||||||
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())
|
source string
|
||||||
|
expect 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")
|
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
|
||||||
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())
|
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}`,
|
||||||
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())
|
{
|
||||||
|
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) {
|
func TestParseArgsIpCam(t *testing.T) {
|
||||||
// [HTTP] video will be copied
|
tests := []struct {
|
||||||
args := parseArgs("http://example.com")
|
name string
|
||||||
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())
|
source string
|
||||||
|
expect 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())
|
name: "[HTTP] video will be copied",
|
||||||
|
source: "http://example.com",
|
||||||
// [HLS] video will be copied, audio will be skipped
|
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}`,
|
||||||
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())
|
{
|
||||||
|
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||||
// [RTSP] video will be copied without transcoding codecs
|
source: "http://example.com#video=h264",
|
||||||
args = parseArgs("rtsp://example.com")
|
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}`,
|
||||||
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
|
name: "[HLS] video will be copied, audio will be skipped",
|
||||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
|
source: "https://example.com#video=copy",
|
||||||
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())
|
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}`,
|
||||||
|
},
|
||||||
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
{
|
||||||
args = parseArgs("rtsp://example.com#input=rtsp/udp")
|
name: "[RTSP] video will be copied without transcoding codecs",
|
||||||
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())
|
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}`,
|
||||||
// [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())
|
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) {
|
func TestParseArgsAudio(t *testing.T) {
|
||||||
// [AUDIO] audio will be transcoded to AAC, video will be skipped
|
tests := []struct {
|
||||||
args := parseArgs("rtsp:///example.com#audio=aac")
|
name string
|
||||||
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())
|
source string
|
||||||
|
expect 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())
|
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=aac",
|
||||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
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 -`,
|
||||||
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())
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
|
||||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
source: "rtsp://example.com#audio=aac/16000",
|
||||||
args = parseArgs("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 aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
|
||||||
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
|
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
|
source: "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 pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
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}`,
|
||||||
|
},
|
||||||
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
|
{
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
|
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
|
||||||
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())
|
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 -`,
|
||||||
// [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())
|
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=pcmu/16000",
|
||||||
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
|
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 -`,
|
||||||
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())
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
|
||||||
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
|
source: "rtsp://example.com#audio=pcmu/48000",
|
||||||
args = parseArgs("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_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||||
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())
|
},
|
||||||
|
{
|
||||||
|
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) {
|
func TestParseArgsHwVaapi(t *testing.T) {
|
||||||
@@ -292,3 +351,23 @@ func TestDrawText(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
verAV = ffmpeg.Version61
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: "/media/bbb.mp4",
|
||||||
|
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
args := parseArgs(test.source)
|
||||||
|
require.Equal(t, test.expect, args.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -152,7 +150,6 @@ var cache = map[string]string{}
|
|||||||
|
|
||||||
func run(bin string, args string) bool {
|
func run(bin string, args string) bool {
|
||||||
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
||||||
log.Printf("%v %v", args, err)
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Producer struct {
|
||||||
|
core.Connection
|
||||||
|
url string
|
||||||
|
query url.Values
|
||||||
|
ffmpeg core.Producer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities
|
||||||
|
func NewProducer(url string) (core.Producer, error) {
|
||||||
|
p := &Producer{}
|
||||||
|
|
||||||
|
i := strings.IndexByte(url, '#')
|
||||||
|
p.url, p.query = url[:i], streams.ParseQuery(url[i+1:])
|
||||||
|
|
||||||
|
// ffmpeg.NewProducer support only one audio
|
||||||
|
if len(p.query["video"]) != 0 || len(p.query["audio"]) != 1 {
|
||||||
|
return nil, errors.New("ffmpeg: unsupported params: " + url[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ID = core.NewID()
|
||||||
|
p.FormatName = "ffmpeg"
|
||||||
|
p.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
// codecs in order from best to worst
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
// OPUS will always marked as OPUS/48000/2
|
||||||
|
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||||
|
{Name: core.CodecPCM, ClockRate: 16000},
|
||||||
|
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||||
|
{Name: core.CodecPCM, ClockRate: 8000},
|
||||||
|
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||||
|
// AAC has unknown problems on Dahua two way
|
||||||
|
{Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) Start() error {
|
||||||
|
var err error
|
||||||
|
if p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, media := range p.ffmpeg.GetMedias() {
|
||||||
|
track, err := p.ffmpeg.GetTrack(media, media.Codecs[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Receivers[i].Replace(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ffmpeg.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) Stop() error {
|
||||||
|
if p.ffmpeg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.ffmpeg.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||||
|
if p.ffmpeg == nil {
|
||||||
|
return json.Marshal(p.Connection)
|
||||||
|
}
|
||||||
|
return json.Marshal(p.ffmpeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) newURL() string {
|
||||||
|
s := p.url
|
||||||
|
// rewrite codecs in url from auto to known presets from defaults
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
codec := receiver.Codec
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecOpus:
|
||||||
|
s += "#audio=opus"
|
||||||
|
case core.CodecAAC:
|
||||||
|
s += "#audio=aac/16000"
|
||||||
|
case core.CodecPCM:
|
||||||
|
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
case core.CodecPCMA:
|
||||||
|
s += "#audio=pcma/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
case core.CodecPCMU:
|
||||||
|
s += "#audio=pcmu/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add other params
|
||||||
|
for key, values := range p.query {
|
||||||
|
if key != "audio" {
|
||||||
|
for _, value := range values {
|
||||||
|
s += "#" + key + "=" + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var verMu sync.Mutex
|
||||||
|
var verErr error
|
||||||
|
var verFF string
|
||||||
|
var verAV string
|
||||||
|
|
||||||
|
func Version() (string, error) {
|
||||||
|
verMu.Lock()
|
||||||
|
defer verMu.Unlock()
|
||||||
|
|
||||||
|
if verFF != "" {
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(defaults["bin"], "-version")
|
||||||
|
b, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
verFF = "-"
|
||||||
|
verErr = err
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
|
|
||||||
|
verFF, verAV = ffmpeg.ParseVersion(b)
|
||||||
|
|
||||||
|
if verFF == "" {
|
||||||
|
verFF = "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
// better to compare libavformat, because nightly/master builds
|
||||||
|
if verAV != "" && verAV < ffmpeg.Version50 {
|
||||||
|
verErr = errors.New("ffmpeg: unsupported version: " + verFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin")
|
||||||
|
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
@@ -4,56 +4,76 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
func GetInput(src string) string {
|
||||||
query, err := url.ParseQuery(src)
|
query, err := url.ParseQuery(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// set defaults (using Add instead of Set)
|
input := "-re"
|
||||||
query.Add("video", "testsrc")
|
|
||||||
query.Add("size", "1920x1080")
|
|
||||||
query.Add("decimals", "2")
|
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-filters.html
|
for _, video := range query["video"] {
|
||||||
video := query.Get("video")
|
// https://ffmpeg.org/ffmpeg-filters.html
|
||||||
input := "-re -f lavfi -i " + video
|
sep := "=" // first separator
|
||||||
|
|
||||||
sep := "=" // first separator
|
if video == "" {
|
||||||
for key, values := range query {
|
video = "testsrc=decimals=2" // default video
|
||||||
value := values[0]
|
sep = ":"
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
|
||||||
switch key {
|
|
||||||
case "color", "rate", "duration", "sar":
|
|
||||||
case "size":
|
|
||||||
switch value {
|
|
||||||
case "720":
|
|
||||||
value = "1280x720" // crf=1 -> 12 Mbps
|
|
||||||
case "1080":
|
|
||||||
value = "1920x1080" // crf=1 -> 25 Mbps
|
|
||||||
case "2K":
|
|
||||||
value = "2560x1440" // crf=1 -> 43 Mbps
|
|
||||||
case "4K":
|
|
||||||
value = "3840x2160" // crf=1 -> 103 Mbps
|
|
||||||
case "8K":
|
|
||||||
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
|
||||||
}
|
|
||||||
case "decimals":
|
|
||||||
if video != "testsrc" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input += sep + key + "=" + value
|
input += " -f lavfi -i " + video
|
||||||
sep = ":" // next separator
|
|
||||||
|
// set defaults (using Add instead of Set)
|
||||||
|
query.Add("size", "1920x1080")
|
||||||
|
|
||||||
|
for key, values := range query {
|
||||||
|
value := values[0]
|
||||||
|
|
||||||
|
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
||||||
|
switch key {
|
||||||
|
case "color", "rate", "duration", "sar", "decimals":
|
||||||
|
case "size":
|
||||||
|
switch value {
|
||||||
|
case "720":
|
||||||
|
value = "1280x720" // crf=1 -> 12 Mbps
|
||||||
|
case "1080":
|
||||||
|
value = "1920x1080" // crf=1 -> 25 Mbps
|
||||||
|
case "2K":
|
||||||
|
value = "2560x1440" // crf=1 -> 43 Mbps
|
||||||
|
case "4K":
|
||||||
|
value = "3840x2160" // crf=1 -> 103 Mbps
|
||||||
|
case "8K":
|
||||||
|
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
input += sep + key + "=" + value
|
||||||
|
sep = ":" // next separator
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := query.Get("format"); s != "" {
|
||||||
|
input += ",format=" + s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := query.Get("format"); s != "" {
|
return input
|
||||||
input += ",format=" + s
|
}
|
||||||
}
|
|
||||||
|
func GetInputTTS(src string) string {
|
||||||
return input, nil
|
query, err := url.ParseQuery(src)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'`
|
||||||
|
|
||||||
|
// ffmpeg -f lavfi -i flite=list_voices=1
|
||||||
|
// awb, kal, kal16, rms, slt
|
||||||
|
if voice := query.Get("voice"); voice != "" {
|
||||||
|
input += ":voice" + voice
|
||||||
|
}
|
||||||
|
|
||||||
|
return input + `"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package virtual
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetInput(t *testing.T) {
|
||||||
|
s := GetInput("video")
|
||||||
|
require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s)
|
||||||
|
|
||||||
|
s = GetInput("video=testsrc2&size=4K")
|
||||||
|
require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInputTTS(t *testing.T) {
|
||||||
|
s := GetInputTTS("text=hello world&voice=slt")
|
||||||
|
require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s)
|
||||||
|
}
|
||||||
@@ -10,15 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("gopro", handleGoPro)
|
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
|
||||||
|
return gopro.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/gopro", apiGoPro)
|
api.HandleFunc("api/gopro", apiGoPro)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGoPro(rawURL string) (core.Producer, error) {
|
|
||||||
return gopro.Dial(rawURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||||
var items []*api.Source
|
var items []*api.Source
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -45,19 +45,14 @@ func Init() {
|
|||||||
return "", nil
|
return "", nil
|
||||||
})
|
})
|
||||||
|
|
||||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
|
||||||
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||||
client, err := hass.NewClient(url)
|
return hass.NewClient(source)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// load static entries from Hass config
|
// load static entries from Hass config
|
||||||
if err := importConfig(conf.Mod.Config); err != nil {
|
if err := importConfig(conf.Mod.Config); err != nil {
|
||||||
log.Debug().Msgf("[hass] can't import config: %s", err)
|
log.Trace().Msgf("[hass] can't import config: %s", err)
|
||||||
|
|
||||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
http.Error(w, "no hass config", http.StatusNotFound)
|
http.Error(w, "no hass config", http.StatusNotFound)
|
||||||
|
|||||||
+4
-7
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
medias := mp4.ParseQuery(r.URL.Query())
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
if medias != nil {
|
if medias != nil {
|
||||||
c := mp4.NewConsumer(medias)
|
c := mp4.NewConsumer(medias)
|
||||||
c.Type = "HLS/fMP4 consumer"
|
c.FormatName = "hls/fmp4"
|
||||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
c.WithRequest(r)
|
||||||
c.UserAgent = r.UserAgent()
|
|
||||||
cons = c
|
cons = c
|
||||||
} else {
|
} else {
|
||||||
c := mpegts.NewConsumer()
|
c := mpegts.NewConsumer()
|
||||||
c.Type = "HLS/TS consumer"
|
c.FormatName = "hls/mpegts"
|
||||||
c.RemoteAddr = tcp.RemoteAddr(r)
|
c.WithRequest(r)
|
||||||
c.UserAgent = r.UserAgent()
|
|
||||||
cons = c
|
cons = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||||
@@ -20,9 +19,8 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
codecs := msg.String()
|
codecs := msg.String()
|
||||||
medias := mp4.ParseCodecs(codecs, true)
|
medias := mp4.ParseCodecs(codecs, true)
|
||||||
cons := mp4.NewConsumer(medias)
|
cons := mp4.NewConsumer(medias)
|
||||||
cons.Type = "HLS/fMP4 consumer"
|
cons.FormatName = "hls/fmp4"
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
cons.WithRequest(tr.Request)
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
||||||
|
|
||||||
|
|||||||
@@ -133,12 +133,19 @@ func Init() {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var servers map[string]*server
|
var servers map[string]*server
|
||||||
|
|
||||||
func streamHandler(url string) (core.Producer, error) {
|
func streamHandler(rawURL string) (core.Producer, error) {
|
||||||
if srtp.Server == nil {
|
if srtp.Server == nil {
|
||||||
return nil, errors.New("homekit: can't work without SRTP server")
|
return nil, errors.New("homekit: can't work without SRTP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
return homekit.Dial(url, srtp.Server)
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
client, err := homekit.Dial(rawURL, srtp.Server)
|
||||||
|
if client != nil && rawQuery != "" {
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
client.Bitrate = parseBitrate(query.Get("bitrate"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -199,3 +206,24 @@ func findHomeKitURL(stream *streams.Stream) string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBitrate(s string) int {
|
||||||
|
n := len(s)
|
||||||
|
if n == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var k int
|
||||||
|
switch n--; s[n] {
|
||||||
|
case 'K':
|
||||||
|
k = 1024
|
||||||
|
s = s[:n]
|
||||||
|
case 'M':
|
||||||
|
k = 1024 * 1024
|
||||||
|
s = s[:n]
|
||||||
|
default:
|
||||||
|
k = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return k * core.Atoi(s)
|
||||||
|
}
|
||||||
|
|||||||
+21
-8
@@ -11,9 +11,9 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hls"
|
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/image"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/multipart"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +45,21 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prod, err := do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, ok := prod.(core.Info); ok {
|
||||||
|
info.SetProtocol("http")
|
||||||
|
info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn
|
||||||
|
info.SetURL(rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func do(req *http.Request) (core.Producer, error) {
|
||||||
res, err := tcp.Do(req)
|
res, err := tcp.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,14 +81,12 @@ func handleHTTP(rawURL string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ct == "image/jpeg":
|
|
||||||
return mjpeg.NewClient(res), nil
|
|
||||||
|
|
||||||
case ct == "multipart/x-mixed-replace":
|
|
||||||
return multipart.Open(res.Body)
|
|
||||||
|
|
||||||
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
|
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
|
||||||
return hls.OpenURL(req.URL, res.Body)
|
return hls.OpenURL(req.URL, res.Body)
|
||||||
|
case ct == "image/jpeg":
|
||||||
|
return image.Open(res)
|
||||||
|
case ct == "multipart/x-mixed-replace":
|
||||||
|
return mpjpeg.Open(res.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return magic.Open(res.Body)
|
return magic.Open(res.Body)
|
||||||
|
|||||||
+3
-12
@@ -7,16 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("isapi", handle)
|
streams.HandleFunc("isapi", func(source string) (core.Producer, error) {
|
||||||
}
|
return isapi.Dial(source)
|
||||||
|
})
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn, err := isapi.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,10 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("ivideon", func(url string) (core.Producer, error) {
|
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
|
||||||
id := strings.Replace(url[8:], "/", ":", 1)
|
return ivideon.Dial(source)
|
||||||
prod := ivideon.NewClient(id)
|
|
||||||
if err := prod.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return prod, nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-15
@@ -10,24 +10,31 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ascii"
|
"github.com/AlexxIT/go2rtc/pkg/ascii"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/AlexxIT/go2rtc/pkg/y4m"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||||
api.HandleFunc("api/stream.ascii", handlerStream)
|
api.HandleFunc("api/stream.ascii", handlerStream)
|
||||||
|
api.HandleFunc("api/stream.y4m", apiStreamY4M)
|
||||||
|
|
||||||
ws.HandleFunc("mjpeg", handlerWS)
|
ws.HandleFunc("mjpeg", handlerWS)
|
||||||
|
|
||||||
|
log = app.GetLogger("mjpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(src)
|
stream := streams.Get(src)
|
||||||
@@ -37,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := magic.NewKeyframe()
|
cons := magic.NewKeyframe()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -93,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mjpeg.NewConsumer()
|
cons := mjpeg.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||||
@@ -110,7 +115,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
wr := mjpeg.NewWriter(w)
|
wr := mjpeg.NewWriter(w)
|
||||||
_, _ = cons.WriteTo(wr)
|
_, _ = cons.WriteTo(wr)
|
||||||
} else {
|
} else {
|
||||||
cons.Type = "ASCII passive consumer "
|
cons.FormatName = "ascii"
|
||||||
|
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
|
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
|
||||||
@@ -128,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
|
prod, _ := mpjpeg.Open(r.Body)
|
||||||
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
|
prod.WithRequest(r)
|
||||||
|
|
||||||
client := mjpeg.NewClient(res)
|
stream.AddProducer(prod)
|
||||||
stream.AddProducer(client)
|
|
||||||
|
|
||||||
if err := client.Start(); err != nil && err != io.EOF {
|
if err := prod.Start(); err != nil && err != io.EOF {
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.Warn().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.RemoveProducer(client)
|
stream.RemoveProducer(prod)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||||
@@ -148,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mjpeg.NewConsumer()
|
cons := mjpeg.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
cons.WithRequest(tr.Request)
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
||||||
@@ -166,3 +169,24 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
|
||||||
|
src := r.URL.Query().Get("src")
|
||||||
|
stream := streams.Get(src)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := y4m.NewConsumer()
|
||||||
|
cons.WithRequest(r)
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}
|
||||||
|
|||||||
+17
-17
@@ -1,6 +1,7 @@
|
|||||||
package mp4
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,7 +13,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
medias := mp4.ParseQuery(r.URL.Query())
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
cons := mp4.NewConsumer(medias)
|
cons := mp4.NewConsumer(medias)
|
||||||
cons.Type = "MP4/HTTP active consumer"
|
cons.FormatName = "mp4"
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.Protocol = "http"
|
||||||
cons.UserAgent = r.UserAgent()
|
cons.WithRequest(r)
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -127,20 +127,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var duration *time.Timer
|
ctx := r.Context() // handle when the client drops the connection
|
||||||
if s := query.Get("duration"); s != "" {
|
|
||||||
if i, _ := strconv.Atoi(s); i > 0 {
|
if i := core.Atoi(query.Get("duration")); i > 0 {
|
||||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
timeout := time.Second * time.Duration(i)
|
||||||
_ = cons.Stop()
|
var cancel context.CancelFunc
|
||||||
})
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
}
|
defer cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = cons.Stop()
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}()
|
||||||
|
|
||||||
_, _ = cons.WriteTo(w)
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
if duration != nil {
|
|
||||||
duration.Stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-7
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||||
@@ -24,9 +23,8 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mp4.NewConsumer(medias)
|
cons := mp4.NewConsumer(medias)
|
||||||
cons.Type = "MSE/WebSocket active consumer"
|
cons.FormatName = "mse/fmp4"
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
cons.WithRequest(tr.Request)
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Debug().Err(err).Msg("[mp4] add consumer")
|
log.Debug().Err(err).Msg("[mp4] add consumer")
|
||||||
@@ -57,9 +55,7 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mp4.NewKeyframe(medias)
|
cons := mp4.NewKeyframe(medias)
|
||||||
cons.Type = "MP4/WebSocket active consumer"
|
cons.WithRequest(tr.Request)
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -19,11 +17,9 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := aac.NewConsumer()
|
cons := aac.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -32,11 +30,9 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := mpegts.NewConsumer()
|
cons := mpegts.NewConsumer()
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
cons.WithRequest(r)
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("nest", streamNest)
|
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
|
||||||
|
return nest.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/nest", apiNest)
|
api.HandleFunc("api/nest", apiNest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamNest(url string) (core.Producer, error) {
|
|
||||||
client, err := nest.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiNest(w http.ResponseWriter, r *http.Request) {
|
func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
cliendID := query.Get("client_id")
|
cliendID := query.Get("client_id")
|
||||||
|
|||||||
@@ -11,22 +11,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("roborock", handle)
|
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
|
||||||
|
return roborock.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/roborock", apiHandle)
|
api.HandleFunc("api/roborock", apiHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn := roborock.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Connect(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var Auth struct {
|
var Auth struct {
|
||||||
UserData *roborock.UserInfo `json:"user_data"`
|
UserData *roborock.UserInfo `json:"user_data"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
|
|||||||
+3
-10
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,17 +127,13 @@ func tcpHandle(netConn net.Conn) error {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func streamsHandle(url string) (core.Producer, error) {
|
func streamsHandle(url string) (core.Producer, error) {
|
||||||
client, err := rtmp.DialPlay(url)
|
return rtmp.DialPlay(url)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||||
cons := flv.NewConsumer()
|
cons := flv.NewConsumer()
|
||||||
run := func() {
|
run := func() {
|
||||||
wr, err := rtmp.DialPublish(url)
|
wr, err := rtmp.DialPublish(url, cons)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := flv.NewConsumer()
|
cons := flv.NewConsumer()
|
||||||
cons.Type = "HTTP-FLV consumer"
|
cons.WithRequest(r)
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
|||||||
+18
-2
@@ -147,6 +147,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
var closer func()
|
var closer func()
|
||||||
|
|
||||||
trace := log.Trace().Enabled()
|
trace := log.Trace().Enabled()
|
||||||
|
level := zerolog.WarnLevel
|
||||||
|
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
if trace {
|
if trace {
|
||||||
@@ -188,8 +189,18 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
conn.PacketSize = uint16(core.Atoi(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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +221,11 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query := conn.URL.Query()
|
||||||
|
if s := query.Get("timeout"); s != "" {
|
||||||
|
conn.Timeout = core.Atoi(s)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||||
|
|
||||||
stream.AddProducer(conn)
|
stream.AddProducer(conn)
|
||||||
@@ -222,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
|
|
||||||
if err := conn.Accept(); err != nil {
|
if err := conn.Accept(); err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.WithLevel(level).Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
if closer != nil {
|
if closer != nil {
|
||||||
closer()
|
closer()
|
||||||
|
|||||||
@@ -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:
|
producers:
|
||||||
for prodN, prod := range s.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 {
|
if prodErrors[prodN] != nil {
|
||||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||||
continue
|
continue
|
||||||
@@ -129,7 +135,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
|||||||
for _, media := range prodMedias {
|
for _, media := range prodMedias {
|
||||||
if media.Direction == core.DirectionRecvonly {
|
if media.Direction == core.DirectionRecvonly {
|
||||||
for _, codec := range media.Codecs {
|
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 {
|
for _, media := range consMedias {
|
||||||
if media.Direction == core.DirectionSendonly {
|
if media.Direction == core.DirectionSendonly {
|
||||||
for _, codec := range media.Codecs {
|
for _, codec := range media.Codecs {
|
||||||
cons = appendString(cons, codec.PrintName())
|
cons = appendString(cons, media.Kind+":"+codec.PrintName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
src := query.Get("src")
|
||||||
|
|
||||||
|
// without source - return all streams list
|
||||||
|
if src == "" && r.Method != "POST" {
|
||||||
|
api.ResponseJSON(w, streams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure about all this API. Should be rewrited...
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
stream := Get(src)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := probe.NewProbe(query)
|
||||||
|
if len(cons.Medias) != 0 {
|
||||||
|
cons.WithRequest(r)
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponsePrettyJSON(w, stream)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
} else {
|
||||||
|
api.ResponsePrettyJSON(w, streams[src])
|
||||||
|
}
|
||||||
|
|
||||||
|
case "PUT":
|
||||||
|
name := query.Get("name")
|
||||||
|
if name == "" {
|
||||||
|
name = src
|
||||||
|
}
|
||||||
|
|
||||||
|
if New(name, query["src"]...) == nil {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.PatchConfig(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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AppendDOT(dot []byte, stream *Stream) []byte {
|
||||||
|
for _, prod := range stream.producers {
|
||||||
|
if prod.conn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, err := marshalConn(prod.conn)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dot = c.appendDOT(dot, "producer")
|
||||||
|
}
|
||||||
|
for _, cons := range stream.consumers {
|
||||||
|
c, err := marshalConn(cons)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dot = c.appendDOT(dot, "consumer")
|
||||||
|
}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalConn(v any) (*conn, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var c conn
|
||||||
|
if err = json.Unmarshal(b, &c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytesK = "KMGTP"
|
||||||
|
|
||||||
|
func humanBytes(i int) string {
|
||||||
|
if i < 1000 {
|
||||||
|
return fmt.Sprintf("%d B", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := float64(i) / 1000
|
||||||
|
var n uint8
|
||||||
|
for f >= 1000 && n < 5 {
|
||||||
|
f /= 1000
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f %cB", f, bytesK[n])
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Codec map[string]any `json:"codec"`
|
||||||
|
Parent uint32 `json:"parent"`
|
||||||
|
Childs []uint32 `json:"childs"`
|
||||||
|
Bytes int `json:"bytes"`
|
||||||
|
//Packets uint32 `json:"packets"`
|
||||||
|
//Drops uint32 `json:"drops"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"}
|
||||||
|
|
||||||
|
func (n *node) name() string {
|
||||||
|
if name, ok := n.Codec["codec_name"].(string); ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) codec() []byte {
|
||||||
|
b := make([]byte, 0, 128)
|
||||||
|
for _, k := range codecKeys {
|
||||||
|
if v := n.Codec[k]; v != nil {
|
||||||
|
b = fmt.Appendf(b, "%s=%v\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l := len(b); l > 0 {
|
||||||
|
return b[:l-1]
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) appendDOT(dot []byte, group string) []byte {
|
||||||
|
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec())
|
||||||
|
//for _, sink := range n.Childs {
|
||||||
|
// dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink)
|
||||||
|
//}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
type conn struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
FormatName string `json:"format_name"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Receivers []node `json:"receivers"`
|
||||||
|
Senders []node `json:"senders"`
|
||||||
|
BytesRecv int `json:"bytes_recv"`
|
||||||
|
BytesSend int `json:"bytes_send"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||||
|
host := c.host()
|
||||||
|
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
||||||
|
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
|
||||||
|
if group == "producer" {
|
||||||
|
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||||
|
} else {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, recv := range c.Receivers {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes))
|
||||||
|
dot = recv.appendDOT(dot, "node")
|
||||||
|
}
|
||||||
|
for _, send := range c.Senders {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes))
|
||||||
|
//dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes))
|
||||||
|
//dot = send.appendDOT(dot, "node")
|
||||||
|
}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) host() (s string) {
|
||||||
|
if c.Protocol == "pipe" {
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if s = c.RemoteAddr; s == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(s, "forwarded"); i > 0 {
|
||||||
|
s = s[i+10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if s[0] == '[' {
|
||||||
|
if i := strings.Index(s, "]"); i > 0 {
|
||||||
|
return s[1:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.IndexAny(s, " ,:"); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) label() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("format_name=" + c.FormatName)
|
||||||
|
if c.Protocol != "" {
|
||||||
|
sb.WriteString("\nprotocol=" + c.Protocol)
|
||||||
|
}
|
||||||
|
if c.Source != "" {
|
||||||
|
sb.WriteString("\nsource=" + c.Source)
|
||||||
|
}
|
||||||
|
if c.URL != "" {
|
||||||
|
sb.WriteString("\nurl=" + c.URL)
|
||||||
|
}
|
||||||
|
if c.UserAgent != "" {
|
||||||
|
sb.WriteString("\nuser_agent=" + c.UserAgent)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler func(url string) (core.Producer, error)
|
type Handler func(source string) (core.Producer, error)
|
||||||
|
|
||||||
var handlers = map[string]Handler{}
|
var handlers = map[string]Handler{}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,18 +82,20 @@ func (s *Stream) Play(source string) error {
|
|||||||
s.AddInternalProducer(src)
|
s.AddInternalProducer(src)
|
||||||
s.AddInternalConsumer(cons)
|
s.AddInternalConsumer(cons)
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = src.Start()
|
|
||||||
_ = dst.Stop()
|
|
||||||
s.RemoveProducer(src)
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_ = dst.Start()
|
_ = dst.Start()
|
||||||
_ = src.Stop()
|
_ = src.Stop()
|
||||||
s.RemoveInternalConsumer(cons)
|
s.RemoveInternalConsumer(cons)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = src.Start()
|
||||||
|
// little timeout before stop dst, so the buffer can be transferred
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
_ = dst.Stop()
|
||||||
|
s.RemoveProducer(src)
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||||
if p.conn != nil {
|
if conn := p.conn; conn != nil {
|
||||||
return json.Marshal(p.conn)
|
return json.Marshal(conn)
|
||||||
}
|
}
|
||||||
|
info := map[string]string{"url": p.url}
|
||||||
info := core.Info{URL: p.url}
|
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +206,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
for _, media := range conn.GetMedias() {
|
for _, media := range conn.GetMedias() {
|
||||||
switch media.Direction {
|
switch media.Direction {
|
||||||
case core.DirectionRecvonly:
|
case core.DirectionRecvonly:
|
||||||
for _, receiver := range p.receivers {
|
for i, receiver := range p.receivers {
|
||||||
codec := media.MatchCodec(receiver.Codec)
|
codec := media.MatchCodec(receiver.Codec)
|
||||||
if codec == nil {
|
if codec == nil {
|
||||||
continue
|
continue
|
||||||
@@ -219,6 +218,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiver.Replace(track)
|
receiver.Replace(track)
|
||||||
|
p.receivers[i] = track
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +234,9 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop previous connection after moving tracks (fix ghost exec/ffmpeg)
|
||||||
|
_ = p.conn.Stop()
|
||||||
|
// swap connections
|
||||||
p.conn = conn
|
p.conn = conn
|
||||||
|
|
||||||
go p.worker(conn, workerID)
|
go p.worker(conn, workerID)
|
||||||
|
|||||||
+10
-11
@@ -21,6 +21,12 @@ func NewStream(source any) *Stream {
|
|||||||
return &Stream{
|
return &Stream{
|
||||||
producers: []*Producer{NewProducer(source)},
|
producers: []*Producer{NewProducer(source)},
|
||||||
}
|
}
|
||||||
|
case []string:
|
||||||
|
s := new(Stream)
|
||||||
|
for _, str := range source {
|
||||||
|
s.producers = append(s.producers, NewProducer(str))
|
||||||
|
}
|
||||||
|
return s
|
||||||
case []any:
|
case []any:
|
||||||
s := new(Stream)
|
s := new(Stream)
|
||||||
for _, src := range source {
|
for _, src := range source {
|
||||||
@@ -112,19 +118,12 @@ producers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||||
if !s.mu.TryLock() {
|
var info = struct {
|
||||||
log.Warn().Msgf("[streams] json locked")
|
|
||||||
return json.Marshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var info struct {
|
|
||||||
Producers []*Producer `json:"producers"`
|
Producers []*Producer `json:"producers"`
|
||||||
Consumers []core.Consumer `json:"consumers"`
|
Consumers []core.Consumer `json:"consumers"`
|
||||||
|
}{
|
||||||
|
Producers: s.producers,
|
||||||
|
Consumers: s.consumers,
|
||||||
}
|
}
|
||||||
info.Producers = s.producers
|
|
||||||
info.Consumers = s.consumers
|
|
||||||
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-100
@@ -1,7 +1,7 @@
|
|||||||
package streams
|
package streams
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +26,8 @@ func Init() {
|
|||||||
streams[name] = NewStream(item)
|
streams[name] = NewStream(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.HandleFunc("api/streams", streamsHandler)
|
api.HandleFunc("api/streams", apiStreams)
|
||||||
|
api.HandleFunc("api/streams.dot", apiStreamsDOT)
|
||||||
|
|
||||||
if cfg.Publish == nil {
|
if cfg.Publish == nil {
|
||||||
return
|
return
|
||||||
@@ -49,14 +48,26 @@ func Get(name string) *Stream {
|
|||||||
|
|
||||||
var sanitize = regexp.MustCompile(`\s`)
|
var sanitize = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
func New(name string, source string) *Stream {
|
// Validate - not allow creating dynamic streams with spaces in the source
|
||||||
// not allow creating dynamic streams with spaces in the source
|
func Validate(source string) error {
|
||||||
if sanitize.MatchString(source) {
|
if sanitize.MatchString(source) {
|
||||||
return nil
|
return errors.New("streams: invalid dynamic source")
|
||||||
|
}
|
||||||
|
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
|
streams[name] = stream
|
||||||
|
streamsMu.Unlock()
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +100,10 @@ func Patch(name string, source string) *Stream {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if Validate(source) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// check an existing stream with this name
|
// check an existing stream with this name
|
||||||
if stream, ok := streams[name]; ok {
|
if stream, ok := streams[name]; ok {
|
||||||
stream.SetSource(source)
|
stream.SetSource(source)
|
||||||
@@ -96,7 +111,9 @@ func Patch(name string, source string) *Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create new stream with this name
|
// create new stream with this name
|
||||||
return New(name, source)
|
stream := NewStream(source)
|
||||||
|
streams[name] = stream
|
||||||
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOrPatch(query url.Values) *Stream {
|
func GetOrPatch(query url.Values) *Stream {
|
||||||
@@ -137,97 +154,6 @@ func Delete(id string) {
|
|||||||
delete(streams, id)
|
delete(streams, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
query := r.URL.Query()
|
|
||||||
src := query.Get("src")
|
|
||||||
|
|
||||||
// without source - return all streams list
|
|
||||||
if src == "" && r.Method != "POST" {
|
|
||||||
api.ResponseJSON(w, streams)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not sure about all this API. Should be rewrited...
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
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 := stream.Play(src); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
api.ResponseJSON(w, stream)
|
|
||||||
}
|
|
||||||
} else if stream = Get(src); stream != nil {
|
|
||||||
if err := stream.Publish(dst); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, "", http.StatusNotFound)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, "", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
delete(streams, src)
|
|
||||||
|
|
||||||
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var streams = map[string]*Stream{}
|
var streams = map[string]*Stream{}
|
||||||
var streamsMu sync.Mutex
|
var streamsMu sync.Mutex
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
|
streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
|
||||||
return kasa.Dial(url)
|
return kasa.Dial(source)
|
||||||
})
|
})
|
||||||
|
|
||||||
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
|
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
|
||||||
return tapo.Dial(url)
|
return tapo.Dial(source)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package webrtc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
pion "github.com/pion/webrtc/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
@@ -75,14 +75,14 @@ func FilterCandidate(candidate *pion.ICECandidate) bool {
|
|||||||
|
|
||||||
// host candidate should be in the hosts list
|
// host candidate should be in the hosts list
|
||||||
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
||||||
if !slices.Contains(filters.Candidates, candidate.Address) {
|
if !core.Contains(filters.Candidates, candidate.Address) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.Networks != nil {
|
if filters.Networks != nil {
|
||||||
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
|
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
|
||||||
if !slices.Contains(filters.Networks, networkType) {
|
if !core.Contains(filters.Networks, networkType) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
// https://aws.amazon.com/kinesis/video-streams/
|
// https://aws.amazon.com/kinesis/video-streams/
|
||||||
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
||||||
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
||||||
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
|
return kinesisClient(rawURL, query, "webrtc/kinesis")
|
||||||
} else if format == "openipc" {
|
} else if format == "openipc" {
|
||||||
return openIPCClient(rawURL, query)
|
return openIPCClient(rawURL, query)
|
||||||
} else {
|
} else {
|
||||||
@@ -77,17 +77,23 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
// 2. Create PeerConnection
|
// 2. Create PeerConnection
|
||||||
pc, err := PeerConnection(true)
|
pc, err := PeerConnection(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
_ = pc.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// waiter will wait PC error or WS error or nil (connection OK)
|
// waiter will wait PC error or WS error or nil (connection OK)
|
||||||
var connState core.Waiter
|
var connState core.Waiter
|
||||||
var connMu sync.Mutex
|
var connMu sync.Mutex
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WebSocket async"
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "ws"
|
||||||
|
prod.URL = url
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
@@ -132,7 +138,8 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if msg.Type != "webrtc/answer" {
|
if msg.Type != "webrtc/answer" {
|
||||||
return nil, errors.New("wrong answer: " + msg.Type)
|
err = errors.New("wrong answer: " + msg.String())
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
answer := msg.String()
|
answer := msg.String()
|
||||||
@@ -180,8 +187,9 @@ func whepClient(url string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WHEP sync"
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
|
prod.URL = url
|
||||||
|
|
||||||
medias := []*core.Media{
|
medias := []*core.Media{
|
||||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (k kinesisResponse) String() string {
|
|||||||
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
|
func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) {
|
||||||
// 1. Connect to signalign server
|
// 1. Connect to signalign server
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,8 +79,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = desc
|
prod.FormatName = format
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "ws"
|
||||||
|
prod.URL = rawURL
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
@@ -216,5 +218,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
|
|||||||
"ice_servers": []string{string(kvs.Servers)},
|
"ice_servers": []string{string(kvs.Servers)},
|
||||||
}
|
}
|
||||||
|
|
||||||
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
|
return kinesisClient(kvs.URL, query, "webrtc/wyze")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/Milestone"
|
prod.FormatName = "webrtc/milestone"
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
|
prod.URL = rawURL
|
||||||
|
|
||||||
offer, err := mc.GetOffer()
|
offer, err := mc.GetOffer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
|||||||
var connState core.Waiter
|
var connState core.Waiter
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/OpenIPC"
|
prod.FormatName = "webrtc/openipc"
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "ws"
|
||||||
|
prod.URL = rawURL
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
url := r.URL.Query().Get("src")
|
url := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(url)
|
stream := streams.Get(url)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +101,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case "application/json":
|
case "application/json":
|
||||||
desc = "WebRTC/JSON sync"
|
desc = "webrtc/json"
|
||||||
case MimeSDP:
|
case MimeSDP:
|
||||||
desc = "WebRTC/WHEP sync"
|
desc = "webrtc/whep"
|
||||||
default:
|
default:
|
||||||
desc = "WebRTC/HTTP sync"
|
desc = "webrtc/post"
|
||||||
}
|
}
|
||||||
|
|
||||||
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
||||||
@@ -168,8 +169,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// create new webrtc instance
|
// create new webrtc instance
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WHIP sync"
|
|
||||||
prod.Mode = core.ModePassiveProducer
|
prod.Mode = core.ModePassiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
prod.UserAgent = r.UserAgent()
|
prod.UserAgent = r.UserAgent()
|
||||||
|
|
||||||
if err = prod.SetOffer(string(offer)); err != nil {
|
if err = prod.SetOffer(string(offer)); err != nil {
|
||||||
|
|||||||
+35
-18
@@ -40,15 +40,17 @@ func Init() {
|
|||||||
AddCandidate(network, candidate)
|
AddCandidate(network, candidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
// create pionAPI with custom codecs list and custom network settings
|
// 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 {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// use same API for WebRTC server and client if no address
|
// use same API for WebRTC server and client if no address
|
||||||
clientAPI := serverAPI
|
clientAPI = serverAPI
|
||||||
|
|
||||||
if address != "" {
|
if address != "" {
|
||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
||||||
@@ -81,11 +83,13 @@ func Init() {
|
|||||||
streams.HandleFunc("webrtc", streamsHandler)
|
streams.HandleFunc("webrtc", streamsHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serverAPI, clientAPI *pion.API
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
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 stream *streams.Stream
|
||||||
var mode core.Mode
|
var mode core.Mode
|
||||||
|
|
||||||
@@ -104,8 +108,30 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
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
|
// 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 {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return err
|
return err
|
||||||
@@ -117,8 +143,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
defer sendAnswer.Done(nil)
|
defer sendAnswer.Done(nil)
|
||||||
|
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = "WebRTC/WebSocket async"
|
|
||||||
conn.Mode = mode
|
conn.Mode = mode
|
||||||
|
conn.Protocol = "ws"
|
||||||
conn.UserAgent = tr.Request.UserAgent()
|
conn.UserAgent = tr.Request.UserAgent()
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -145,20 +171,10 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// V2 - json/object exchange, V1 - raw SDP exchange
|
log.Trace().Msgf("[webrtc] offer:\n%s", offer.SDP)
|
||||||
apiV2 := msg.Type == "webrtc"
|
|
||||||
|
|
||||||
// 1. SetOffer, so we can get remote client codecs
|
// 1. SetOffer, so we can get remote client codecs
|
||||||
var offer string
|
if err = conn.SetOffer(offer.SDP); err != nil {
|
||||||
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 {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.Warn().Err(err).Caller().Send()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -207,8 +223,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
|||||||
|
|
||||||
// create new webrtc instance
|
// create new webrtc instance
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = desc
|
conn.FormatName = desc
|
||||||
conn.UserAgent = userAgent
|
conn.UserAgent = userAgent
|
||||||
|
conn.Protocol = "http"
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case pion.PeerConnectionState:
|
case pion.PeerConnectionState:
|
||||||
|
|||||||
@@ -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 {
|
if stream == nil {
|
||||||
return "", errors.New(api.StreamNotFound)
|
return "", errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
|
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
app.Version = "1.9.7"
|
||||||
|
|
||||||
// 1. Core modules: app, api/ws, streams
|
// 1. Core modules: app, api/ws, streams
|
||||||
|
|
||||||
app.Init() // init config and logs
|
app.Init() // init config and logs
|
||||||
|
|||||||
@@ -1,3 +1,85 @@
|
|||||||
|
# Notes
|
||||||
|
|
||||||
|
go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.
|
||||||
|
Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.
|
||||||
|
|
||||||
|
## Producers (input)
|
||||||
|
|
||||||
|
- The initiator of the connection can be go2rtc - **Source protocols**
|
||||||
|
- The initiator of the connection can be an external program - **Ingress protocols**
|
||||||
|
- Codecs can be incoming - **Recevers codecs**
|
||||||
|
- Codecs can be outgoing (two way audio) - **Senders codecs**
|
||||||
|
|
||||||
|
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|
||||||
|
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
|
||||||
|
| adts | http,tcp,pipe | http | aac | | `http:` |
|
||||||
|
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
|
||||||
|
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
|
||||||
|
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
|
||||||
|
| gopro | http+udp | | TODO | | `gopro:` |
|
||||||
|
| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` |
|
||||||
|
| hls/mpegts | http | | h264,h265,aac,opus | | `http:` |
|
||||||
|
| homekit | homekit+udp | | h264,eld* | | `homekit:` |
|
||||||
|
| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` |
|
||||||
|
| ivideon | ws | | h264 | | `ivideon:` |
|
||||||
|
| kasa | http | | h264,pcm_mulaw | | `kasa:` |
|
||||||
|
| h264 | http,tcp,pipe | http | h264 | | `http:` |
|
||||||
|
| hevc | http,tcp,pipe | http | hevc | | `http:` |
|
||||||
|
| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||||
|
| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||||
|
| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` |
|
||||||
|
| nest/webrtc | http+udp | | TODO | | `nest:` |
|
||||||
|
| roborock | mqtt+udp | | h264,opus | opus | `roborock:` |
|
||||||
|
| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` |
|
||||||
|
| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` |
|
||||||
|
| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` |
|
||||||
|
| tapo | http | | h264,pcma | pcm_alaw | `tapo:` |
|
||||||
|
| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` |
|
||||||
|
| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` |
|
||||||
|
| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` |
|
||||||
|
| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` |
|
||||||
|
|
||||||
|
- **eld** - rare variant of aac codec
|
||||||
|
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||||
|
- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep
|
||||||
|
|
||||||
|
## Consumers (output)
|
||||||
|
|
||||||
|
| Format | Protocol | Send codecs | Recv codecs | Example |
|
||||||
|
|--------------|-------------|------------------------------|-------------------------|---------------------------------------|
|
||||||
|
| adts | http | aac | | `GET /api/stream.adts` |
|
||||||
|
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
|
||||||
|
| flv | http | h264,aac | | `GET /api/stream.flv` |
|
||||||
|
| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` |
|
||||||
|
| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` |
|
||||||
|
| homekit | homekit+udp | h264,opus | | Apple HomeKit app |
|
||||||
|
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
|
||||||
|
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
|
||||||
|
| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` |
|
||||||
|
| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` |
|
||||||
|
| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` |
|
||||||
|
| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` |
|
||||||
|
| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` |
|
||||||
|
| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` |
|
||||||
|
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
|
||||||
|
|
||||||
|
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||||
|
|
||||||
|
## Snapshots
|
||||||
|
|
||||||
|
| Format | Protocol | Send codecs | Example |
|
||||||
|
|--------|----------|-------------|-----------------------|
|
||||||
|
| jpeg | http | mjpeg | `GET /api/frame.jpeg` |
|
||||||
|
| mp4 | http | h264,hevc | `GET /api/frame.mp4` |
|
||||||
|
|
||||||
|
## Developers
|
||||||
|
|
||||||
|
File naming:
|
||||||
|
|
||||||
|
- `pkg/{format}/producer.go` - producer for this format (also if support backchannel)
|
||||||
|
- `pkg/{format}/consumer.go` - consumer for this format
|
||||||
|
- `pkg/{format}/backchanel.go` - producer with only backchannel func
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- https://www.wowza.com/blog/streaming-protocols
|
- https://www.wowza.com/blog/streaming-protocols
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate u
|
|||||||
sampleFreqIdx = rd.ReadBits8(4)
|
sampleFreqIdx = rd.ReadBits8(4)
|
||||||
if sampleFreqIdx == 0b1111 {
|
if sampleFreqIdx == 0b1111 {
|
||||||
sampleRate = rd.ReadBits(24)
|
sampleRate = rd.ReadBits(24)
|
||||||
|
} else {
|
||||||
|
sampleRate = sampleRates[sampleFreqIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
channels = rd.ReadBits8(4)
|
channels = rd.ReadBits8(4)
|
||||||
|
|||||||
@@ -41,3 +41,12 @@ func TestADTS(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, src[:len(dst)], dst)
|
require.Equal(t, src[:len(dst)], dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodeConfig(t *testing.T) {
|
||||||
|
conf := EncodeConfig(TypeAACLC, 48000, 1, false)
|
||||||
|
require.Equal(t, "1188", hex.EncodeToString(conf))
|
||||||
|
conf = EncodeConfig(TypeAACLC, 16000, 1, false)
|
||||||
|
require.Equal(t, "1408", hex.EncodeToString(conf))
|
||||||
|
conf = EncodeConfig(TypeAACLC, 8000, 1, false)
|
||||||
|
require.Equal(t, "1588", hex.EncodeToString(conf))
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func IsADTS(b []byte) bool {
|
func IsADTS(b []byte) bool {
|
||||||
_ = b[1]
|
|
||||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-11
@@ -8,15 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
wr *core.WriteBuffer
|
wr *core.WriteBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumer() *Consumer {
|
func NewConsumer() *Consumer {
|
||||||
cons := &Consumer{
|
medias := []*core.Media{
|
||||||
wr: core.NewWriteBuffer(nil),
|
|
||||||
}
|
|
||||||
cons.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionSendonly,
|
Direction: core.DirectionSendonly,
|
||||||
@@ -25,7 +22,16 @@ func NewConsumer() *Consumer {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cons
|
wr := core.NewWriteBuffer(nil)
|
||||||
|
return &Consumer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "adts",
|
||||||
|
Medias: medias,
|
||||||
|
Transport: wr,
|
||||||
|
},
|
||||||
|
wr: wr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
@@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
|||||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||||
return c.wr.WriteTo(wr)
|
return c.wr.WriteTo(wr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Stop() error {
|
|
||||||
_ = c.SuperConsumer.Close()
|
|
||||||
return c.wr.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-14
@@ -10,9 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Producer struct {
|
type Producer struct {
|
||||||
core.SuperProducer
|
core.Connection
|
||||||
rd *bufio.Reader
|
rd *bufio.Reader
|
||||||
cl io.Closer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(r io.Reader) (*Producer, error) {
|
func Open(r io.Reader) (*Producer, error) {
|
||||||
@@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
codec := ADTSToCodec(b)
|
medias := []*core.Media{
|
||||||
|
|
||||||
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
|
||||||
prod.Type = "ADTS producer"
|
|
||||||
prod.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionRecvonly,
|
Direction: core.DirectionRecvonly,
|
||||||
Codecs: []*core.Codec{codec},
|
Codecs: []*core.Codec{ADTSToCodec(b)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return prod, nil
|
return &Producer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "adts",
|
||||||
|
Medias: medias,
|
||||||
|
Transport: r,
|
||||||
|
},
|
||||||
|
rd: rd,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Producer) Start() error {
|
func (c *Producer) Start() error {
|
||||||
@@ -66,8 +69,3 @@ func (c *Producer) Start() error {
|
|||||||
c.Receivers[0].WriteRTP(pkt)
|
c.Receivers[0].WriteRTP(pkt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Producer) Stop() error {
|
|
||||||
_ = c.SuperProducer.Close()
|
|
||||||
return c.cl.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
//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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuggy_RTSP_AAC(t *testing.T) {
|
||||||
|
// https: //github.com/AlexxIT/go2rtc/issues/1328
|
||||||
|
payload, _ := hex.DecodeString("fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0")
|
||||||
|
packet := &rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
SequenceNumber: 36944,
|
||||||
|
Timestamp: 4217191328,
|
||||||
|
SSRC: 12892774,
|
||||||
|
},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int
|
||||||
|
|
||||||
|
RTPDepay(func(packet *core.Packet) {
|
||||||
|
size = len(packet.Payload)
|
||||||
|
})(packet)
|
||||||
|
|
||||||
|
require.Equal(t, len(payload), size+ADTSHeaderSize)
|
||||||
|
}
|
||||||
+8
-1
@@ -156,7 +156,7 @@ const x256b = "\x00\x00\x00\x00\x80\x80\x80\xc0\x80\x00\x00\x00\xff\xff\xff\xff\
|
|||||||
func xterm256color(r, g, b uint8, n int) (index uint8) {
|
func xterm256color(r, g, b uint8, n int) (index uint8) {
|
||||||
best := uint16(0xFFFF)
|
best := uint16(0xFFFF)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
diff := uint16(r-x256r[i]) + uint16(g-x256g[i]) + uint16(b-x256b[i])
|
diff := sqDiff(r, x256r[i]) + sqDiff(g, x256g[i]) + sqDiff(b, x256b[i])
|
||||||
if diff < best {
|
if diff < best {
|
||||||
best = diff
|
best = diff
|
||||||
index = uint8(i)
|
index = uint8(i)
|
||||||
@@ -164,3 +164,10 @@ func xterm256color(r, g, b uint8, n int) (index uint8) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sqDiff - just like from image/color/color.go
|
||||||
|
func sqDiff(x, y uint8) uint16 {
|
||||||
|
d := uint16(x - y)
|
||||||
|
//return d
|
||||||
|
return (d * d) >> 2
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,3 +131,7 @@ func (r *Reader) ReadSEGolomb() int32 {
|
|||||||
func (r *Reader) Left() []byte {
|
func (r *Reader) Left() []byte {
|
||||||
return r.buf[r.pos:]
|
return r.buf[r.pos:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Pos() (int, byte) {
|
||||||
|
return r.pos - 1, r.bits
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated: should be rewritten to core.Connection
|
||||||
type Client struct {
|
type Client struct {
|
||||||
core.Listener
|
core.Listener
|
||||||
|
|
||||||
@@ -43,8 +44,12 @@ type Client struct {
|
|||||||
recv int
|
recv int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(url string) *Client {
|
func Dial(rawURL string) (*Client, error) {
|
||||||
return &Client{url: url}
|
client := &Client{url: rawURL}
|
||||||
|
if err := client.Dial(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -226,7 +231,7 @@ func (c *Client) Handle() error {
|
|||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Timestamp: core.Now90000(),
|
Timestamp: core.Now90000(),
|
||||||
},
|
},
|
||||||
Payload: annexb.EncodeToAVCC(b[6:], false),
|
Payload: annexb.EncodeToAVCC(b[6:]),
|
||||||
}
|
}
|
||||||
c.videoTrack.WriteRTP(pkt)
|
c.videoTrack.WriteRTP(pkt)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+10
-5
@@ -65,11 +65,16 @@ func (c *Client) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
info := &core.Info{
|
info := &core.Connection{
|
||||||
Type: "Bubble active producer",
|
ID: core.ID(c),
|
||||||
Medias: c.medias,
|
FormatName: "bubble",
|
||||||
Recv: c.recv,
|
Protocol: "http",
|
||||||
Receivers: c.receivers,
|
Medias: c.medias,
|
||||||
|
Recv: c.recv,
|
||||||
|
Receivers: c.receivers,
|
||||||
|
}
|
||||||
|
if c.conn != nil {
|
||||||
|
info.RemoteAddr = c.conn.RemoteAddr().String()
|
||||||
}
|
}
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-21
@@ -2,8 +2,8 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -18,34 +18,76 @@ type Codec struct {
|
|||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) String() string {
|
// MarshalJSON - return FFprobe compatible output
|
||||||
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
|
func (c *Codec) MarshalJSON() ([]byte, error) {
|
||||||
|
info := map[string]any{}
|
||||||
|
if name := FFmpegCodecName(c.Name); name != "" {
|
||||||
|
info["codec_name"] = name
|
||||||
|
info["codec_type"] = c.Kind()
|
||||||
|
}
|
||||||
|
if c.Name == CodecH264 {
|
||||||
|
profile, level := DecodeH264(c.FmtpLine)
|
||||||
|
if profile != "" {
|
||||||
|
info["profile"] = profile
|
||||||
|
info["level"] = level
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||||
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
|
info["sample_rate"] = c.ClockRate
|
||||||
}
|
}
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
info["channels"] = c.Channels
|
||||||
}
|
}
|
||||||
return s
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Text() string {
|
func FFmpegCodecName(name string) string {
|
||||||
switch c.Name {
|
switch name {
|
||||||
case CodecH264:
|
case CodecH264:
|
||||||
if profile := DecodeH264(c.FmtpLine); profile != "" {
|
return "h264"
|
||||||
return "H.264 " + profile
|
case CodecH265:
|
||||||
}
|
return "hevc"
|
||||||
return c.Name
|
case CodecJPEG:
|
||||||
|
return "mjpeg"
|
||||||
|
case CodecRAW:
|
||||||
|
return "rawvideo"
|
||||||
|
case CodecPCMA:
|
||||||
|
return "pcm_alaw"
|
||||||
|
case CodecPCMU:
|
||||||
|
return "pcm_mulaw"
|
||||||
|
case CodecPCM:
|
||||||
|
return "pcm_s16be"
|
||||||
|
case CodecPCML:
|
||||||
|
return "pcm_s16le"
|
||||||
|
case CodecAAC:
|
||||||
|
return "aac"
|
||||||
|
case CodecOpus:
|
||||||
|
return "opus"
|
||||||
|
case CodecVP8:
|
||||||
|
return "vp8"
|
||||||
|
case CodecVP9:
|
||||||
|
return "vp9"
|
||||||
|
case CodecAV1:
|
||||||
|
return "av1"
|
||||||
|
case CodecELD:
|
||||||
|
return "aac/eld"
|
||||||
|
case CodecFLAC:
|
||||||
|
return "flac"
|
||||||
|
case CodecMP3:
|
||||||
|
return "mp3"
|
||||||
}
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
s := c.Name
|
func (c *Codec) String() (s string) {
|
||||||
|
s = c.Name
|
||||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||||
s += "/" + strconv.Itoa(int(c.ClockRate))
|
s += fmt.Sprintf("/%d", c.ClockRate)
|
||||||
}
|
}
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
s += "/" + strconv.Itoa(int(c.Channels))
|
s += fmt.Sprintf("/%d", c.Channels)
|
||||||
}
|
}
|
||||||
return s
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) IsRTP() bool {
|
func (c *Codec) IsRTP() bool {
|
||||||
@@ -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
|
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||||
switch payloadType {
|
switch payloadType {
|
||||||
case "0":
|
case "0":
|
||||||
@@ -181,10 +228,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeH264(fmtp string) string {
|
func DecodeH264(fmtp string) (profile string, level byte) {
|
||||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||||
var profile string
|
|
||||||
switch sps[1] {
|
switch sps[1] {
|
||||||
case 0x42:
|
case 0x42:
|
||||||
profile = "Baseline"
|
profile = "Baseline"
|
||||||
@@ -198,8 +244,8 @@ func DecodeH264(fmtp string) string {
|
|||||||
profile = fmt.Sprintf("0x%02X", sps[1])
|
profile = fmt.Sprintf("0x%02X", sps[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10)
|
level = sps[3]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,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
|
||||||
|
}
|
||||||
+5
-85
@@ -1,5 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DirectionRecvonly = "recvonly"
|
DirectionRecvonly = "recvonly"
|
||||||
DirectionSendonly = "sendonly"
|
DirectionSendonly = "sendonly"
|
||||||
@@ -18,6 +20,7 @@ const (
|
|||||||
CodecVP9 = "VP9"
|
CodecVP9 = "VP9"
|
||||||
CodecAV1 = "AV1"
|
CodecAV1 = "AV1"
|
||||||
CodecJPEG = "JPEG" // payloadType: 26
|
CodecJPEG = "JPEG" // payloadType: 26
|
||||||
|
CodecRAW = "RAW"
|
||||||
|
|
||||||
CodecPCMU = "PCMU" // payloadType: 0
|
CodecPCMU = "PCMU" // payloadType: 0
|
||||||
CodecPCMA = "PCMA" // payloadType: 8
|
CodecPCMA = "PCMA" // payloadType: 8
|
||||||
@@ -89,89 +92,6 @@ func (m Mode) String() string {
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
func (m Mode) MarshalJSON() ([]byte, error) {
|
||||||
Type string `json:"type,omitempty"`
|
return json.Marshal(m.String())
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
|
||||||
UserAgent string `json:"user_agent,omitempty"`
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
|
||||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
|
||||||
Senders []*Sender `json:"senders,omitempty"`
|
|
||||||
Recv int `json:"recv,omitempty"`
|
|
||||||
Send int `json:"send,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnsupportedCodec = "unsupported codec"
|
|
||||||
WrongMediaDirection = "wrong media direction"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SuperProducer struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
|
||||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
|
||||||
Recv int `json:"recv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperProducer) GetMedias() []*Media {
|
|
||||||
return s.Medias
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
|
||||||
for _, receiver := range s.Receivers {
|
|
||||||
if receiver.Codec == codec {
|
|
||||||
return receiver, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
receiver := NewReceiver(media, codec)
|
|
||||||
s.Receivers = append(s.Receivers, receiver)
|
|
||||||
return receiver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperProducer) Close() error {
|
|
||||||
for _, receiver := range s.Receivers {
|
|
||||||
receiver.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SuperConsumer struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
|
||||||
UserAgent string `json:"user_agent,omitempty"`
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
|
||||||
Senders []*Sender `json:"senders,omitempty"`
|
|
||||||
Send int `json:"send,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) GetMedias() []*Media {
|
|
||||||
return s.Medias
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) {
|
|
||||||
// return 0, nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) Close() error {
|
|
||||||
for _, sender := range s.Senders {
|
|
||||||
sender.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) Codecs() []*Codec {
|
|
||||||
codecs := make([]*Codec, len(s.Senders))
|
|
||||||
for i, sender := range s.Senders {
|
|
||||||
codecs[i] = sender.Codec
|
|
||||||
}
|
|
||||||
return codecs
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type producer struct {
|
||||||
|
Medias []*Media
|
||||||
|
Receivers []*Receiver
|
||||||
|
|
||||||
|
id byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) GetMedias() []*Media {
|
||||||
|
return p.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) {
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
if receiver.Codec == codec {
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiver := NewReceiver(nil, codec)
|
||||||
|
p.Receivers = append(p.Receivers, receiver)
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) Start() error {
|
||||||
|
pkt := &Packet{Payload: []byte{p.id}}
|
||||||
|
p.Receivers[0].Input(pkt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) Stop() error {
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
receiver.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type consumer struct {
|
||||||
|
Medias []*Media
|
||||||
|
Senders []*Sender
|
||||||
|
|
||||||
|
cache chan byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) GetMedias() []*Media {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error {
|
||||||
|
c.cache = make(chan byte, 1)
|
||||||
|
sender := NewSender(nil, track.Codec)
|
||||||
|
sender.Output = func(packet *Packet) {
|
||||||
|
c.cache <- packet.Payload[0]
|
||||||
|
}
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
c.Senders = append(c.Senders, sender)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) Stop() error {
|
||||||
|
for _, sender := range c.Senders {
|
||||||
|
sender.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) read() byte {
|
||||||
|
return <-c.cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestName(t *testing.T) {
|
||||||
|
GetProducer := func(b byte) Producer {
|
||||||
|
return &producer{
|
||||||
|
Medias: []*Media{
|
||||||
|
{
|
||||||
|
Kind: KindVideo,
|
||||||
|
Direction: DirectionRecvonly,
|
||||||
|
Codecs: []*Codec{
|
||||||
|
{Name: CodecH264},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage1
|
||||||
|
prod1 := GetProducer(1)
|
||||||
|
cons2 := &consumer{}
|
||||||
|
|
||||||
|
media1 := prod1.GetMedias()[0]
|
||||||
|
track1, _ := prod1.GetTrack(media1, media1.Codecs[0])
|
||||||
|
|
||||||
|
_ = cons2.AddTrack(nil, nil, track1)
|
||||||
|
|
||||||
|
_ = prod1.Start()
|
||||||
|
require.Equal(t, byte(1), cons2.read())
|
||||||
|
|
||||||
|
// stage2
|
||||||
|
prod2 := GetProducer(2)
|
||||||
|
media2 := prod2.GetMedias()[0]
|
||||||
|
require.NotEqual(t, fmt.Sprintf("%p", media1), fmt.Sprintf("%p", media2))
|
||||||
|
track2, _ := prod2.GetTrack(media2, media2.Codecs[0])
|
||||||
|
track1.Replace(track2)
|
||||||
|
|
||||||
|
_ = prod1.Stop()
|
||||||
|
|
||||||
|
_ = prod2.Start()
|
||||||
|
require.Equal(t, byte(2), cons2.read())
|
||||||
|
|
||||||
|
// stage3
|
||||||
|
_ = prod2.Stop()
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ func RandString(size, base byte) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Before(s, sep string) string {
|
||||||
|
if i := strings.Index(s, sep); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func Between(s, sub1, sub2 string) string {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|||||||
+8
-4
@@ -22,7 +22,7 @@ type Media struct {
|
|||||||
func (m *Media) String() string {
|
func (m *Media) String() string {
|
||||||
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
||||||
for _, codec := range m.Codecs {
|
for _, codec := range m.Codecs {
|
||||||
name := codec.Text()
|
name := codec.String()
|
||||||
|
|
||||||
if strings.Contains(s, name) {
|
if strings.Contains(s, name) {
|
||||||
continue
|
continue
|
||||||
@@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool {
|
|||||||
|
|
||||||
func GetKind(name string) string {
|
func GetKind(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:
|
||||||
return KindVideo
|
return KindVideo
|
||||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
@@ -124,9 +124,13 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
|||||||
|
|
||||||
codec := media.Codecs[0]
|
codec := media.Codecs[0]
|
||||||
|
|
||||||
name := codec.Name
|
switch codec.Name {
|
||||||
if name == CodecELD {
|
case CodecELD:
|
||||||
name = CodecAAC
|
name = CodecAAC
|
||||||
|
case CodecPCML:
|
||||||
|
name = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server
|
||||||
|
default:
|
||||||
|
name = codec.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
md := &sdp.MediaDescription{
|
md := &sdp.MediaDescription{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+149
-160
@@ -3,223 +3,212 @@ package core
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Packet struct {
|
|
||||||
PayloadType uint8
|
|
||||||
Sequence uint16
|
|
||||||
Timestamp uint32 // PTS if DTS == 0 else DTS
|
|
||||||
Composition uint32 // CTS = PTS-DTS (for support B-frames)
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrCantGetTrack = errors.New("can't get track")
|
var ErrCantGetTrack = errors.New("can't get track")
|
||||||
|
|
||||||
type Receiver struct {
|
type Receiver struct {
|
||||||
Codec *Codec
|
Node
|
||||||
Media *Media
|
|
||||||
|
|
||||||
ID byte // Channel for RTSP, PayloadType for MPEG-TS
|
// Deprecated: should be removed
|
||||||
|
Media *Media `json:"-"`
|
||||||
|
// Deprecated: should be removed
|
||||||
|
ID byte `json:"-"` // Channel for RTSP, PayloadType for MPEG-TS
|
||||||
|
|
||||||
senders map[*Sender]chan *rtp.Packet
|
Bytes int `json:"bytes,omitempty"`
|
||||||
mu sync.RWMutex
|
Packets int `json:"packets,omitempty"`
|
||||||
bytes int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
||||||
Assert(codec != nil)
|
r := &Receiver{
|
||||||
return &Receiver{Codec: codec, Media: media}
|
Node: Node{id: NewID(), Codec: codec},
|
||||||
}
|
Media: media,
|
||||||
|
}
|
||||||
// WriteRTP - fast and non blocking write to all readers buffers
|
r.Input = func(packet *Packet) {
|
||||||
func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
r.Bytes += len(packet.Payload)
|
||||||
t.mu.Lock()
|
r.Packets++
|
||||||
t.bytes += len(packet.Payload)
|
for _, child := range r.childs {
|
||||||
for sender, buffer := range t.senders {
|
child.Input(packet)
|
||||||
select {
|
|
||||||
case buffer <- packet:
|
|
||||||
default:
|
|
||||||
sender.overflow++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.mu.Unlock()
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Receiver) Senders() (senders []*Sender) {
|
// Deprecated: should be removed
|
||||||
t.mu.RLock()
|
func (r *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||||
for sender := range t.senders {
|
r.Input(packet)
|
||||||
senders = append(senders, sender)
|
}
|
||||||
|
|
||||||
|
// Deprecated: should be removed
|
||||||
|
func (r *Receiver) Senders() []*Sender {
|
||||||
|
if len(r.childs) > 0 {
|
||||||
|
return []*Sender{{}}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
t.mu.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Receiver) Close() {
|
// Deprecated: should be removed
|
||||||
t.mu.Lock()
|
func (r *Receiver) Replace(target *Receiver) {
|
||||||
// close all sender channel buffers and erase senders list
|
MoveNode(&target.Node, &r.Node)
|
||||||
for _, buffer := range t.senders {
|
|
||||||
close(buffer)
|
|
||||||
}
|
|
||||||
t.senders = nil
|
|
||||||
t.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Receiver) Replace(target *Receiver) {
|
func (r *Receiver) Close() {
|
||||||
// move this receiver senders to new receiver
|
r.Node.Close()
|
||||||
t.mu.Lock()
|
|
||||||
senders := t.senders
|
|
||||||
t.mu.Unlock()
|
|
||||||
|
|
||||||
target.mu.Lock()
|
|
||||||
target.senders = senders
|
|
||||||
target.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Receiver) String() string {
|
|
||||||
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
|
|
||||||
t.mu.RLock()
|
|
||||||
s += fmt.Sprintf(", senders=%d", len(t.senders))
|
|
||||||
t.mu.RUnlock()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Receiver) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(t.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sender struct {
|
type Sender struct {
|
||||||
Codec *Codec
|
Node
|
||||||
Media *Media
|
|
||||||
|
|
||||||
Handler HandlerFunc
|
// Deprecated:
|
||||||
|
Media *Media `json:"-"`
|
||||||
|
// Deprecated:
|
||||||
|
Handler HandlerFunc `json:"-"`
|
||||||
|
|
||||||
receivers []*Receiver
|
Bytes int `json:"bytes,omitempty"`
|
||||||
mu sync.RWMutex
|
Packets int `json:"packets,omitempty"`
|
||||||
bytes int
|
Drops int `json:"drops,omitempty"`
|
||||||
|
|
||||||
overflow int
|
buf chan *Packet
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(media *Media, codec *Codec) *Sender {
|
func NewSender(media *Media, codec *Codec) *Sender {
|
||||||
return &Sender{Codec: codec, Media: media}
|
var bufSize uint16
|
||||||
}
|
|
||||||
|
|
||||||
// HandlerFunc like http.HandlerFunc
|
if GetKind(codec.Name) == KindVideo {
|
||||||
type HandlerFunc func(packet *rtp.Packet)
|
if codec.IsRTP() {
|
||||||
|
|
||||||
func (s *Sender) HandleRTP(track *Receiver) {
|
|
||||||
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() {
|
|
||||||
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
||||||
// for the h264.RTPDepay => RTPPay queue
|
// for the h264.RTPDepay => RTPPay queue
|
||||||
bufferSize = 5000
|
bufSize = 4096
|
||||||
} else {
|
} else {
|
||||||
bufferSize = 50
|
bufSize = 64
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bufferSize = 100
|
bufSize = 128
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make(chan *rtp.Packet, bufferSize)
|
buf := make(chan *Packet, bufSize)
|
||||||
|
s := &Sender{
|
||||||
track.mu.Lock()
|
Node: Node{id: NewID(), Codec: codec},
|
||||||
if track.senders == nil {
|
Media: media,
|
||||||
track.senders = map[*Sender]chan *rtp.Packet{}
|
buf: buf,
|
||||||
}
|
}
|
||||||
track.senders[s] = buffer
|
s.Input = func(packet *Packet) {
|
||||||
track.mu.Unlock()
|
// writing to nil chan - OK, writing to closed chan - panic
|
||||||
|
s.mu.Lock()
|
||||||
s.mu.Lock()
|
select {
|
||||||
s.receivers = append(s.receivers, track)
|
case s.buf <- packet:
|
||||||
s.mu.Unlock()
|
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) {
|
// Deprecated: should be removed
|
||||||
track.mu.Lock()
|
func (s *Sender) HandleRTP(parent *Receiver) {
|
||||||
buffer := track.senders[s]
|
s.WithParent(parent)
|
||||||
track.mu.Unlock()
|
s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
// read packets from buffer channel until it will be closed
|
// Deprecated: should be removed
|
||||||
if buffer != nil {
|
func (s *Sender) Bind(parent *Receiver) {
|
||||||
for packet := range buffer {
|
s.WithParent(parent)
|
||||||
s.bytes += len(packet.Payload)
|
}
|
||||||
s.Handler(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove current receiver from list
|
func (s *Sender) WithParent(parent *Receiver) *Sender {
|
||||||
// it can only happen when receiver close buffer channel
|
s.Node.WithParent(&parent.Node)
|
||||||
s.mu.Lock()
|
return s
|
||||||
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) Start() {
|
func (s *Sender) Start() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
for _, track := range s.receivers {
|
defer s.mu.Unlock()
|
||||||
go s.worker(track)
|
|
||||||
|
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() {
|
func (s *Sender) Close() {
|
||||||
s.mu.Lock()
|
// close buffer if exists
|
||||||
// remove this sender from all receivers list
|
if buf := s.buf; buf != nil {
|
||||||
for _, receiver := range s.receivers {
|
s.buf = nil
|
||||||
receiver.mu.Lock()
|
defer close(buf)
|
||||||
if buffer := receiver.senders[s]; buffer != nil {
|
|
||||||
// remove channel from list
|
|
||||||
delete(receiver.senders, s)
|
|
||||||
// close channel
|
|
||||||
close(buffer)
|
|
||||||
}
|
|
||||||
receiver.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
s.receivers = nil
|
|
||||||
s.mu.Unlock()
|
s.Node.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) String() string {
|
func (r *Receiver) MarshalJSON() ([]byte, error) {
|
||||||
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
|
v := struct {
|
||||||
s.mu.RLock()
|
ID uint32 `json:"id"`
|
||||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
Codec *Codec `json:"codec"`
|
||||||
s.mu.RUnlock()
|
Childs []uint32 `json:"childs,omitempty"`
|
||||||
if s.overflow > 0 {
|
Bytes int `json:"bytes,omitempty"`
|
||||||
info += ", overflow=" + strconv.Itoa(s.overflow)
|
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) {
|
func (s *Sender) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(s.String())
|
v := struct {
|
||||||
}
|
ID uint32 `json:"id"`
|
||||||
|
Codec *Codec `json:"codec"`
|
||||||
// VA - helper, for extract video and audio receivers from list
|
Parent uint32 `json:"parent,omitempty"`
|
||||||
func VA(receivers []*Receiver) (video, audio *Receiver) {
|
Bytes int `json:"bytes,omitempty"`
|
||||||
for _, receiver := range receivers {
|
Packets int `json:"packets,omitempty"`
|
||||||
switch GetKind(receiver.Codec.Name) {
|
Drops int `json:"drops,omitempty"`
|
||||||
case KindVideo:
|
}{
|
||||||
video = receiver
|
ID: s.Node.id,
|
||||||
case KindAudio:
|
Codec: s.Node.Codec,
|
||||||
audio = receiver
|
Bytes: s.Bytes,
|
||||||
}
|
Packets: s.Packets,
|
||||||
|
Drops: s.Drops,
|
||||||
}
|
}
|
||||||
return
|
if s.parent != nil {
|
||||||
|
v.Parent = s.parent.id
|
||||||
|
}
|
||||||
|
return json.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"%s: size:%6d, ts:%10d, type:%2d, ssrc:%d, seq:%5d, mark:%t, dts:%4d, dtime:%3d\n",
|
"%s: size=%6d ts=%10d type=%2d ssrc=%d seq=%5d mark=%t dts=%4d dtime=%3dms\n",
|
||||||
now.Format("15:04:05.000"),
|
now.Format("15:04:05.000"),
|
||||||
len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
||||||
packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),
|
packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),
|
||||||
@@ -41,7 +41,7 @@ func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {
|
|||||||
|
|
||||||
if dt := now.Sub(secTime); dt > time.Second {
|
if dt := now.Sub(secTime); dt > time.Second {
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"%s: size:%6d, cnt:%d, dts: %d, dtime:%d\n",
|
"%s: size=%6d cnt=%d dts=%d dtime=%3dms\n",
|
||||||
now.Format("15:04:05.000"),
|
now.Format("15:04:05.000"),
|
||||||
secSize, secCnt, lastTS-secTS, dt.Milliseconds(),
|
secSize, secCnt, lastTS-secTS, dt.Milliseconds(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Backchannel struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
client *Client
|
client *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||||
return nil, core.ErrCantGetTrack
|
return nil, core.ErrCantGetTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Start() error {
|
func (c *Backchannel) Start() error {
|
||||||
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -30,12 +30,7 @@ func (c *Consumer) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Stop() error {
|
func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||||
_ = c.SuperConsumer.Close()
|
|
||||||
return c.client.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
|
||||||
if err := c.client.Talk(); err != nil {
|
if err := c.client.Talk(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
+1
-1
@@ -114,7 +114,7 @@ func (c *Client) Play() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Talk() error {
|
func (c *Client) Talk() error {
|
||||||
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s","AudioFormat":{"EncodeType":"G711_ALAW"}}}` + "\x0A\x00"
|
||||||
|
|
||||||
data := fmt.Sprintf(format, c.session, "Claim")
|
data := fmt.Sprintf(format, c.session, "Claim")
|
||||||
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
||||||
|
|||||||
+13
-7
@@ -8,26 +8,32 @@ func Dial(url string) (core.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn := core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "dvrip",
|
||||||
|
Protocol: "tcp",
|
||||||
|
RemoteAddr: client.conn.RemoteAddr().String(),
|
||||||
|
Transport: client.conn,
|
||||||
|
}
|
||||||
|
|
||||||
if client.stream != "" {
|
if client.stream != "" {
|
||||||
prod := &Producer{client: client}
|
prod := &Producer{Connection: conn, client: client}
|
||||||
prod.Type = "DVRIP active producer"
|
|
||||||
if err := prod.probe(); err != nil {
|
if err := prod.probe(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return prod, nil
|
return prod, nil
|
||||||
} else {
|
} else {
|
||||||
cons := &Consumer{client: client}
|
conn.Medias = []*core.Media{
|
||||||
cons.Type = "DVRIP active consumer"
|
|
||||||
cons.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionSendonly,
|
Direction: core.DirectionSendonly,
|
||||||
Codecs: []*core.Codec{
|
Codecs: []*core.Codec{
|
||||||
|
// leave only one codec here for better compatibility with cameras
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/1111
|
||||||
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
|
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
|
||||||
{Name: core.CodecPCMU, ClockRate: 8000, PayloadType: 0},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cons, nil
|
return &Backchannel{Connection: conn, client: client}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Producer struct {
|
type Producer struct {
|
||||||
core.SuperProducer
|
core.Connection
|
||||||
|
|
||||||
client *Client
|
client *Client
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ func (c *Producer) Start() error {
|
|||||||
|
|
||||||
packet := &rtp.Packet{
|
packet := &rtp.Packet{
|
||||||
Header: rtp.Header{Timestamp: c.videoTS},
|
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)
|
//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 {
|
func (c *Producer) probe() error {
|
||||||
if err := c.client.Play(); err != nil {
|
if err := c.client.Play(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -150,7 +146,7 @@ func (c *Producer) probe() error {
|
|||||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||||
c.videoDT = 90000 / uint32(fps)
|
c.videoDT = 90000 / uint32(fps)
|
||||||
|
|
||||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
payload := annexb.EncodeToAVCC(b[16:])
|
||||||
c.addVideoTrack(b[4], payload)
|
c.addVideoTrack(b[4], payload)
|
||||||
|
|
||||||
case 0xFA: // audio
|
case 0xFA: // audio
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// correlation of libavformat versions with ffmpeg versions
|
||||||
|
const (
|
||||||
|
Version50 = "59. 16"
|
||||||
|
Version51 = "59. 27"
|
||||||
|
Version60 = "60. 3"
|
||||||
|
Version61 = "60. 16"
|
||||||
|
Version70 = "61. 1"
|
||||||
|
)
|
||||||
|
|
||||||
type Args struct {
|
type Args struct {
|
||||||
Bin string // ffmpeg
|
Bin string // ffmpeg
|
||||||
Global string // -hide_banner -v error
|
Global string // -hide_banner -v error
|
||||||
@@ -13,6 +22,7 @@ type Args struct {
|
|||||||
Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
||||||
Filters []string // scale=1920:1080
|
Filters []string // scale=1920:1080
|
||||||
Output string // -f rtsp {output}
|
Output string // -f rtsp {output}
|
||||||
|
Version string // libavformat version, it's more reliable than the ffmpeg version
|
||||||
|
|
||||||
Video, Audio int // count of Video and Audio params
|
Video, Audio int // count of Video and Audio params
|
||||||
}
|
}
|
||||||
@@ -52,6 +62,11 @@ func (a *Args) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.WriteByte(' ')
|
b.WriteByte(' ')
|
||||||
|
// starting from FFmpeg 6.1 readrate=1 has default initial bust 0.5 sec
|
||||||
|
// it might make us miss the first couple seconds of the file
|
||||||
|
if strings.HasPrefix(a.Input, "-re ") && a.Version >= Version61 {
|
||||||
|
b.WriteString("-readrate_initial_burst 0.001 ")
|
||||||
|
}
|
||||||
b.WriteString(a.Input)
|
b.WriteString(a.Input)
|
||||||
|
|
||||||
multimode := a.Video > 1 || a.Audio > 1
|
multimode := a.Video > 1 || a.Audio > 1
|
||||||
@@ -91,3 +106,18 @@ func (a *Args) String() string {
|
|||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseVersion(b []byte) (ffmpeg string, libavformat string) {
|
||||||
|
if len(b) > 100 {
|
||||||
|
// ffmpeg version n7.0-30-g8b0fe91754-20240520 Copyright (c) 2000-2024 the FFmpeg developers
|
||||||
|
if i := bytes.IndexByte(b[15:], ' '); i > 0 {
|
||||||
|
ffmpeg = string(b[15 : 15+i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// libavformat 60. 16.100 / 60. 16.100
|
||||||
|
if i := strings.Index(string(b), "libavformat"); i > 0 {
|
||||||
|
libavformat = string(b[i+15 : i+25])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,6 +137,70 @@ func TestNewReader(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d4c4e5820392c302c3132342c32000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
|
||||||
|
expect: []any{
|
||||||
|
"_result", float64(1), map[string]any{
|
||||||
|
"capabilities": float64(31),
|
||||||
|
"fmsVer": "LNX 9,0,124,2",
|
||||||
|
}, map[string]any{
|
||||||
|
"code": "NetConnection.Connect.Success",
|
||||||
|
"description": "Connection succeeded.",
|
||||||
|
"level": "status",
|
||||||
|
"objectEncoding": float64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
|
||||||
|
expect: []any{"_result", float64(4), any(nil), float64(1)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5265736574000b6465736372697074696f6e02000a706c6179207265736574000009",
|
||||||
|
expect: []any{
|
||||||
|
"onStatus", float64(5), any(nil), map[string]any{
|
||||||
|
"code": "NetStream.Play.Reset",
|
||||||
|
"description": "play reset",
|
||||||
|
"level": "status",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e02000a706c6179207374617274000009",
|
||||||
|
expect: []any{
|
||||||
|
"onStatus", float64(5), any(nil), map[string]any{
|
||||||
|
"code": "NetStream.Play.Start",
|
||||||
|
"description": "play start",
|
||||||
|
"level": "status",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e446174612e5374617274000b6465736372697074696f6e02000a64617461207374617274000009",
|
||||||
|
expect: []any{
|
||||||
|
"onStatus", float64(5), any(nil), map[string]any{
|
||||||
|
"code": "NetStream.Data.Start",
|
||||||
|
"description": "data start",
|
||||||
|
"level": "status",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f646502001c4e657453747265616d2e506c61792e5075626c6973684e6f74696679000b6465736372697074696f6e02000e7075626c697368206e6f74696679000009",
|
||||||
|
expect: []any{
|
||||||
|
"onStatus", float64(5), any(nil), map[string]any{
|
||||||
|
"code": "NetStream.Play.PublishNotify",
|
||||||
|
"description": "publish notify",
|
||||||
|
"level": "status",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "obs-connect",
|
name: "obs-connect",
|
||||||
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
||||||
|
|||||||
+13
-12
@@ -10,17 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
wr *core.WriteBuffer
|
wr *core.WriteBuffer
|
||||||
muxer *Muxer
|
muxer *Muxer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumer() *Consumer {
|
func NewConsumer() *Consumer {
|
||||||
c := &Consumer{
|
medias := []*core.Media{
|
||||||
wr: core.NewWriteBuffer(nil),
|
|
||||||
muxer: &Muxer{},
|
|
||||||
}
|
|
||||||
c.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindVideo,
|
Kind: core.KindVideo,
|
||||||
Direction: core.DirectionSendonly,
|
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 {
|
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)
|
return c.wr.WriteTo(wr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Stop() error {
|
|
||||||
_ = c.SuperConsumer.Close()
|
|
||||||
return c.wr.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ func (m *Muxer) GetInit() []byte {
|
|||||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
if len(sps) == 0 {
|
if len(sps) == 0 {
|
||||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||||
|
} else {
|
||||||
|
h264.FixPixFmt(sps)
|
||||||
}
|
}
|
||||||
if len(pps) == 0 {
|
if len(pps) == 0 {
|
||||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||||
|
|||||||
+10
-9
@@ -15,18 +15,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Producer struct {
|
type Producer struct {
|
||||||
core.SuperProducer
|
core.Connection
|
||||||
rd *core.ReadBuffer
|
rd *core.ReadBuffer
|
||||||
|
|
||||||
video, audio *core.Receiver
|
video, audio *core.Receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(rd io.Reader) (*Producer, error) {
|
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 {
|
if err := prod.probe(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
prod.Type = "FLV producer"
|
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
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 {
|
if media.Kind == core.KindVideo {
|
||||||
c.video = receiver
|
c.video = receiver
|
||||||
} else {
|
} 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 {
|
func (c *Producer) probe() error {
|
||||||
if err := c.readHeader(); err != nil {
|
if err := c.readHeader(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user