Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4885c2c3a | |||
| f5aaee006e | |||
| db6745e8ff | |||
| ba34855602 | |||
| e6fa97c738 | |||
| 5b481a27c6 | |||
| bdc7ff1035 | |||
| da5f060741 | |||
| a56d335380 | |||
| d8aed552bc | |||
| d7286fa06e | |||
| 906f554d74 | |||
| cb44d5431a | |||
| a69eb8a66e | |||
| 1b411b1fed | |||
| 5d57959608 | |||
| 31e57c2ff8 | |||
| 734393d638 | |||
| 96504e2fb0 | |||
| ecfe802065 | |||
| 1ac9d54dab | |||
| 72d7e8aaaa | |||
| 0395696866 | |||
| 0667683e4d | |||
| aca0781c4b | |||
| b389d0eb9c | |||
| bf303ed471 | |||
| cd777ba2b4 | |||
| e3188a0a6d | |||
| 2bab0a014d | |||
| a01da18018 | |||
| 9d5a5c1e45 | |||
| 8377ad1d05 | |||
| ec33796bd3 | |||
| 31e4ba2722 | |||
| e0b1a50356 | |||
| 9bb36ebb6c | |||
| 756be9801e | |||
| bd73b07ed8 | |||
| df1d44d24e | |||
| 79245eeff4 | |||
| aa86c1ec25 | |||
| 2ab1d9d774 | |||
| a9e7a73cc8 | |||
| ea17b420d6 | |||
| 660979dfda | |||
| a6b9b4993f | |||
| cc74504ed8 | |||
| 791239be12 | |||
| a79061c7c2 | |||
| 50ad3b20c4 | |||
| 649de0131c | |||
| 8cb513cb89 | |||
| 3932dbaa84 | |||
| 4534b4d8ca | |||
| 8e571a66e3 | |||
| 0ccfcb0ec0 | |||
| 8bae4631d2 | |||
| 268629f551 | |||
| 0bd2fcde54 | |||
| 6f34cf0c95 | |||
| f8bc25d0ae | |||
| 8749562c96 | |||
| d9d2bdff44 | |||
| b3e9ed23ac | |||
| bf3f81ccac | |||
| ff39e2e496 | |||
| d2346a2aed | |||
| 8f57b1acb6 | |||
| 6fafd10482 | |||
| c726651b8b | |||
| 02af2e2849 | |||
| 6d9c7012b0 | |||
| 8a7712a4c8 | |||
| 82fa803a37 | |||
| 78a74da8d6 | |||
| 53242ea02f | |||
| af05083a1f | |||
| c41bddbbea | |||
| 54c8ca0112 | |||
| a518488289 | |||
| 99cc21aacb | |||
| bc8295baee | |||
| 50f9913c41 | |||
| 4c135b5a46 | |||
| 686fb374e9 | |||
| 2b3e6a2730 | |||
| 9143729042 | |||
| 3952f0ba0f | |||
| 7a131822db |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with: { go-version: '1.21' }
|
with: { go-version: '1.22' }
|
||||||
|
|
||||||
- name: Build go2rtc_win64
|
- name: Build go2rtc_win64
|
||||||
env: { GOOS: windows, GOARCH: amd64 }
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
@@ -123,7 +123,9 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}},enable=false
|
type=semver,pattern={{version}},enable=false
|
||||||
@@ -142,6 +144,14 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -168,7 +178,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
|
||||||
@@ -189,6 +201,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
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
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
|
go2rtc_linux*
|
||||||
|
go2rtc_mac*
|
||||||
|
go2rtc_win*
|
||||||
|
|
||||||
0_test.go
|
0_test.go
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+3
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.11"
|
||||||
ARG GO_VERSION="1.21"
|
ARG GO_VERSION="1.22"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
@@ -20,6 +20,8 @@ ENV GOARCH=${TARGETARCH}
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
# Cache dependencies
|
# Cache dependencies
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||||
|
|||||||
@@ -779,7 +779,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
|||||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||||
|
|
||||||
- Supported codecs: H264 for video and AAC for audio
|
- Supported codecs: H264 for video and AAC for audio
|
||||||
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
|
- AAC audio is required for YouTube, videos without audio will not work
|
||||||
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||||
|
|
||||||
You can use API:
|
You can use API:
|
||||||
@@ -792,16 +792,19 @@ Or config file:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
publish:
|
publish:
|
||||||
# publish stream "tplink_tapo" to Telegram
|
# publish stream "video_audio_transcode" to Telegram
|
||||||
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
video_audio_transcode:
|
||||||
# publish stream "other_camera" to Telegram and YouTube
|
|
||||||
other_camera:
|
|
||||||
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
# publish stream "audio_transcode" to Telegram and YouTube
|
||||||
|
audio_transcode:
|
||||||
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
- rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||||
|
|
||||||
streams:
|
streams:
|
||||||
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
|
video_audio_transcode:
|
||||||
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
||||||
|
audio_transcode:
|
||||||
|
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := rtsp.NewClient(os.Args[1])
|
||||||
|
if err := client.Dial(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||||
|
},
|
||||||
|
ID: "streamid=0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.Announce(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if _, err := client.SetupMedia(client.Medias[0]); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if err := client.Record(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.RunUntilSignal()
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astits v1.13.0
|
github.com/asticode/go-astits v1.13.0
|
||||||
github.com/expr-lang/expr v1.16.5
|
github.com/expr-lang/expr v1.16.9
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/miekg/dns v1.1.59
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/pion/ice/v2 v2.3.24
|
github.com/pion/ice/v2 v2.3.24
|
||||||
github.com/pion/interceptor v0.1.29
|
github.com/pion/interceptor v0.1.29
|
||||||
@@ -15,12 +16,12 @@ require (
|
|||||||
github.com/pion/srtp/v2 v2.0.18
|
github.com/pion/srtp/v2 v2.0.18
|
||||||
github.com/pion/stun v0.6.1
|
github.com/pion/stun v0.6.1
|
||||||
github.com/pion/webrtc/v3 v3.2.40
|
github.com/pion/webrtc/v3 v3.2.40
|
||||||
github.com/rs/zerolog v1.32.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
golang.org/x/crypto v0.23.0
|
golang.org/x/crypto v0.24.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/pion/datachannel v1.5.6 // indirect
|
github.com/pion/datachannel v1.5.6 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.11 // indirect
|
github.com/pion/dtls/v2 v2.2.11 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
@@ -40,9 +40,9 @@ require (
|
|||||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.26.0 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.20.0 // indirect
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
||||||
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||||
|
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||||
|
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -85,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
@@ -115,10 +119,14 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
|||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -134,6 +142,8 @@ golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
|||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -160,6 +170,8 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
|||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -184,6 +196,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
||||||
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# only debian 13 (trixie) has latest ffmpeg
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
# https://packages.debian.org/trixie/ffmpeg
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
ARG DEBIAN_VERSION="trixie-slim"
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
ARG GO_VERSION="1.21-bookworm"
|
ARG GO_VERSION="1.22-bookworm"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
|||||||
+21
-5
@@ -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)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -21,15 +25,24 @@ Also go2rtc support templates for using environment variables in any part of con
|
|||||||
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 env "RTSP_USER" not set
|
||||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JSON Schema
|
||||||
|
|
||||||
|
Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) supports autocomplete and syntax validation.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/website/schema.json
|
||||||
|
```
|
||||||
|
|
||||||
## Defaults
|
## 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"
|
||||||
@@ -51,4 +67,4 @@ webrtc:
|
|||||||
listen: ":8555/tcp"
|
listen: ":8555/tcp"
|
||||||
ice_servers:
|
ice_servers:
|
||||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||||
```
|
```
|
||||||
|
|||||||
+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())
|
||||||
|
}
|
||||||
+100
-43
@@ -1,6 +1,7 @@
|
|||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -49,8 +51,10 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execHandle(rawURL string) (core.Producer, error) {
|
func execHandle(rawURL string) (core.Producer, error) {
|
||||||
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
|
||||||
var path string
|
var path string
|
||||||
var query url.Values
|
|
||||||
|
|
||||||
// RTSP flow should have `{output}` inside URL
|
// RTSP flow should have `{output}` inside URL
|
||||||
// pipe flow may have `#{params}` inside URL
|
// pipe flow may have `#{params}` inside URL
|
||||||
@@ -62,60 +66,73 @@ func execHandle(rawURL string) (core.Producer, error) {
|
|||||||
sum := md5.Sum([]byte(rawURL))
|
sum := md5.Sum([]byte(rawURL))
|
||||||
path = "/" + hex.EncodeToString(sum[:])
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||||
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
|
|
||||||
query = streams.ParseQuery(rawURL[i+1:])
|
|
||||||
rawURL = rawURL[:i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
if log.Debug().Enabled() {
|
cmd.Stderr = &logWriter{
|
||||||
cmd.Stderr = os.Stderr
|
buf: make([]byte, 512),
|
||||||
|
debug: log.Debug().Enabled(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
return handlePipe(rawURL, cmd, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleRTSP(rawURL, cmd, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
|
||||||
if query.Get("backchannel") == "1" {
|
if query.Get("backchannel") == "1" {
|
||||||
return stdin.NewClient(cmd)
|
return stdin.NewClient(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := PipeCloser(cmd, query)
|
cl := &closer{cmd: cmd, query: query}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return handlePipe(rawURL, cmd, cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleRTSP(rawURL, cmd, cl, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc := struct {
|
||||||
|
io.Reader
|
||||||
|
io.Closer
|
||||||
|
}{
|
||||||
|
// add buffer for pipe reader to reduce syscall
|
||||||
|
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||||
|
cl,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
if err = cmd.Start(); err != nil {
|
if err = cmd.Start(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prod, err := magic.Open(r)
|
prod, err := magic.Open(rc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = r.Close()
|
_ = rc.Close()
|
||||||
|
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return prod, err
|
if info, ok := prod.(core.Info); ok {
|
||||||
|
info.SetProtocol("pipe")
|
||||||
|
setRemoteInfo(info, source, cmd.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) {
|
||||||
stderr := limitBuffer{buf: make([]byte, 512)}
|
|
||||||
|
|
||||||
if cmd.Stderr != nil {
|
|
||||||
cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr)
|
|
||||||
} else {
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
waiter := make(chan core.Producer)
|
waiter := make(chan *pkg.Conn, 1)
|
||||||
|
|
||||||
waitersMu.Lock()
|
waitersMu.Lock()
|
||||||
waiters[path] = waiter
|
waiters[path] = waiter
|
||||||
@@ -127,12 +144,12 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
waitersMu.Unlock()
|
waitersMu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run")
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp")
|
||||||
|
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Error().Err(err).Str("url", url).Msg("[exec]")
|
log.Error().Err(err).Str("source", source).Msg("[exec]")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,15 +159,17 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 60):
|
case <-time.After(time.Minute):
|
||||||
_ = cmd.Process.Kill()
|
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
_ = cl.Close()
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("exec: timeout")
|
||||||
case <-done:
|
case <-done:
|
||||||
// limit message size
|
// limit message size
|
||||||
return nil, errors.New("exec: " + stderr.String())
|
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||||
case prod := <-waiter:
|
case prod := <-waiter:
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||||
|
setRemoteInfo(prod, source, cmd.Args)
|
||||||
|
prod.OnClose = cl.Close
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,25 +178,63 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
waiters = map[string]chan core.Producer{}
|
waiters = make(map[string]chan *pkg.Conn)
|
||||||
waitersMu sync.Mutex
|
waitersMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
type limitBuffer struct {
|
type logWriter struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
n int
|
debug bool
|
||||||
|
n int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *limitBuffer) String() string {
|
func (l *logWriter) String() string {
|
||||||
if l.n == len(l.buf) {
|
if l.n == len(l.buf) {
|
||||||
return string(l.buf) + "..."
|
return string(l.buf) + "..."
|
||||||
}
|
}
|
||||||
return string(l.buf[:l.n])
|
return string(l.buf[:l.n])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *limitBuffer) Write(p []byte) (int, error) {
|
func (l *logWriter) Write(p []byte) (n int, err error) {
|
||||||
if l.n < cap(l.buf) {
|
if l.n < cap(l.buf) {
|
||||||
l.n += copy(l.buf[l.n:], p)
|
l.n += copy(l.buf[l.n:], p)
|
||||||
}
|
}
|
||||||
return len(p), nil
|
n = len(p)
|
||||||
|
if l.debug {
|
||||||
|
if p = trimSpace(p); p != nil {
|
||||||
|
log.Debug().Msgf("[exec] %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpace(b []byte) []byte {
|
||||||
|
start := 0
|
||||||
|
stop := len(b)
|
||||||
|
for ; start < stop; start++ {
|
||||||
|
if b[start] >= ' ' {
|
||||||
|
break // trim all ASCII before 0x20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; ; stop-- {
|
||||||
|
if stop == start {
|
||||||
|
return nil // skip empty output
|
||||||
|
}
|
||||||
|
if b[stop-1] > ' ' {
|
||||||
|
break // trim all ASCII before 0x21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[start:stop]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRemoteInfo(info core.Info, source string, args []string) {
|
||||||
|
info.SetSource(source)
|
||||||
|
|
||||||
|
if i := slices.Index(args, "-i"); i > 0 && i < len(args)-1 {
|
||||||
|
rawURL := args[i+1]
|
||||||
|
if u, err := url.Parse(rawURL); err == nil && u.Host != "" {
|
||||||
|
info.SetRemoteAddr(u.Host)
|
||||||
|
info.SetURL(rawURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package exec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
|
||||||
func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) {
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// add buffer for pipe reader to reduce syscall
|
|
||||||
return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pipeCloser struct {
|
|
||||||
io.Reader
|
|
||||||
io.Closer
|
|
||||||
cmd *exec.Cmd
|
|
||||||
query url.Values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipeCloser) Close() error {
|
|
||||||
return errors.Join(p.Closer.Close(), p.Kill(), p.Wait())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipeCloser) Kill() error {
|
|
||||||
if s := p.query.Get("killsignal"); s != "" {
|
|
||||||
log.Trace().Msgf("[exec] kill with custom sig=%s", s)
|
|
||||||
sig := syscall.Signal(core.Atoi(s))
|
|
||||||
return p.cmd.Process.Signal(sig)
|
|
||||||
}
|
|
||||||
return p.cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipeCloser) Wait() error {
|
|
||||||
if s := p.query.Get("killtimeout"); s != "" {
|
|
||||||
timeout := time.Duration(core.Atoi(s)) * time.Second
|
|
||||||
timer := time.AfterFunc(timeout, func() {
|
|
||||||
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
|
||||||
_ = p.cmd.Process.Kill()
|
|
||||||
})
|
|
||||||
defer timer.Stop() // stop timer if Wait ends before timeout
|
|
||||||
}
|
|
||||||
return p.cmd.Wait()
|
|
||||||
}
|
|
||||||
@@ -45,6 +45,13 @@
|
|||||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TTS
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma
|
||||||
|
```
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
dst := query.Get("dst")
|
||||||
|
stream := streams.Get(dst)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var src string
|
||||||
|
if s := query.Get("file"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto#input=file"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("live"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("text"); s != "" {
|
||||||
|
if strings.IndexAny(s, `'"&%$`) < 0 {
|
||||||
|
src = "ffmpeg:tts?text=" + s
|
||||||
|
if s = query.Get("voice"); s != "" {
|
||||||
|
src += "&voice=" + s
|
||||||
|
}
|
||||||
|
src += "#audio=auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if src == "" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stream.Play(src); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,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
|
||||||
|
|||||||
+69
-20
@@ -2,35 +2,58 @@ package ffmpeg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]string `yaml:"ffmpeg"`
|
Mod map[string]string `yaml:"ffmpeg"`
|
||||||
|
Log struct {
|
||||||
|
Level string `yaml:"ffmpeg"`
|
||||||
|
} `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Mod = defaults // will be overriden from yaml
|
cfg.Mod = defaults // will be overriden from yaml
|
||||||
|
cfg.Log.Level = "error"
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
log = app.GetLogger("ffmpeg")
|
||||||
defaults["global"] += " -v error"
|
|
||||||
|
// zerolog levels: trace debug info warn error fatal panic disabled
|
||||||
|
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
|
||||||
|
if cfg.Log.Level == "warn" {
|
||||||
|
cfg.Log.Level = "warning"
|
||||||
}
|
}
|
||||||
|
defaults["global"] += " -v " + cfg.Log.Level
|
||||||
|
|
||||||
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
||||||
|
if _, err := Version(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
args := parseArgs(url[7:])
|
args := parseArgs(url[7:])
|
||||||
|
if slices.Contains(args.Codecs, "auto") {
|
||||||
|
return "", nil // force call streams.HandleFunc("ffmpeg")
|
||||||
|
}
|
||||||
return "exec:" + args.String(), nil
|
return "exec:" + args.String(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("ffmpeg", NewProducer)
|
||||||
|
|
||||||
|
api.HandleFunc("api/ffmpeg", apiFFmpeg)
|
||||||
|
|
||||||
device.Init(defaults["bin"])
|
device.Init(defaults["bin"])
|
||||||
hardware.Init(defaults["bin"])
|
hardware.Init(defaults["bin"])
|
||||||
}
|
}
|
||||||
@@ -49,6 +72,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 +85,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 +148,8 @@ var defaults = map[string]string{
|
|||||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
// configTemplate - return template from config (defaults) if exist or return raw template
|
// configTemplate - return template from config (defaults) if exist or return raw template
|
||||||
func configTemplate(template string) string {
|
func configTemplate(template string) string {
|
||||||
if s := defaults[template]; s != "" {
|
if s := defaults[template]; s != "" {
|
||||||
@@ -140,9 +174,10 @@ func inputTemplate(name, s string, query url.Values) string {
|
|||||||
func parseArgs(s string) *ffmpeg.Args {
|
func parseArgs(s string) *ffmpeg.Args {
|
||||||
// init FFmpeg arguments
|
// init FFmpeg arguments
|
||||||
args := &ffmpeg.Args{
|
args := &ffmpeg.Args{
|
||||||
Bin: defaults["bin"],
|
Bin: defaults["bin"],
|
||||||
Global: defaults["global"],
|
Global: defaults["global"],
|
||||||
Output: defaults["output"],
|
Output: defaults["output"],
|
||||||
|
Version: verAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
var query url.Values
|
var query url.Values
|
||||||
@@ -188,16 +223,14 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
s += "?video&audio"
|
s += "?video&audio"
|
||||||
}
|
}
|
||||||
args.Input = inputTemplate("rtsp", s, query)
|
args.Input = inputTemplate("rtsp", s, query)
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
} else if i = strings.Index(s, "?"); i > 0 {
|
||||||
var err error
|
switch s[:i] {
|
||||||
args.Input, err = device.GetInput(s)
|
case "device":
|
||||||
if err != nil {
|
args.Input = device.GetInput(s[i+1:])
|
||||||
return nil
|
case "virtual":
|
||||||
}
|
args.Input = virtual.GetInput(s[i+1:])
|
||||||
} else if strings.HasPrefix(s, "virtual?") {
|
case "tts":
|
||||||
var err error
|
args.Input = virtual.GetInputTTS(s[i+1:])
|
||||||
if args.Input, err = virtual.GetInput(s[8:]); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args.Input = inputTemplate("file", s, query)
|
args.Input = inputTemplate("file", s, query)
|
||||||
@@ -315,11 +348,27 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
args.AddCodec("-an")
|
args.AddCodec("-an")
|
||||||
}
|
}
|
||||||
|
|
||||||
// transcoding to only mjpeg
|
// change otput from RTSP to some other pipe format
|
||||||
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
|
switch {
|
||||||
// no transcoding from mjpeg input
|
case args.Video == 0 && args.Audio == 0:
|
||||||
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
|
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
|
||||||
args.Output = defaults["output/mjpeg"]
|
if strings.Contains(args.Input, " mjpeg ") {
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
}
|
||||||
|
case args.Video == 1 && args.Audio == 0:
|
||||||
|
switch core.Before(query.Get("video"), "/") {
|
||||||
|
case "mjpeg":
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
case "raw":
|
||||||
|
args.Output = defaults["output/raw"]
|
||||||
|
}
|
||||||
|
case args.Video == 0 && args.Audio == 1:
|
||||||
|
switch core.Before(query.Get("audio"), "/") {
|
||||||
|
case "aac":
|
||||||
|
args.Output = defaults["output/aac"]
|
||||||
|
case "pcma", "pcmu", "pcml":
|
||||||
|
args.Output = defaults["output/wav"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ffmpeg
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,3 +293,23 @@ func TestDrawText(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
verAV = ffmpeg.Version61
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: "/media/bbb.mp4",
|
||||||
|
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
args := parseArgs(test.source)
|
||||||
|
require.Equal(t, test.expect, args.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -152,7 +150,6 @@ var cache = map[string]string{}
|
|||||||
|
|
||||||
func run(bin string, args string) bool {
|
func run(bin string, args string) bool {
|
||||||
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
||||||
log.Printf("%v %v", args, err)
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,11 +127,7 @@ func tcpHandle(netConn net.Conn) error {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func streamsHandle(url string) (core.Producer, error) {
|
func streamsHandle(url string) (core.Producer, error) {
|
||||||
client, err := rtmp.DialPlay(url)
|
return rtmp.DialPlay(url)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||||
@@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cons := flv.NewConsumer()
|
cons := flv.NewConsumer()
|
||||||
cons.Type = "HTTP-FLV consumer"
|
cons.WithRequest(r)
|
||||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
|
||||||
cons.UserAgent = r.UserAgent()
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
|||||||
@@ -210,6 +210,11 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query := conn.URL.Query()
|
||||||
|
if s := query.Get("timeout"); s != "" {
|
||||||
|
conn.Timeout = core.Atoi(s)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||||
|
|
||||||
stream.AddProducer(conn)
|
stream.AddProducer(conn)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## Testing notes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
test1-basic: ffmpeg:virtual?video#video=h264
|
||||||
|
test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264
|
||||||
|
test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output}
|
||||||
|
```
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
src := query.Get("src")
|
||||||
|
|
||||||
|
// without source - return all streams list
|
||||||
|
if src == "" && r.Method != "POST" {
|
||||||
|
api.ResponseJSON(w, streams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure about all this API. Should be rewrited...
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
stream := Get(src)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := probe.NewProbe(query)
|
||||||
|
if len(cons.Medias) != 0 {
|
||||||
|
cons.WithRequest(r)
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponsePrettyJSON(w, stream)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
} else {
|
||||||
|
api.ResponsePrettyJSON(w, streams[src])
|
||||||
|
}
|
||||||
|
|
||||||
|
case "PUT":
|
||||||
|
name := query.Get("name")
|
||||||
|
if name == "" {
|
||||||
|
name = src
|
||||||
|
}
|
||||||
|
|
||||||
|
if New(name, src) == nil {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.PatchConfig(name, src, "streams"); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "PATCH":
|
||||||
|
name := query.Get("name")
|
||||||
|
if name == "" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
|
||||||
|
if Patch(name, src) == nil {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "POST":
|
||||||
|
// with dst - redirect source to dst
|
||||||
|
if dst := query.Get("dst"); dst != "" {
|
||||||
|
if stream := Get(dst); stream != nil {
|
||||||
|
if err := Validate(src); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
} else if err = stream.Play(src); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
} else {
|
||||||
|
api.ResponseJSON(w, stream)
|
||||||
|
}
|
||||||
|
} else if stream = Get(src); stream != nil {
|
||||||
|
if err := Validate(dst); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
} else if err = stream.Publish(dst); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DELETE":
|
||||||
|
delete(streams, src)
|
||||||
|
|
||||||
|
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
dot := make([]byte, 0, 1024)
|
||||||
|
dot = append(dot, "digraph {\n"...)
|
||||||
|
if query.Has("src") {
|
||||||
|
for _, name := range query["src"] {
|
||||||
|
if stream := streams[name]; stream != nil {
|
||||||
|
dot = AppendDOT(dot, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, stream := range streams {
|
||||||
|
dot = AppendDOT(dot, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dot = append(dot, '}')
|
||||||
|
|
||||||
|
api.Response(w, dot, "text/vnd.graphviz")
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AppendDOT(dot []byte, stream *Stream) []byte {
|
||||||
|
for _, prod := range stream.producers {
|
||||||
|
if prod.conn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, err := marshalConn(prod.conn)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dot = c.appendDOT(dot, "producer")
|
||||||
|
}
|
||||||
|
for _, cons := range stream.consumers {
|
||||||
|
c, err := marshalConn(cons)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dot = c.appendDOT(dot, "consumer")
|
||||||
|
}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalConn(v any) (*conn, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var c conn
|
||||||
|
if err = json.Unmarshal(b, &c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytesK = "KMGTP"
|
||||||
|
|
||||||
|
func humanBytes(i int) string {
|
||||||
|
if i < 1000 {
|
||||||
|
return fmt.Sprintf("%d B", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := float64(i) / 1000
|
||||||
|
var n uint8
|
||||||
|
for f >= 1000 && n < 5 {
|
||||||
|
f /= 1000
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f %cB", f, bytesK[n])
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Codec map[string]any `json:"codec"`
|
||||||
|
Parent uint32 `json:"parent"`
|
||||||
|
Childs []uint32 `json:"childs"`
|
||||||
|
Bytes int `json:"bytes"`
|
||||||
|
//Packets uint32 `json:"packets"`
|
||||||
|
//Drops uint32 `json:"drops"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"}
|
||||||
|
|
||||||
|
func (n *node) name() string {
|
||||||
|
if name, ok := n.Codec["codec_name"].(string); ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) codec() []byte {
|
||||||
|
b := make([]byte, 0, 128)
|
||||||
|
for _, k := range codecKeys {
|
||||||
|
if v := n.Codec[k]; v != nil {
|
||||||
|
b = fmt.Appendf(b, "%s=%v\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l := len(b); l > 0 {
|
||||||
|
return b[:l-1]
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) appendDOT(dot []byte, group string) []byte {
|
||||||
|
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec())
|
||||||
|
//for _, sink := range n.Childs {
|
||||||
|
// dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink)
|
||||||
|
//}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
type conn struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
FormatName string `json:"format_name"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Receivers []node `json:"receivers"`
|
||||||
|
Senders []node `json:"senders"`
|
||||||
|
BytesRecv int `json:"bytes_recv"`
|
||||||
|
BytesSend int `json:"bytes_send"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||||
|
host := c.host()
|
||||||
|
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
||||||
|
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
|
||||||
|
if group == "producer" {
|
||||||
|
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||||
|
} else {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, recv := range c.Receivers {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes))
|
||||||
|
dot = recv.appendDOT(dot, "node")
|
||||||
|
}
|
||||||
|
for _, send := range c.Senders {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes))
|
||||||
|
//dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes))
|
||||||
|
//dot = send.appendDOT(dot, "node")
|
||||||
|
}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) host() (s string) {
|
||||||
|
if c.Protocol == "pipe" {
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if s = c.RemoteAddr; s == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(s, "forwarded"); i > 0 {
|
||||||
|
s = s[i+10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if s[0] == '[' {
|
||||||
|
if i := strings.Index(s, "]"); i > 0 {
|
||||||
|
return s[1:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.IndexAny(s, " ,:"); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) label() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("format_name=" + c.FormatName)
|
||||||
|
if c.Protocol != "" {
|
||||||
|
sb.WriteString("\nprotocol=" + c.Protocol)
|
||||||
|
}
|
||||||
|
if c.Source != "" {
|
||||||
|
sb.WriteString("\nsource=" + c.Source)
|
||||||
|
}
|
||||||
|
if c.URL != "" {
|
||||||
|
sb.WriteString("\nurl=" + c.URL)
|
||||||
|
}
|
||||||
|
if c.UserAgent != "" {
|
||||||
|
sb.WriteString("\nuser_agent=" + c.UserAgent)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler func(url string) (core.Producer, error)
|
type Handler func(source string) (core.Producer, error)
|
||||||
|
|
||||||
var handlers = map[string]Handler{}
|
var handlers = map[string]Handler{}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,18 +82,20 @@ func (s *Stream) Play(source string) error {
|
|||||||
s.AddInternalProducer(src)
|
s.AddInternalProducer(src)
|
||||||
s.AddInternalConsumer(cons)
|
s.AddInternalConsumer(cons)
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = src.Start()
|
|
||||||
_ = dst.Stop()
|
|
||||||
s.RemoveProducer(src)
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_ = dst.Start()
|
_ = dst.Start()
|
||||||
_ = src.Stop()
|
_ = src.Stop()
|
||||||
s.RemoveInternalConsumer(cons)
|
s.RemoveInternalConsumer(cons)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = src.Start()
|
||||||
|
// little timeout before stop dst, so the buffer can be transferred
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
_ = dst.Stop()
|
||||||
|
s.RemoveProducer(src)
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||||
if p.conn != nil {
|
if conn := p.conn; conn != nil {
|
||||||
return json.Marshal(p.conn)
|
return json.Marshal(conn)
|
||||||
}
|
}
|
||||||
|
info := map[string]string{"url": p.url}
|
||||||
info := core.Info{URL: p.url}
|
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +206,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
for _, media := range conn.GetMedias() {
|
for _, media := range conn.GetMedias() {
|
||||||
switch media.Direction {
|
switch media.Direction {
|
||||||
case core.DirectionRecvonly:
|
case core.DirectionRecvonly:
|
||||||
for _, receiver := range p.receivers {
|
for i, receiver := range p.receivers {
|
||||||
codec := media.MatchCodec(receiver.Codec)
|
codec := media.MatchCodec(receiver.Codec)
|
||||||
if codec == nil {
|
if codec == nil {
|
||||||
continue
|
continue
|
||||||
@@ -219,6 +218,7 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiver.Replace(track)
|
receiver.Replace(track)
|
||||||
|
p.receivers[i] = track
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +234,9 @@ func (p *Producer) reconnect(workerID, retry int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop previous connection after moving tracks (fix ghost exec/ffmpeg)
|
||||||
|
_ = p.conn.Stop()
|
||||||
|
// swap connections
|
||||||
p.conn = conn
|
p.conn = conn
|
||||||
|
|
||||||
go p.worker(conn, workerID)
|
go p.worker(conn, workerID)
|
||||||
|
|||||||
@@ -112,19 +112,12 @@ producers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||||
if !s.mu.TryLock() {
|
var info = struct {
|
||||||
log.Warn().Msgf("[streams] json locked")
|
|
||||||
return json.Marshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var info struct {
|
|
||||||
Producers []*Producer `json:"producers"`
|
Producers []*Producer `json:"producers"`
|
||||||
Consumers []core.Consumer `json:"consumers"`
|
Consumers []core.Consumer `json:"consumers"`
|
||||||
|
}{
|
||||||
|
Producers: s.producers,
|
||||||
|
Consumers: s.consumers,
|
||||||
}
|
}
|
||||||
info.Producers = s.producers
|
|
||||||
info.Consumers = s.consumers
|
|
||||||
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-97
@@ -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,9 +48,16 @@ func Get(name string) *Stream {
|
|||||||
|
|
||||||
var sanitize = regexp.MustCompile(`\s`)
|
var sanitize = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
func New(name string, source string) *Stream {
|
// Validate - not allow creating dynamic streams with spaces in the source
|
||||||
// not allow creating dynamic streams with spaces in the source
|
func Validate(source string) error {
|
||||||
if sanitize.MatchString(source) {
|
if sanitize.MatchString(source) {
|
||||||
|
return errors.New("streams: invalid dynamic source")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(name string, source string) *Stream {
|
||||||
|
if Validate(source) != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,97 +143,6 @@ func Delete(id string) {
|
|||||||
delete(streams, id)
|
delete(streams, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
query := r.URL.Query()
|
|
||||||
src := query.Get("src")
|
|
||||||
|
|
||||||
// without source - return all streams list
|
|
||||||
if src == "" && r.Method != "POST" {
|
|
||||||
api.ResponseJSON(w, streams)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not sure about all this API. Should be rewrited...
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
defer sendAnswer.Done(nil)
|
defer sendAnswer.Done(nil)
|
||||||
|
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = "WebRTC/WebSocket async"
|
|
||||||
conn.Mode = mode
|
conn.Mode = mode
|
||||||
|
conn.Protocol = "ws"
|
||||||
conn.UserAgent = tr.Request.UserAgent()
|
conn.UserAgent = tr.Request.UserAgent()
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -207,8 +207,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
|||||||
|
|
||||||
// create new webrtc instance
|
// create new webrtc instance
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = desc
|
conn.FormatName = desc
|
||||||
conn.UserAgent = userAgent
|
conn.UserAgent = userAgent
|
||||||
|
conn.Protocol = "http"
|
||||||
conn.Listen(func(msg any) {
|
conn.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case pion.PeerConnectionState:
|
case pion.PeerConnectionState:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func Init() {
|
|||||||
if stream == nil {
|
if stream == nil {
|
||||||
return "", errors.New(api.StreamNotFound)
|
return "", errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
|
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
app.Version = "1.9.4"
|
||||||
|
|
||||||
// 1. Core modules: app, api/ws, streams
|
// 1. Core modules: app, api/ws, streams
|
||||||
|
|
||||||
app.Init() // init config and logs
|
app.Init() // init config and logs
|
||||||
|
|||||||
@@ -1,3 +1,85 @@
|
|||||||
|
# Notes
|
||||||
|
|
||||||
|
go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.
|
||||||
|
Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.
|
||||||
|
|
||||||
|
## Producers (input)
|
||||||
|
|
||||||
|
- The initiator of the connection can be go2rtc - **Source protocols**
|
||||||
|
- The initiator of the connection can be an external program - **Ingress protocols**
|
||||||
|
- Codecs can be incoming - **Recevers codecs**
|
||||||
|
- Codecs can be outgoing (two way audio) - **Senders codecs**
|
||||||
|
|
||||||
|
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|
||||||
|
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
|
||||||
|
| adts | http,tcp,pipe | http | aac | | `http:` |
|
||||||
|
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
|
||||||
|
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
|
||||||
|
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
|
||||||
|
| gopro | http+udp | | TODO | | `gopro:` |
|
||||||
|
| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` |
|
||||||
|
| hls/mpegts | http | | h264,h265,aac,opus | | `http:` |
|
||||||
|
| homekit | homekit+udp | | h264,eld* | | `homekit:` |
|
||||||
|
| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` |
|
||||||
|
| ivideon | ws | | h264 | | `ivideon:` |
|
||||||
|
| kasa | http | | h264,pcm_mulaw | | `kasa:` |
|
||||||
|
| h264 | http,tcp,pipe | http | h264 | | `http:` |
|
||||||
|
| hevc | http,tcp,pipe | http | hevc | | `http:` |
|
||||||
|
| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||||
|
| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
|
||||||
|
| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` |
|
||||||
|
| nest/webrtc | http+udp | | TODO | | `nest:` |
|
||||||
|
| roborock | mqtt+udp | | h264,opus | opus | `roborock:` |
|
||||||
|
| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` |
|
||||||
|
| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` |
|
||||||
|
| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` |
|
||||||
|
| tapo | http | | h264,pcma | pcm_alaw | `tapo:` |
|
||||||
|
| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` |
|
||||||
|
| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` |
|
||||||
|
| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` |
|
||||||
|
| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` |
|
||||||
|
|
||||||
|
- **eld** - rare variant of aac codec
|
||||||
|
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||||
|
- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep
|
||||||
|
|
||||||
|
## Consumers (output)
|
||||||
|
|
||||||
|
| Format | Protocol | Send codecs | Recv codecs | Example |
|
||||||
|
|--------------|-------------|------------------------------|-------------------------|---------------------------------------|
|
||||||
|
| adts | http | aac | | `GET /api/stream.adts` |
|
||||||
|
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
|
||||||
|
| flv | http | h264,aac | | `GET /api/stream.flv` |
|
||||||
|
| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` |
|
||||||
|
| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` |
|
||||||
|
| homekit | homekit+udp | h264,opus | | Apple HomeKit app |
|
||||||
|
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
|
||||||
|
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
|
||||||
|
| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` |
|
||||||
|
| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` |
|
||||||
|
| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` |
|
||||||
|
| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` |
|
||||||
|
| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` |
|
||||||
|
| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` |
|
||||||
|
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
|
||||||
|
|
||||||
|
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
|
||||||
|
|
||||||
|
## Snapshots
|
||||||
|
|
||||||
|
| Format | Protocol | Send codecs | Example |
|
||||||
|
|--------|----------|-------------|-----------------------|
|
||||||
|
| jpeg | http | mjpeg | `GET /api/frame.jpeg` |
|
||||||
|
| mp4 | http | h264,hevc | `GET /api/frame.mp4` |
|
||||||
|
|
||||||
|
## Developers
|
||||||
|
|
||||||
|
File naming:
|
||||||
|
|
||||||
|
- `pkg/{format}/producer.go` - producer for this format (also if support backchannel)
|
||||||
|
- `pkg/{format}/consumer.go` - consumer for this format
|
||||||
|
- `pkg/{format}/backchanel.go` - producer with only backchannel func
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- https://www.wowza.com/blog/streaming-protocols
|
- https://www.wowza.com/blog/streaming-protocols
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate u
|
|||||||
sampleFreqIdx = rd.ReadBits8(4)
|
sampleFreqIdx = rd.ReadBits8(4)
|
||||||
if sampleFreqIdx == 0b1111 {
|
if sampleFreqIdx == 0b1111 {
|
||||||
sampleRate = rd.ReadBits(24)
|
sampleRate = rd.ReadBits(24)
|
||||||
|
} else {
|
||||||
|
sampleRate = sampleRates[sampleFreqIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
channels = rd.ReadBits8(4)
|
channels = rd.ReadBits8(4)
|
||||||
|
|||||||
@@ -41,3 +41,12 @@ func TestADTS(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, src[:len(dst)], dst)
|
require.Equal(t, src[:len(dst)], dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodeConfig(t *testing.T) {
|
||||||
|
conf := EncodeConfig(TypeAACLC, 48000, 1, false)
|
||||||
|
require.Equal(t, "1188", hex.EncodeToString(conf))
|
||||||
|
conf = EncodeConfig(TypeAACLC, 16000, 1, false)
|
||||||
|
require.Equal(t, "1408", hex.EncodeToString(conf))
|
||||||
|
conf = EncodeConfig(TypeAACLC, 8000, 1, false)
|
||||||
|
require.Equal(t, "1588", hex.EncodeToString(conf))
|
||||||
|
}
|
||||||
|
|||||||
+12
-11
@@ -8,15 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
wr *core.WriteBuffer
|
wr *core.WriteBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumer() *Consumer {
|
func NewConsumer() *Consumer {
|
||||||
cons := &Consumer{
|
medias := []*core.Media{
|
||||||
wr: core.NewWriteBuffer(nil),
|
|
||||||
}
|
|
||||||
cons.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionSendonly,
|
Direction: core.DirectionSendonly,
|
||||||
@@ -25,7 +22,16 @@ func NewConsumer() *Consumer {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cons
|
wr := core.NewWriteBuffer(nil)
|
||||||
|
return &Consumer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "adts",
|
||||||
|
Medias: medias,
|
||||||
|
Transport: wr,
|
||||||
|
},
|
||||||
|
wr: wr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
@@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
|||||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||||
return c.wr.WriteTo(wr)
|
return c.wr.WriteTo(wr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Stop() error {
|
|
||||||
_ = c.SuperConsumer.Close()
|
|
||||||
return c.wr.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-14
@@ -10,9 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Producer struct {
|
type Producer struct {
|
||||||
core.SuperProducer
|
core.Connection
|
||||||
rd *bufio.Reader
|
rd *bufio.Reader
|
||||||
cl io.Closer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(r io.Reader) (*Producer, error) {
|
func Open(r io.Reader) (*Producer, error) {
|
||||||
@@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
codec := ADTSToCodec(b)
|
medias := []*core.Media{
|
||||||
|
|
||||||
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
|
||||||
prod.Type = "ADTS producer"
|
|
||||||
prod.Medias = []*core.Media{
|
|
||||||
{
|
{
|
||||||
Kind: core.KindAudio,
|
Kind: core.KindAudio,
|
||||||
Direction: core.DirectionRecvonly,
|
Direction: core.DirectionRecvonly,
|
||||||
Codecs: []*core.Codec{codec},
|
Codecs: []*core.Codec{ADTSToCodec(b)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return prod, nil
|
return &Producer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "adts",
|
||||||
|
Medias: medias,
|
||||||
|
Transport: r,
|
||||||
|
},
|
||||||
|
rd: rd,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Producer) Start() error {
|
func (c *Producer) Start() error {
|
||||||
@@ -66,8 +69,3 @@ func (c *Producer) Start() error {
|
|||||||
c.Receivers[0].WriteRTP(pkt)
|
c.Receivers[0].WriteRTP(pkt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Producer) Stop() error {
|
|
||||||
_ = c.SuperProducer.Close()
|
|
||||||
return c.cl.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
+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 (
|
||||||
|
|||||||
+10
-5
@@ -65,11 +65,16 @@ func (c *Client) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
info := &core.Info{
|
info := &core.Connection{
|
||||||
Type: "Bubble active producer",
|
ID: core.ID(c),
|
||||||
Medias: c.medias,
|
FormatName: "bubble",
|
||||||
Recv: c.recv,
|
Protocol: "http",
|
||||||
Receivers: c.receivers,
|
Medias: c.medias,
|
||||||
|
Recv: c.recv,
|
||||||
|
Receivers: c.receivers,
|
||||||
|
}
|
||||||
|
if c.conn != nil {
|
||||||
|
info.RemoteAddr = c.conn.RemoteAddr().String()
|
||||||
}
|
}
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-20
@@ -2,8 +2,8 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -18,34 +18,76 @@ type Codec struct {
|
|||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) String() string {
|
// MarshalJSON - return FFprobe compatible output
|
||||||
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
|
func (c *Codec) MarshalJSON() ([]byte, error) {
|
||||||
|
info := map[string]any{}
|
||||||
|
if name := FFmpegCodecName(c.Name); name != "" {
|
||||||
|
info["codec_name"] = name
|
||||||
|
info["codec_type"] = c.Kind()
|
||||||
|
}
|
||||||
|
if c.Name == CodecH264 {
|
||||||
|
profile, level := DecodeH264(c.FmtpLine)
|
||||||
|
if profile != "" {
|
||||||
|
info["profile"] = profile
|
||||||
|
info["level"] = level
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||||
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
|
info["sample_rate"] = c.ClockRate
|
||||||
}
|
}
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
info["channels"] = c.Channels
|
||||||
}
|
}
|
||||||
return s
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Text() string {
|
func FFmpegCodecName(name string) string {
|
||||||
switch c.Name {
|
switch name {
|
||||||
case CodecH264:
|
case CodecH264:
|
||||||
if profile := DecodeH264(c.FmtpLine); profile != "" {
|
return "h264"
|
||||||
return "H.264 " + profile
|
case CodecH265:
|
||||||
}
|
return "hevc"
|
||||||
return c.Name
|
case CodecJPEG:
|
||||||
|
return "mjpeg"
|
||||||
|
case CodecRAW:
|
||||||
|
return "rawvideo"
|
||||||
|
case CodecPCMA:
|
||||||
|
return "pcm_alaw"
|
||||||
|
case CodecPCMU:
|
||||||
|
return "pcm_mulaw"
|
||||||
|
case CodecPCM:
|
||||||
|
return "pcm_s16be"
|
||||||
|
case CodecPCML:
|
||||||
|
return "pcm_s16le"
|
||||||
|
case CodecAAC:
|
||||||
|
return "aac"
|
||||||
|
case CodecOpus:
|
||||||
|
return "opus"
|
||||||
|
case CodecVP8:
|
||||||
|
return "vp8"
|
||||||
|
case CodecVP9:
|
||||||
|
return "vp9"
|
||||||
|
case CodecAV1:
|
||||||
|
return "av1"
|
||||||
|
case CodecELD:
|
||||||
|
return "aac/eld"
|
||||||
|
case CodecFLAC:
|
||||||
|
return "flac"
|
||||||
|
case CodecMP3:
|
||||||
|
return "mp3"
|
||||||
}
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
s := c.Name
|
func (c *Codec) String() (s string) {
|
||||||
|
s = c.Name
|
||||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||||
s += "/" + strconv.Itoa(int(c.ClockRate))
|
s += fmt.Sprintf("/%d", c.ClockRate)
|
||||||
}
|
}
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
s += "/" + strconv.Itoa(int(c.Channels))
|
s += fmt.Sprintf("/%d", c.Channels)
|
||||||
}
|
}
|
||||||
return s
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) IsRTP() bool {
|
func (c *Codec) IsRTP() bool {
|
||||||
@@ -181,10 +223,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeH264(fmtp string) string {
|
func DecodeH264(fmtp string) (profile string, level byte) {
|
||||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||||
var profile string
|
|
||||||
switch sps[1] {
|
switch sps[1] {
|
||||||
case 0x42:
|
case 0x42:
|
||||||
profile = "Baseline"
|
profile = "Baseline"
|
||||||
@@ -198,8 +239,8 @@ func DecodeH264(fmtp string) string {
|
|||||||
profile = fmt.Sprintf("0x%02X", sps[1])
|
profile = fmt.Sprintf("0x%02X", sps[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10)
|
level = sps[3]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewID() uint32 {
|
||||||
|
return id.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use NewID instead
|
||||||
|
func ID(v any) uint32 {
|
||||||
|
p := uintptr(reflect.ValueOf(v).UnsafePointer())
|
||||||
|
return 0x8000_0000 | uint32(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
var id atomic.Uint32
|
||||||
|
|
||||||
|
type Info interface {
|
||||||
|
SetProtocol(string)
|
||||||
|
SetRemoteAddr(string)
|
||||||
|
SetSource(string)
|
||||||
|
SetURL(string)
|
||||||
|
WithRequest(*http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection just like webrtc.PeerConnection
|
||||||
|
// - ID and RemoteAddr used for building Connection(s) graph
|
||||||
|
// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection
|
||||||
|
// - FormatName and Protocol has FFmpeg compatible names
|
||||||
|
// - Transport used for auto closing on Stop
|
||||||
|
type Connection struct {
|
||||||
|
ID uint32 `json:"id,omitempty"`
|
||||||
|
FormatName string `json:"format_name,omitempty"` // rtsp, webrtc, mp4, mjpeg, mpjpeg...
|
||||||
|
Protocol string `json:"protocol,omitempty"` // tcp, udp, http, ws, pipe...
|
||||||
|
RemoteAddr string `json:"remote_addr,omitempty"` // host:port other info
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
SDP string `json:"sdp,omitempty"`
|
||||||
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
|
||||||
|
Medias []*Media `json:"medias,omitempty"`
|
||||||
|
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||||
|
Senders []*Sender `json:"senders,omitempty"`
|
||||||
|
Recv int `json:"bytes_recv,omitempty"`
|
||||||
|
Send int `json:"bytes_send,omitempty"`
|
||||||
|
|
||||||
|
Transport any `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) GetMedias() []*Media {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
||||||
|
for _, receiver := range c.Receivers {
|
||||||
|
if receiver.Codec == codec {
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiver := NewReceiver(media, codec)
|
||||||
|
c.Receivers = append(c.Receivers, receiver)
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Stop() error {
|
||||||
|
for _, receiver := range c.Receivers {
|
||||||
|
receiver.Close()
|
||||||
|
}
|
||||||
|
for _, sender := range c.Senders {
|
||||||
|
sender.Close()
|
||||||
|
}
|
||||||
|
if closer, ok := c.Transport.(io.Closer); ok {
|
||||||
|
return closer.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated:
|
||||||
|
func (c *Connection) Codecs() []*Codec {
|
||||||
|
codecs := make([]*Codec, len(c.Senders))
|
||||||
|
for i, sender := range c.Senders {
|
||||||
|
codecs[i] = sender.Codec
|
||||||
|
}
|
||||||
|
return codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) SetProtocol(s string) {
|
||||||
|
c.Protocol = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) SetRemoteAddr(s string) {
|
||||||
|
if c.RemoteAddr == "" {
|
||||||
|
c.RemoteAddr = s
|
||||||
|
} else {
|
||||||
|
c.RemoteAddr += " forwarded " + s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) SetSource(s string) {
|
||||||
|
c.Source = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) SetURL(s string) {
|
||||||
|
c.URL = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) WithRequest(r *http.Request) {
|
||||||
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
|
c.Protocol = "ws"
|
||||||
|
} else {
|
||||||
|
c.Protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.RemoteAddr = r.RemoteAddr
|
||||||
|
if remote := r.Header.Get("X-Forwarded-For"); remote != "" {
|
||||||
|
c.RemoteAddr += " forwarded " + remote
|
||||||
|
}
|
||||||
|
|
||||||
|
c.UserAgent = r.UserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create like os.Create, init Consumer with existing Transport
|
||||||
|
func Create(w io.Writer) (*Connection, error) {
|
||||||
|
return &Connection{Transport: w}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open like os.Open, init Producer from existing Transport
|
||||||
|
func Open(r io.Reader) (*Connection, error) {
|
||||||
|
return &Connection{Transport: r}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial like net.Dial, init Producer via Dialing
|
||||||
|
func Dial(rawURL string) (*Connection, error) {
|
||||||
|
return &Connection{}, nil
|
||||||
|
}
|
||||||
+5
-85
@@ -1,5 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DirectionRecvonly = "recvonly"
|
DirectionRecvonly = "recvonly"
|
||||||
DirectionSendonly = "sendonly"
|
DirectionSendonly = "sendonly"
|
||||||
@@ -18,6 +20,7 @@ const (
|
|||||||
CodecVP9 = "VP9"
|
CodecVP9 = "VP9"
|
||||||
CodecAV1 = "AV1"
|
CodecAV1 = "AV1"
|
||||||
CodecJPEG = "JPEG" // payloadType: 26
|
CodecJPEG = "JPEG" // payloadType: 26
|
||||||
|
CodecRAW = "RAW"
|
||||||
|
|
||||||
CodecPCMU = "PCMU" // payloadType: 0
|
CodecPCMU = "PCMU" // payloadType: 0
|
||||||
CodecPCMA = "PCMA" // payloadType: 8
|
CodecPCMA = "PCMA" // payloadType: 8
|
||||||
@@ -89,89 +92,6 @@ func (m Mode) String() string {
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
func (m Mode) MarshalJSON() ([]byte, error) {
|
||||||
Type string `json:"type,omitempty"`
|
return json.Marshal(m.String())
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
|
||||||
UserAgent string `json:"user_agent,omitempty"`
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
|
||||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
|
||||||
Senders []*Sender `json:"senders,omitempty"`
|
|
||||||
Recv int `json:"recv,omitempty"`
|
|
||||||
Send int `json:"send,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnsupportedCodec = "unsupported codec"
|
|
||||||
WrongMediaDirection = "wrong media direction"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SuperProducer struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
|
||||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
|
||||||
Recv int `json:"recv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperProducer) GetMedias() []*Media {
|
|
||||||
return s.Medias
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
|
||||||
for _, receiver := range s.Receivers {
|
|
||||||
if receiver.Codec == codec {
|
|
||||||
return receiver, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
receiver := NewReceiver(media, codec)
|
|
||||||
s.Receivers = append(s.Receivers, receiver)
|
|
||||||
return receiver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperProducer) Close() error {
|
|
||||||
for _, receiver := range s.Receivers {
|
|
||||||
receiver.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SuperConsumer struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
|
||||||
UserAgent string `json:"user_agent,omitempty"`
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
|
||||||
Senders []*Sender `json:"senders,omitempty"`
|
|
||||||
Send int `json:"send,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) GetMedias() []*Media {
|
|
||||||
return s.Medias
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) {
|
|
||||||
// return 0, nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) Close() error {
|
|
||||||
for _, sender := range s.Senders {
|
|
||||||
sender.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SuperConsumer) Codecs() []*Codec {
|
|
||||||
codecs := make([]*Codec, len(s.Senders))
|
|
||||||
for i, sender := range s.Senders {
|
|
||||||
codecs[i] = sender.Codec
|
|
||||||
}
|
|
||||||
return codecs
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type producer struct {
|
||||||
|
Medias []*Media
|
||||||
|
Receivers []*Receiver
|
||||||
|
|
||||||
|
id byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) GetMedias() []*Media {
|
||||||
|
return p.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) {
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
if receiver.Codec == codec {
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiver := NewReceiver(nil, codec)
|
||||||
|
p.Receivers = append(p.Receivers, receiver)
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) Start() error {
|
||||||
|
pkt := &Packet{Payload: []byte{p.id}}
|
||||||
|
p.Receivers[0].Input(pkt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *producer) Stop() error {
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
receiver.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type consumer struct {
|
||||||
|
Medias []*Media
|
||||||
|
Senders []*Sender
|
||||||
|
|
||||||
|
cache chan byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) GetMedias() []*Media {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error {
|
||||||
|
c.cache = make(chan byte, 1)
|
||||||
|
sender := NewSender(nil, track.Codec)
|
||||||
|
sender.Output = func(packet *Packet) {
|
||||||
|
c.cache <- packet.Payload[0]
|
||||||
|
}
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
c.Senders = append(c.Senders, sender)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) Stop() error {
|
||||||
|
for _, sender := range c.Senders {
|
||||||
|
sender.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consumer) read() byte {
|
||||||
|
return <-c.cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestName(t *testing.T) {
|
||||||
|
GetProducer := func(b byte) Producer {
|
||||||
|
return &producer{
|
||||||
|
Medias: []*Media{
|
||||||
|
{
|
||||||
|
Kind: KindVideo,
|
||||||
|
Direction: DirectionRecvonly,
|
||||||
|
Codecs: []*Codec{
|
||||||
|
{Name: CodecH264},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage1
|
||||||
|
prod1 := GetProducer(1)
|
||||||
|
cons2 := &consumer{}
|
||||||
|
|
||||||
|
media1 := prod1.GetMedias()[0]
|
||||||
|
track1, _ := prod1.GetTrack(media1, media1.Codecs[0])
|
||||||
|
|
||||||
|
_ = cons2.AddTrack(nil, nil, track1)
|
||||||
|
|
||||||
|
_ = prod1.Start()
|
||||||
|
require.Equal(t, byte(1), cons2.read())
|
||||||
|
|
||||||
|
// stage2
|
||||||
|
prod2 := GetProducer(2)
|
||||||
|
media2 := prod2.GetMedias()[0]
|
||||||
|
require.NotEqual(t, fmt.Sprintf("%p", media1), fmt.Sprintf("%p", media2))
|
||||||
|
track2, _ := prod2.GetTrack(media2, media2.Codecs[0])
|
||||||
|
track1.Replace(track2)
|
||||||
|
|
||||||
|
_ = prod1.Stop()
|
||||||
|
|
||||||
|
_ = prod2.Start()
|
||||||
|
require.Equal(t, byte(2), cons2.read())
|
||||||
|
|
||||||
|
// stage3
|
||||||
|
_ = prod2.Stop()
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ func RandString(size, base byte) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Before(s, sep string) string {
|
||||||
|
if i := strings.Index(s, sep); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func Between(s, sub1, sub2 string) string {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ type Media struct {
|
|||||||
func (m *Media) String() string {
|
func (m *Media) String() string {
|
||||||
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
||||||
for _, codec := range m.Codecs {
|
for _, codec := range m.Codecs {
|
||||||
name := codec.Text()
|
name := codec.String()
|
||||||
|
|
||||||
if strings.Contains(s, name) {
|
if strings.Contains(s, name) {
|
||||||
continue
|
continue
|
||||||
@@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool {
|
|||||||
|
|
||||||
func GetKind(name string) string {
|
func GetKind(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:
|
||||||
return KindVideo
|
return KindVideo
|
||||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
//type Packet struct {
|
||||||
|
// Payload []byte
|
||||||
|
// Timestamp uint32 // PTS if DTS == 0 else DTS
|
||||||
|
// Composition uint32 // CTS = PTS-DTS (for support B-frames)
|
||||||
|
// Sequence uint16
|
||||||
|
//}
|
||||||
|
|
||||||
|
type Packet = rtp.Packet
|
||||||
|
|
||||||
|
// HandlerFunc - process input packets (just like http.HandlerFunc)
|
||||||
|
type HandlerFunc func(packet *Packet)
|
||||||
|
|
||||||
|
// Filter - a decorator for any HandlerFunc
|
||||||
|
type Filter func(handler HandlerFunc) HandlerFunc
|
||||||
|
|
||||||
|
// Node - Receiver or Sender or Filter (transform)
|
||||||
|
type Node struct {
|
||||||
|
Codec *Codec
|
||||||
|
Input HandlerFunc
|
||||||
|
Output HandlerFunc
|
||||||
|
|
||||||
|
id uint32
|
||||||
|
childs []*Node
|
||||||
|
parent *Node
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) WithParent(parent *Node) *Node {
|
||||||
|
parent.AppendChild(n)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) AppendChild(child *Node) {
|
||||||
|
n.mu.Lock()
|
||||||
|
n.childs = append(n.childs, child)
|
||||||
|
n.mu.Unlock()
|
||||||
|
|
||||||
|
child.parent = n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) RemoveChild(child *Node) {
|
||||||
|
n.mu.Lock()
|
||||||
|
for i, ch := range n.childs {
|
||||||
|
if ch == child {
|
||||||
|
n.childs = append(n.childs[:i], n.childs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Close() {
|
||||||
|
if parent := n.parent; parent != nil {
|
||||||
|
parent.RemoveChild(n)
|
||||||
|
|
||||||
|
if len(parent.childs) == 0 {
|
||||||
|
parent.Close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, childs := range n.childs {
|
||||||
|
childs.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveNode(dst, src *Node) {
|
||||||
|
src.mu.Lock()
|
||||||
|
childs := src.childs
|
||||||
|
src.childs = nil
|
||||||
|
src.mu.Unlock()
|
||||||
|
|
||||||
|
dst.mu.Lock()
|
||||||
|
dst.childs = childs
|
||||||
|
dst.mu.Unlock()
|
||||||
|
|
||||||
|
for _, child := range childs {
|
||||||
|
child.parent = dst
|
||||||
|
}
|
||||||
|
}
|
||||||
+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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Dial(rawURL string) (core.Producer, error) {
|
func Dial(rawURL string) (*mpegts.Producer, error) {
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -32,7 +31,15 @@ func Dial(rawURL string) (core.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return mpegts.Open(r)
|
prod, err := mpegts.Open(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prod.FormatName = "gopro"
|
||||||
|
prod.RemoteAddr = u.Host
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type listener struct {
|
type listener struct {
|
||||||
+134
-1
@@ -1,6 +1,10 @@
|
|||||||
package h264
|
package h264
|
||||||
|
|
||||||
import "github.com/AlexxIT/go2rtc/pkg/bits"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/bits"
|
||||||
|
)
|
||||||
|
|
||||||
// http://www.itu.int/rec/T-REC-H.264
|
// http://www.itu.int/rec/T-REC-H.264
|
||||||
// https://webrtc.googlesource.com/src/+/refs/heads/main/common_video/h264/sps_parser.cc
|
// https://webrtc.googlesource.com/src/+/refs/heads/main/common_video/h264/sps_parser.cc
|
||||||
@@ -229,3 +233,132 @@ func (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SPS) Profile() string {
|
||||||
|
switch s.profile_idc {
|
||||||
|
case 0x42:
|
||||||
|
return "Baseline"
|
||||||
|
case 0x4D:
|
||||||
|
return "Main"
|
||||||
|
case 0x58:
|
||||||
|
return "Extended"
|
||||||
|
case 0x64:
|
||||||
|
return "High"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("0x%02X", s.profile_idc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SPS) PixFmt() string {
|
||||||
|
if s.bit_depth_luma_minus8 == 0 {
|
||||||
|
switch s.chroma_format_idc {
|
||||||
|
case 1:
|
||||||
|
if s.video_full_range_flag == 1 {
|
||||||
|
return "yuvj420p"
|
||||||
|
}
|
||||||
|
return "yuv420p"
|
||||||
|
case 2:
|
||||||
|
return "yuv422p"
|
||||||
|
case 3:
|
||||||
|
return "yuv444p"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SPS) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s %d.%d, %s, %dx%d",
|
||||||
|
s.Profile(), s.level_idc/10, s.level_idc%10, s.PixFmt(), s.Width(), s.Height(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FixPixFmt - change yuvj420p to yuv420p in SPS
|
||||||
|
// same as "-c:v copy -bsf:v h264_metadata=video_full_range_flag=0"
|
||||||
|
func FixPixFmt(sps []byte) {
|
||||||
|
r := bits.NewReader(sps)
|
||||||
|
|
||||||
|
_ = r.ReadByte()
|
||||||
|
|
||||||
|
profile := r.ReadByte()
|
||||||
|
_ = r.ReadByte()
|
||||||
|
_ = r.ReadByte()
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
|
||||||
|
switch profile {
|
||||||
|
case 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135:
|
||||||
|
n := byte(8)
|
||||||
|
|
||||||
|
if r.ReadUEGolomb() == 3 {
|
||||||
|
_ = r.ReadBit()
|
||||||
|
n = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadBit()
|
||||||
|
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
for i := byte(0); i < n; i++ {
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
return // skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
|
||||||
|
switch r.ReadUEGolomb() {
|
||||||
|
case 0:
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
case 1:
|
||||||
|
_ = r.ReadBit()
|
||||||
|
_ = r.ReadSEGolomb()
|
||||||
|
_ = r.ReadSEGolomb()
|
||||||
|
|
||||||
|
n := r.ReadUEGolomb()
|
||||||
|
for i := uint32(0); i < n; i++ {
|
||||||
|
_ = r.ReadSEGolomb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadBit()
|
||||||
|
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
|
||||||
|
if r.ReadBit() == 0 {
|
||||||
|
_ = r.ReadBit()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.ReadBit()
|
||||||
|
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
_ = r.ReadUEGolomb()
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
if r.ReadByte() == 255 {
|
||||||
|
_ = r.ReadUint16()
|
||||||
|
_ = r.ReadUint16()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
_ = r.ReadBit()
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ReadBit() != 0 {
|
||||||
|
_ = r.ReadBits8(3)
|
||||||
|
if r.ReadBit() == 1 {
|
||||||
|
pos, bit := r.Pos()
|
||||||
|
sps[pos] &= ^byte(1 << bit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ type Stream struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewStream(
|
func NewStream(
|
||||||
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session,
|
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec,
|
||||||
|
videoSession, audioSession *srtp.Session, bitrate int,
|
||||||
) (*Stream, error) {
|
) (*Stream, error) {
|
||||||
stream := &Stream{
|
stream := &Stream{
|
||||||
id: core.RandString(16, 0),
|
id: core.RandString(16, 0),
|
||||||
@@ -30,11 +31,17 @@ func NewStream(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bitrate != 0 {
|
||||||
|
bitrate /= 1024 // convert bps to kbps
|
||||||
|
} else {
|
||||||
|
bitrate = 4096 // default kbps for general FullHD camera
|
||||||
|
}
|
||||||
|
|
||||||
videoCodec.RTPParams = []RTPParams{
|
videoCodec.RTPParams = []RTPParams{
|
||||||
{
|
{
|
||||||
PayloadType: 99,
|
PayloadType: 99,
|
||||||
SSRC: videoSession.Local.SSRC,
|
SSRC: videoSession.Local.SSRC,
|
||||||
MaxBitrate: 299,
|
MaxBitrate: uint16(bitrate), // iPhone query 299Kbps, iPad/AppleTV query 802Kbps
|
||||||
RTCPInterval: 0.5,
|
RTCPInterval: 0.5,
|
||||||
MaxMTU: []uint16{1378},
|
MaxMTU: []uint16{1378},
|
||||||
},
|
},
|
||||||
@@ -43,7 +50,7 @@ func NewStream(
|
|||||||
{
|
{
|
||||||
PayloadType: 110,
|
PayloadType: 110,
|
||||||
SSRC: audioSession.Local.SSRC,
|
SSRC: audioSession.Local.SSRC,
|
||||||
MaxBitrate: 24,
|
MaxBitrate: 24, // any iDevice query 24Kbps (this is OK for 16KHz and 1 channel)
|
||||||
RTCPInterval: 5,
|
RTCPInterval: 5,
|
||||||
|
|
||||||
ComfortNoisePayloadType: []uint8{13},
|
ComfortNoisePayloadType: []uint8{13},
|
||||||
|
|||||||
+3
-1
@@ -61,8 +61,10 @@ func NewClient(rawURL string) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn := webrtc.NewConn(pc)
|
conn := webrtc.NewConn(pc)
|
||||||
conn.Desc = "Hass"
|
conn.FormatName = "hass/webrtc"
|
||||||
conn.Mode = core.ModeActiveProducer
|
conn.Mode = core.ModeActiveProducer
|
||||||
|
conn.Protocol = "ws"
|
||||||
|
conn.URL = rawURL
|
||||||
|
|
||||||
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
|
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
|
||||||
medias := []*core.Media{
|
medias := []*core.Media{
|
||||||
|
|||||||
+8
-3
@@ -4,14 +4,19 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OpenURL(u *url.URL, body io.ReadCloser) (core.Producer, error) {
|
func OpenURL(u *url.URL, body io.ReadCloser) (*mpegts.Producer, error) {
|
||||||
rd, err := NewReader(u, body)
|
rd, err := NewReader(u, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return mpegts.Open(rd)
|
prod, err := mpegts.Open(rd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prod.FormatName = "hls/mpegts"
|
||||||
|
prod.RemoteAddr = u.Host
|
||||||
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-24
@@ -3,7 +3,7 @@ package homekit
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
core.SuperConsumer
|
core.Connection
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
srtp *srtp.Server
|
srtp *srtp.Server
|
||||||
|
|
||||||
@@ -29,28 +29,31 @@ type Consumer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
|
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
|
||||||
return &Consumer{
|
medias := []*core.Media{
|
||||||
SuperConsumer: core.SuperConsumer{
|
{
|
||||||
Type: "HomeKit passive consumer",
|
Kind: core.KindVideo,
|
||||||
RemoteAddr: conn.RemoteAddr().String(),
|
Direction: core.DirectionSendonly,
|
||||||
Medias: []*core.Media{
|
Codecs: []*core.Codec{
|
||||||
{
|
{Name: core.CodecH264},
|
||||||
Kind: core.KindVideo,
|
|
||||||
Direction: core.DirectionSendonly,
|
|
||||||
Codecs: []*core.Codec{
|
|
||||||
{Name: core.CodecH264},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: core.KindAudio,
|
|
||||||
Direction: core.DirectionSendonly,
|
|
||||||
Codecs: []*core.Codec{
|
|
||||||
{Name: core.CodecOpus},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecOpus},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &Consumer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "homekit",
|
||||||
|
Protocol: "udp",
|
||||||
|
RemoteAddr: conn.RemoteAddr().String(),
|
||||||
|
Medias: medias,
|
||||||
|
Transport: conn,
|
||||||
|
},
|
||||||
conn: conn,
|
conn: conn,
|
||||||
srtp: server,
|
srtp: server,
|
||||||
}
|
}
|
||||||
@@ -175,11 +178,10 @@ func (c *Consumer) WriteTo(io.Writer) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Stop() error {
|
func (c *Consumer) Stop() error {
|
||||||
_ = c.SuperConsumer.Close()
|
|
||||||
if c.deadline != nil {
|
if c.deadline != nil {
|
||||||
c.deadline.Reset(0)
|
c.deadline.Reset(0)
|
||||||
}
|
}
|
||||||
return c.SuperConsumer.Close()
|
return c.Connection.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) srtpEndpoint() *srtp.Endpoint {
|
func (c *Consumer) srtpEndpoint() *srtp.Endpoint {
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated: rename to Producer
|
||||||
type Client struct {
|
type Client struct {
|
||||||
core.SuperProducer
|
core.Connection
|
||||||
|
|
||||||
hap *hap.Client
|
hap *hap.Client
|
||||||
srtp *srtp.Server
|
srtp *srtp.Server
|
||||||
@@ -28,6 +29,8 @@ type Client struct {
|
|||||||
audioSession *srtp.Session
|
audioSession *srtp.Session
|
||||||
|
|
||||||
stream *camera.Stream
|
stream *camera.Stream
|
||||||
|
|
||||||
|
Bitrate int // in bits/s
|
||||||
}
|
}
|
||||||
|
|
||||||
func Dial(rawURL string, server *srtp.Server) (*Client, error) {
|
func Dial(rawURL string, server *srtp.Server) (*Client, error) {
|
||||||
@@ -50,9 +53,13 @@ func Dial(rawURL string, server *srtp.Server) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
SuperProducer: core.SuperProducer{
|
Connection: core.Connection{
|
||||||
Type: "HomeKit active producer",
|
ID: core.NewID(),
|
||||||
URL: conn.URL(),
|
FormatName: "homekit",
|
||||||
|
Protocol: "udp",
|
||||||
|
RemoteAddr: conn.Conn.RemoteAddr().String(),
|
||||||
|
Source: rawURL,
|
||||||
|
Transport: conn,
|
||||||
},
|
},
|
||||||
hap: conn,
|
hap: conn,
|
||||||
srtp: server,
|
srtp: server,
|
||||||
@@ -91,7 +98,6 @@ func (c *Client) GetMedias() []*core.Media {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.URL = c.hap.URL()
|
|
||||||
c.SDP = fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig)
|
c.SDP = fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig)
|
||||||
|
|
||||||
c.Medias = []*core.Media{
|
c.Medias = []*core.Media{
|
||||||
@@ -132,7 +138,7 @@ func (c *Client) Start() error {
|
|||||||
c.audioSession = &srtp.Session{Local: c.srtpEndpoint()}
|
c.audioSession = &srtp.Session{Local: c.srtpEndpoint()}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession)
|
c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession, c.Bitrate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -173,8 +179,6 @@ func (c *Client) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stop() error {
|
func (c *Client) Stop() error {
|
||||||
_ = c.SuperProducer.Close()
|
|
||||||
|
|
||||||
if c.videoSession != nil && c.videoSession.Remote != nil {
|
if c.videoSession != nil && c.videoSession.Remote != nil {
|
||||||
c.srtp.DelSession(c.videoSession)
|
c.srtp.DelSession(c.videoSession)
|
||||||
}
|
}
|
||||||
@@ -182,7 +186,7 @@ func (c *Client) Stop() error {
|
|||||||
c.srtp.DelSession(c.audioSession)
|
c.srtp.DelSession(c.audioSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.hap.Close()
|
return c.Connection.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) trackByKind(kind string) *core.Receiver {
|
func (c *Client) trackByKind(kind string) *core.Receiver {
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Producer struct {
|
||||||
|
core.Connection
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
res *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(res *http.Response) (*Producer, error) {
|
||||||
|
return &Producer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "image",
|
||||||
|
Protocol: "http",
|
||||||
|
RemoteAddr: res.Request.URL.Host,
|
||||||
|
Transport: res.Body,
|
||||||
|
Medias: []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindVideo,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{
|
||||||
|
Name: core.CodecJPEG,
|
||||||
|
ClockRate: 90000,
|
||||||
|
PayloadType: core.PayloadTypeRAW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Producer) Start() error {
|
||||||
|
body, err := io.ReadAll(c.res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := &rtp.Packet{
|
||||||
|
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||||
|
Payload: body,
|
||||||
|
}
|
||||||
|
c.Receivers[0].WriteRTP(pkt)
|
||||||
|
|
||||||
|
c.Recv += len(body)
|
||||||
|
|
||||||
|
req := c.res.Request
|
||||||
|
|
||||||
|
for !c.closed {
|
||||||
|
res, err := tcp.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("wrong status: " + res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Recv += len(body)
|
||||||
|
|
||||||
|
pkt = &rtp.Packet{
|
||||||
|
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||||
|
Payload: body,
|
||||||
|
}
|
||||||
|
c.Receivers[0].WriteRTP(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Producer) Stop() error {
|
||||||
|
c.closed = true
|
||||||
|
return c.Connection.Stop()
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user