Compare commits

..

94 Commits

Author SHA1 Message Date
Alexey Khit
da92256910 Update version to 1.5.0 2023-05-04 15:04:06 +03:00
Alexey Khit
035b824645 Update readme 2023-05-04 15:03:03 +03:00
Alexey Khit
2a91c4625a Add ALSA support inside docker 2023-05-04 13:47:12 +03:00
Alexey Khit
23dd5b450c Add support H265 codec for MPEG-TS 2023-05-04 12:26:56 +03:00
Alexey Khit
f617c148cd Fix FFmpeg template for H265 2023-05-04 11:57:32 +03:00
Alexey Khit
b5f4c7f75b Rewrite exec pipe, TCP and HTTP sources 2023-05-04 11:56:56 +03:00
Alexey Khit
d44efb84a0 Fix buffer size for mpegts 2023-05-04 11:49:38 +03:00
Alexey Khit
03968d2f2e Restore hadrware transcoding for MJPEG 2023-05-04 07:39:15 +03:00
Alexey Khit
3c371e7046 Change FFmpeg output for MJPEG to pipe 2023-05-04 07:38:49 +03:00
Alexey Khit
4656086985 Add auto transcoding to JPEG snapshot 2023-05-04 06:49:54 +03:00
Alexey Khit
e78f9fa69d Add support pipe to exec source 2023-05-04 06:49:54 +03:00
Alexey Khit
2e8be342ef Rework FFmpeg hardware support 2023-05-04 01:24:37 +03:00
Alexey Khit
5387e88fe3 Rework FFmpeg devices support 2023-05-04 00:03:01 +03:00
Alexey Khit
1746f55eda Add pix_fmt to ffmpeg h264 transcoding 2023-05-03 23:57:39 +03:00
Alexey Khit
4d53889519 Improve support ONVIF client 2023-05-03 08:02:56 +03:00
Alexey Khit
6d9d89bbe3 Fix support 2 way audio for Reolink Doorbell #331 2023-05-03 08:01:33 +03:00
Alexey Khit
c1923627c0 Fix panic on Producer GetMedias 2023-05-03 07:57:00 +03:00
Alexey Khit
95ca5f5fe1 Remove unnecessary run.sh file 2023-05-02 14:20:55 +03:00
Alexey Khit
4bbd3a1cd2 Fix ONVIF discovery for buggy camera 2023-05-02 11:25:00 +03:00
Alexey Khit
9c8a1d8b19 Add path to ONVIF requests 2023-05-02 11:07:12 +03:00
Alexey Khit
53967fc72a Update ONVIF discovery request #397 2023-05-02 09:36:38 +03:00
Alexey Khit
31f870e950 Update internal readme 2023-05-01 15:09:58 +03:00
Alexey Khit
c7d228daff Remove mp4 pkg dependency from rtsp pkg 2023-05-01 14:33:03 +03:00
Alexey Khit
378f071e2c Add go2rtc_rtsp app 2023-05-01 12:55:32 +03:00
Alexey Khit
75f61b38ac Move cmd module to internal 2023-05-01 12:55:14 +03:00
Alexey Khit
bc770f1a85 Remove FFmpeg buffer because have problems with MJPEG 2023-04-29 17:04:56 +03:00
Alexey Khit
d276311fcf Add support insecure HTTPS client 2023-04-29 17:00:52 +03:00
Alexey Khit
1e14dc9ab2 Add ONVIF client and server support 2023-04-29 15:12:59 +03:00
Alexey Khit
8dbaa4ba93 Fix RTSP client Session processing 2023-04-29 13:48:17 +03:00
Alexey Khit
f0893bd78b Fix bug in SDP from Annke CZ400 #384 2023-04-27 14:02:55 +03:00
Alexey Khit
6247746177 Change localhost to 127.0.0.1 2023-04-25 06:21:50 +03:00
Alexey Khit
a20de73ab2 Add pkt_size option fort RTSP server 2023-04-24 06:40:11 +03:00
Alexey Khit
813c8b3b3d Make core atoi func public 2023-04-24 06:39:07 +03:00
Alexey Khit
63d9c6c2b7 Fix Chinese cameras with wrong Session header after v1.4.0 #382 2023-04-23 20:42:49 +03:00
Alexey Khit
2610f15eb6 Update readme about codecs 2023-04-23 08:08:52 +03:00
Alexey Khit
9268acf1ca Update version to 1.4.0 2023-04-23 08:08:16 +03:00
Alexey Khit
55fdf1a647 Fix RTSP server handler for some Cloud clients #347 2023-04-22 21:04:44 +03:00
Alexey Khit
5fe07aeea0 Fix FLV to RTSP transport after v1.3 #362 2023-04-22 21:04:26 +03:00
Alexey Khit
e8b22bca99 Fix RTSP server close (panic) without client request #364 2023-04-22 21:04:13 +03:00
Alexey Khit
5926c1deb9 Fix default sample rate for MP3 codec 2023-04-22 18:15:20 +03:00
Alexey Khit
dd98edc48e Add support resampling for PCM for WebRTC 2023-04-22 08:54:31 +03:00
Alexey Khit
fb1cc7dfc2 Update FFmpeg OPUS params 2023-04-22 08:53:35 +03:00
Alexey Khit
7626a09c1c Fix unsupported FLAC encoder params 2023-04-22 08:53:08 +03:00
Alexey Khit
db85533e74 Add more sample rates to FLAC encoder 2023-04-22 08:52:32 +03:00
Alexey Khit
5939c8acba Update MP4 links query 2023-04-20 21:47:28 +03:00
Alexey Khit
e985ad23a2 Fix HLS handler 2023-04-20 21:47:21 +03:00
Alexey Khit
7452eb5e05 Add support FLAC codec to MP4/MSE 2023-04-20 21:32:46 +03:00
Alexey Khit
5f9788209d Move MP4 mutex from HTTP to Muxer 2023-04-20 13:20:52 +03:00
Alexey Khit
c07ddb8309 Add HTTP 500 error response for MP4 API 2023-04-20 13:16:15 +03:00
Alexey Khit
79f1dcfea3 Update version to 1.3.2 2023-04-17 17:03:12 +03:00
Alexey Khit
3feaf852af Fix panic for wrong ffmpeg device in linux 2023-04-17 17:02:24 +03:00
Alexey Khit
76ec70d2a0 Adds RTSP client custom keepalive timeout 2023-04-17 16:54:02 +03:00
Alexey Khit
6cef5faf27 Add timeout value to RTSP SETUP response #289 2023-04-17 15:12:03 +03:00
Alexey Khit
edb4e6eaad Update error msg for stream start 2023-04-17 15:04:45 +03:00
Alexey Khit
116319f876 Restore mutex for MP4 2023-04-17 14:17:45 +03:00
Alexey Khit
a0e6005598 Remove Range header check for MP4 for Chrome 2023-04-17 14:17:21 +03:00
Alexey Khit
fd580b6f2c Fix RTSP passive producer 2023-04-17 10:09:38 +03:00
Alexey Khit
1837e7c86c Fix cons number in trace logs 2023-04-17 10:08:42 +03:00
Alexey Khit
235f2fde0d Add control attr to RTSP server SDP 2023-04-16 14:52:02 +03:00
Alexey Khit
35087e0812 Remove mutex from MP4 2023-04-16 14:48:26 +03:00
Alexey Khit
da08d8e973 Fix RTSP backchannel processing 2023-04-16 14:47:59 +03:00
Alexey Khit
757091e43d Rewrite RTSP keepalive 2023-04-16 14:47:07 +03:00
Alexey Khit
a5c4854aeb Add reconnect logic to RTSP client 2023-04-16 13:57:27 +03:00
Alexey Khit
4b4deaaaf2 Fix missed control in SDP 2023-04-15 12:52:52 +03:00
Alexey Khit
553f5ff0d8 Add timeout to RTSP client requests 2023-04-15 12:51:15 +03:00
Alexey Khit
25dc3664fd Set random session for RTSP server 2023-04-15 12:50:50 +03:00
Alexey Khit
8dd9991268 Fix mutex lock after #339 2023-04-15 07:53:26 +03:00
Alexey Khit
d633d331bb Fix new stream from camera entity from Hass 2023-04-15 07:34:38 +03:00
Alexey Khit
7d3fbf2ee0 Add trace logs for media matching 2023-04-15 07:33:22 +03:00
Alex X
c44aaebd65 Merge pull request #186 from skrashevich/fix-cors-auth
Fix CORS for Basic auth
2023-04-14 06:29:13 +03:00
Alexey Khit
d6259fc0e9 Merge remote-tracking branch 'skrashevich/patch-230328' 2023-04-14 06:23:17 +03:00
Alex X
5c657d557a Merge pull request #332 from acardace/fix-isapi-2way
Fix ISAPI 2 Way Audio
2023-04-14 06:18:26 +03:00
Alex X
93be5cd92f Merge pull request #339 from yousong/mp4-conc
Fixes for working with rtsp stream of TL-IPC44GW
2023-04-14 06:13:30 +03:00
Yousong Zhou
cf6a35d0c7 rtsp: fixup sdp media type returned by TP-LINK camera
Related: https://github.com/aler9/mediamtx/issues/990
2023-04-10 09:06:50 +08:00
Yousong Zhou
af79e6054b rtsp: conn.Close() before retry dialing with backchannel=false 2023-04-07 16:46:20 +08:00
Yousong Zhou
9f3d5e7460 stream.mp4: write response in a serial way 2023-04-07 15:58:40 +08:00
Alexey Khit
abbf180b1b Fix Reolink Doorbell bug with SDP 2023-04-06 17:31:32 +03:00
Alex X
696588e52e Merge pull request #327 from skrashevich/patch-dockerfiles-go120
Update Go version to 1.20 in Dockerfiles and test.yml
2023-04-04 19:32:25 +03:00
Antonio Cardace
3e97ce8b2a Fix ISAPI 2 Way Audio
Signed-off-by: Antonio Cardace <acardace@redhat.com>
2023-04-04 17:41:06 +02:00
Sergey Krashevich
722b2827a1 Update Go version to 1.20 in test.yml and Dockerfiles 2023-04-03 20:30:25 +03:00
Alex X
69598b508c Merge pull request #321 from skrashevich/github-actions-update
Update Docker build-push-action to version 4 in workflows/docker.yml …
2023-04-02 10:08:44 +03:00
Alex X
f49fcc4f68 Merge pull request #322 from skrashevich/actions-docker-cache
Add cache for faster Docker builds from the GitHub Actions environment
2023-04-02 07:12:38 +03:00
Sergey Krashevich
59347a409e Add cache for faster Docker builds from the GitHub Actions environment in ".github/workflows/docker.yml". 2023-04-02 07:05:09 +03:00
Alex X
45b25d29b7 Merge pull request #320 from skrashevich/hardware-dockerfile-patch
Update hardware.Dockerfile
2023-04-02 06:39:48 +03:00
Sergey Krashevich
49e861d1b0 Update Docker build-push-action to version 4 in workflows/docker.yml and workflows/test.yml and deploy-pages to version 2 in workflows/gh-pages.yml 2023-04-02 04:02:28 +03:00
Sergey Krashevich
b1701e856a Update hardware.Dockerfile 2023-04-02 02:28:44 +03:00
Sergey Krashevich
a6260d0f56 Refactor Receiver and Sender methods to use RLock instead of Lock 2023-03-28 16:44:52 +03:00
Sergey Krashevich
693d41be87 Add buffer size of 8M to RTSP output in ffmpeg and handle EOF error when accepting RTSP connection in rtsp.go 2023-03-28 15:47:25 +03:00
Sergey Krashevich
222dc6a5c2 Refactor code to include buffer channels to prevent blocking in handler functions and add RWMutex to Receiver and Sender structs for concurrency-safe data access 2023-03-28 05:56:35 +03:00
Alexey Khit
8fde2b6fe5 Add support TCP MPEG-TS source 2023-03-26 17:29:59 +03:00
Alexey Khit
15e205cc01 Change HLS ID format 2023-03-26 15:45:07 +03:00
Alexey Khit
1db9ed4946 Disable MP4 redirect for Safari with duration query 2023-03-26 15:26:49 +03:00
Alexey Khit
fd83d151d2 Fix HLS after big refactoring 2023-03-26 15:26:18 +03:00
Sergey Krashevich
a36359f3dd Update api.go 2023-01-20 18:38:46 +03:00
124 changed files with 4026 additions and 1420 deletions

View File

@@ -52,7 +52,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
platforms: |
@@ -63,9 +63,10 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Hardware
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile
@@ -73,3 +74,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -34,4 +34,4 @@ jobs:
path: './website'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v2

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.19'
go-version: '1.20'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
@@ -76,7 +76,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/${{ matrix.platform }}
@@ -89,7 +89,7 @@ jobs:
- name: Build and push Hardware
if: matrix.platform == 'amd64'
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile

View File

@@ -2,7 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.19"
ARG GO_VERSION="1.20"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
@@ -40,7 +40,8 @@ FROM base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
RUN apk add --no-cache tini ffmpeg bash curl jq
# alsa-plugins-pulse for ALSA support (+0MB)
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH

124
README.md
View File

@@ -42,6 +42,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: RTSP](#source-rtsp)
* [Source: RTMP](#source-rtmp)
* [Source: HTTP](#source-http)
* [Source: ONVIF](#source-onvif)
* [Source: FFmpeg](#source-ffmpeg)
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
@@ -156,10 +157,11 @@ Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support
- [rtmp](#source-rtmp) - `RTMP` streams
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
@@ -229,6 +231,8 @@ Support Content-Type:
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
- **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream)
Source also support HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
```yaml
streams:
# [HTTP-FLV] stream in video/x-flv format
@@ -239,10 +243,26 @@ streams:
# [MJPEG] stream will be proxied without modification
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
tcp_magic: tcp://192.168.1.123:12345
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
#### Source: ONVIF
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
```yaml
streams:
dahua1: onvif://admin:password@192.168.1.123
reolink1: onvif://admin:password@192.168.1.123:8000
tapo1: onvif://admin:password@192.168.1.123:2020
```
#### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
@@ -273,7 +293,7 @@ streams:
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
```
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
@@ -301,25 +321,40 @@ Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2r
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
- check available devices in Web interface
- `resolution` and `framerate` must be supported by your camera!
- `video_size` and `framerate` must be supported by your camera!
- for Linux supported only video for now
- for macOS you can stream Facetime camera or whole Desktop!
- for macOS important to set right framerate
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
```yaml
streams:
linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264
linux_usbcam: ffmpeg:device?video=0&video_size=1280x720#video=h264
windows_webcam: ffmpeg:device?video=0#video=h264
macos_facetime: ffmpeg:device?video=0&audio=1&resolution=1280x720&framerate=30#video=h264#audio=pcma
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
```
#### Source: Exec
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
The source can be used with:
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
- [GStreamer](https://gstreamer.freedesktop.org/)
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
- any your own software
```yaml
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
picam_h264: exec:libcamera-vid -t 0 --inline -o -
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
```
#### Source: Echo
@@ -803,8 +838,8 @@ Provides several features:
API examples:
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265)
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC)
Read more about [codecs filters](#codecs-filters).
@@ -895,7 +930,7 @@ But it cannot be done for [RTSP](#module-rtsp), [HTTP progressive streaming](#mo
Without filters:
- RTSP will provide only the first video and only the first audio
- RTSP will provide only the first video and only the first audio (any codec)
- MP4 will include only compatible codecs (H264, H265, AAC)
- HLS will output in the legacy TS format (H264 without audio)
@@ -906,23 +941,25 @@ Some examples:
- `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks
- `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu` - MP4 file with non standard audio codecs, does not work in some players
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non standard audio codecs, won't work on some players
## Codecs madness
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|---------------------|-------------------------------|------------------------|-----------------------------------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, OPUS | H264, AAC, OPUS |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, ?, AAC, OPUS, PCMU, PCMA, MP3 |
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
| masOS Hass App | no | no | no |
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|---------------------|-------------------------------|-------------------------------|------------------------------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS |
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** |
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** |
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
| masOS Hass App | no | no | no |
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
@@ -931,9 +968,9 @@ Some examples:
**Audio**
- Go2rtc support [automatic repack](#built-in-transcoding) `PCMA/PCMU/PCM` codecs to `FLAC` for MSE/MP4/HLS so they will work almost anywhere
- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple)
- `PCMU` and `PCMA` inside **MP4** isn't a standard, but some players support them, for example Chromium browsers
**Apple devices**
@@ -941,6 +978,45 @@ Some examples:
- iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
**Codec names**
- H264 = H.264 = AVC (Advanced Video Coding)
- H265 = H.265 = HEVC (High Efficiency Video Coding)
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
- AAC = MPEG4-GENERIC
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
## Built-in transcoding
There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.
But go2rtc has some simple algorithms. They are turned on automatically, you do not need to set them up additionally.
**PCM for MSE/MP4/HLS**
Go2rtc can pack `PCMA`, `PCMU` and `PCM` codecs into an MP4 container so that they work in all browsers and all built-in players on modern devices. Including Apple QuickTime:
```
PCMA/PCMU => PCM => FLAC => MSE/MP4/HLS
```
**Resample PCMA/PCMU for WebRTC**
By default WebRTC support only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codec with with a different sample rate. Also go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
```
PCM/xxx => PCMA/8000 => WebRTC
PCMA/xxx => PCMA/8000 => WebRTC
PCMU/xxx => PCMU/8000 => WebRTC
```
**Important**
- FLAC codec not supported in a RTSP stream. If you using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio - you should setup transcoding to AAC codec.
- PCMA and PCMU are VERY low quality codecs. Them support only 256! different sounds. Use them only when you have no other options.
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.

View File

@@ -1,19 +0,0 @@
#!/bin/bash
set -euo pipefail
echo "Starting go2rtc..." >&2
readonly config_path="/config"
if [[ -x "${config_path}/go2rtc" ]]; then
readonly binary_path="${config_path}/go2rtc"
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
else
readonly binary_path="/usr/local/bin/go2rtc"
fi
# set cwd for go2rtc (for config file, Hass integration, etc)
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
exec "${binary_path}"

View File

@@ -1,4 +0,0 @@
**Project layout**
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro

View File

@@ -1,61 +0,0 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `"` + video.ID + `:` + audio.ID + `"`
case video != nil:
return `"` + video.ID + `"`
case audio != nil:
return `"` + audio.ID + `"`
}
return ""
}
func loadMedias() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
var kind string
lines := strings.Split(buf.String(), "\n")
process:
for _, line := range lines {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = core.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = core.KindAudio
continue
case strings.HasPrefix(line, "dummy"):
break process
}
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
name := line[42:]
media := loadMedia(kind, name)
medias = append(medias, media)
}
}
func loadMedia(kind, name string) *core.Media {
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -1,48 +0,0 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"io/ioutil"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(core.KindVideo, videoIdx)
return video.ID
}
func loadMedias() {
files, err := ioutil.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
log.Trace().Msg("[ffmpeg] " + file.Name())
if strings.HasPrefix(file.Name(), core.KindVideo) {
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
if media != nil {
medias = append(medias, media)
}
}
}
}
func loadMedia(kind, name string) *core.Media {
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
if !bytes.Contains(buf.Bytes(), []byte("Raw")) {
return nil
}
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -1,57 +0,0 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `video="` + video.ID + `":audio=` + audio.ID + `"`
case video != nil:
return `video="` + video.ID + `"`
case audio != nil:
return `audio="` + audio.ID + `"`
}
return ""
}
func loadMedias() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
lines := strings.Split(buf.String(), "\r\n")
for _, line := range lines {
var kind string
if strings.HasSuffix(line, "(video)") {
kind = core.KindVideo
} else if strings.HasSuffix(line, "(audio)") {
kind = core.KindAudio
} else {
continue
}
// hope we have constant prefix and suffix sizes
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
name := line[28 : len(line)-9]
media := loadMedia(kind, name)
medias = append(medias, media)
}
}
func loadMedia(kind, name string) *core.Media {
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -1,91 +0,0 @@
package device
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/rs/zerolog"
"net/http"
"net/url"
"strconv"
"strings"
)
func Init() {
log = app.GetLogger("exec")
api.HandleFunc("api/devices", handle)
}
func GetInput(src string) (string, error) {
if medias == nil {
loadMedias()
}
input := deviceInputPrefix
var videoIdx, audioIdx int
if i := strings.IndexByte(src, '?'); i > 0 {
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
for key, value := range query {
switch key {
case "video":
videoIdx, _ = strconv.Atoi(value[0])
case "audio":
audioIdx, _ = strconv.Atoi(value[0])
case "framerate":
input += " -framerate " + value[0]
case "resolution":
input += " -video_size " + value[0]
}
}
}
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
return input, nil
}
var Bin string
var log zerolog.Logger
var medias []*core.Media
func findMedia(kind string, index int) *core.Media {
for _, media := range medias {
if media.Kind != kind {
continue
}
if index == 0 {
return media
}
index--
}
return nil
}
func handle(w http.ResponseWriter, r *http.Request) {
if medias == nil {
loadMedias()
}
var items []api.Stream
var iv, ia int
for _, media := range medias {
var source string
switch media.Kind {
case core.KindVideo:
source = "ffmpeg:device?video=" + strconv.Itoa(iv)
iv++
case core.KindAudio:
source = "ffmpeg:device?audio=" + strconv.Itoa(ia)
ia++
}
items = append(items, api.Stream{Name: media.ID, URL: source})
}
api.ResponseStreams(w, items)
}

View File

@@ -1,120 +0,0 @@
package ffmpeg
import (
"os/exec"
"strings"
"github.com/rs/zerolog/log"
)
const (
EngineSoftware = "software"
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
)
var cache = map[string]string{}
// MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect
func MakeHardware(args *Args, engine string) {
for i, codec := range args.codecs {
if len(codec) < 12 {
continue // skip short line (-c:v libx264...)
}
// get current codec name
name := cut(codec, ' ', 1)
switch name {
case "libx264":
name = "h264"
case "libx265":
name = "h265"
case "mjpeg":
default:
continue // skip unsupported codec
}
// temporary disable probe for H265 and MJPEG
if engine == "" && name == "h264" {
if engine = cache[name]; engine == "" {
engine = ProbeHardware(name)
cache[name] = engine
}
}
switch engine {
case EngineVAAPI:
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_vaapi=" + filter[6:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.filters[i] = "transpose_vaapi=4" // reversal
} else {
args.filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// fix if input doesn't support hwaccel, do nothing when support
args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA:
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_cuda=" + filter[6:]
}
}
case EngineDXVA2:
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_qsv=" + filter[6:]
}
}
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
case EngineVideoToolbox:
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
args.codecs[i] = defaults[name+"/"+engine]
case EngineV4L2M2M:
args.codecs[i] = defaults[name+"/"+engine]
}
}
}
func run(arg ...string) bool {
err := exec.Command(defaults["bin"], arg...).Run()
log.Printf("%v %v", arg, err)
return err == nil
}
func cut(s string, sep byte, pos int) string {
for n := 0; n < pos; n++ {
if i := strings.IndexByte(s, sep); i > 0 {
s = s[i+1:]
} else {
return ""
}
}
if i := strings.IndexByte(s, sep); i > 0 {
return s[:i]
}
return s
}

View File

@@ -1,21 +0,0 @@
package ffmpeg
func ProbeHardware(name string) string {
switch name {
case "h264":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_videotoolbox", "-f", "null", "-") {
return EngineVideoToolbox
}
case "h265":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_videotoolbox", "-f", "null", "-") {
return EngineVideoToolbox
}
}
return EngineSoftware
}

View File

@@ -1,67 +0,0 @@
package ffmpeg
import (
"runtime"
)
func ProbeHardware(name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_v4l2m2m", "-f", "null", "-") {
return EngineV4L2M2M
}
case "h265":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
return EngineV4L2M2M
}
}
return EngineSoftware
}
switch name {
case "h264":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "h264_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
case "h265":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "hevc_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
case "mjpeg":
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "mjpeg_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
}
return EngineSoftware
}

View File

@@ -1,40 +0,0 @@
package ffmpeg
func ProbeHardware(name string) string {
switch name {
case "h264":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_qsv", "-f", "null", "-") {
return EngineDXVA2
}
case "h265":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_qsv", "-f", "null", "-") {
return EngineDXVA2
}
case "mjpeg":
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "mjpeg_qsv", "-f", "null", "-") {
return EngineDXVA2
}
}
return EngineSoftware
}

23
cmd/go2rtc_rtsp/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"os"
"os/signal"
"syscall"
)
func main() {
app.Init()
streams.Init()
rtsp.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
println("exit OK")
}

View File

@@ -1,65 +0,0 @@
package http
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net/http"
"strings"
)
func Init() {
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
}
func handle(url string) (core.Producer, error) {
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
case "video/mpeg":
client := mpegts.NewClient(res)
if err = client.Handle(); err != nil {
return nil, err
}
return client, nil
}
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
}

2
go.mod
View File

@@ -16,6 +16,8 @@ require (
github.com/pion/stun v0.4.0
github.com/pion/webrtc/v3 v3.1.58
github.com/rs/zerolog v1.29.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.2
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
gopkg.in/yaml.v3 v3.0.1

4
go.sum
View File

@@ -106,6 +106,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/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/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@@ -1,7 +1,9 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.19-buster"
ARG GO_VERSION="1.20-buster"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
@@ -16,37 +18,43 @@ WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
COPY ./build/docker/run.sh /
COPY --link --from=build /build/go2rtc /usr/local/bin/
COPY --link --from=ngrok /bin/ngrok /usr/local/bin/
# 3. Final image
FROM base
# Prepare apt for buildkit cache
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
# and other common tools for the echo source.
# non-free for Intel QSV support (not used by go2rtc, just for tests)
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
# libasound2-plugins for ALSA support
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg \
python3 curl jq \
intel-media-va-driver-non-free \
libasound2-plugins
COPY --link --from=rootfs / /
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config
WORKDIR /config
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
CMD ["/run.sh"]
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]

11
internal/README.md Normal file
View File

@@ -0,0 +1,11 @@
## Go
```
go mod why github.com/pion/rtcp
go list -deps .\cmd\go2rtc_rtsp\
```
## Useful links
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro

View File

@@ -2,7 +2,7 @@ package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
"net"
"net/http"
@@ -121,6 +121,7 @@ func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
next.ServeHTTP(w, r)
})
}

View File

@@ -1,7 +1,7 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
"io"
"net/http"

View File

@@ -16,7 +16,7 @@ import (
"gopkg.in/yaml.v3"
)
var Version = "1.3.1"
var Version = "1.5.0"
var UserAgent = "go2rtc/" + Version
var ConfigPath string

View File

@@ -1,8 +1,8 @@
package debug
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
)

View File

@@ -13,15 +13,15 @@ var stackSkip = [][]byte{
[]byte("created by os/signal.Notify"),
// api/stack.go
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
[]byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"),
// api/api.go
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"),
[]byte("created by net/http.(*connReader).startBackgroundRead"),
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),

View File

@@ -1,7 +1,7 @@
package dvrip
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
)

View File

@@ -2,8 +2,8 @@ package echo
import (
"bytes"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/shell"
"os/exec"

View File

@@ -5,26 +5,21 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"os"
"os/exec"
"strings"
"sync"
"time"
)
func Init() {
// depends on RTSP server
if rtsp.Port == "" {
return
}
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
@@ -43,30 +38,66 @@ func Init() {
}
})
streams.HandleFunc("exec", Handle)
streams.HandleFunc("exec", execHandle)
log = app.GetLogger("exec")
}
func Handle(url string) (core.Producer, error) {
sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:])
func execHandle(url string) (core.Producer, error) {
var path string
url = strings.Replace(
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
)
args := shell.QuoteSplit(url[5:]) // remove `exec:`
for i, arg := range args {
if arg == "{output}" {
if rtsp.Port == "" {
return nil, errors.New("rtsp module disabled")
}
// remove `exec:`
args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
sum := md5.Sum([]byte(url))
path = "/" + hex.EncodeToString(sum[:])
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
break
}
}
cmd := exec.Command(args[0], args[1:]...)
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr
}
if path == "" {
return handlePipe(url, cmd)
}
return handleRTSP(url, path, cmd)
}
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
r, err := PipeCloser(cmd)
if err != nil {
return nil, err
}
if err = cmd.Start(); err != nil {
return nil, err
}
client := magic.NewClient(r)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "exec active producer"
client.URL = url
return client, nil
}
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
}
ch := make(chan core.Producer)
waitersMu.Lock()

26
internal/exec/pipe.go Normal file
View File

@@ -0,0 +1,26 @@
package exec
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"io"
"os/exec"
)
// PipeCloser - return StdoutPipe that Kill cmd on Close call
func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
return pipeCloser{stdout, cmd}, nil
}
type pipeCloser struct {
io.ReadCloser
cmd *exec.Cmd
}
func (p pipeCloser) Close() error {
return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
}

View File

@@ -0,0 +1,68 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"regexp"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `"` + video + `:` + audio + `"`
case video != "":
return `"` + video + `"`
case audio != "":
return `":` + audio + `"`
}
return ""
}
func initDevices() {
// [AVFoundation indev @ 0x147f04510] AVFoundation video devices:
// [AVFoundation indev @ 0x147f04510] [0] FaceTime HD Camera
// [AVFoundation indev @ 0x147f04510] [1] Capture screen 0
// [AVFoundation indev @ 0x147f04510] AVFoundation audio devices:
// [AVFoundation indev @ 0x147f04510] [0] MacBook Pro Microphone
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "",
)
b, _ := cmd.CombinedOutput()
re := regexp.MustCompile(`\[\d+] (.+)`)
var kind string
for _, line := range strings.Split(string(b), "\n") {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = core.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = core.KindAudio
continue
}
m := re.FindStringSubmatch(line)
if m == nil {
continue
}
name := m[1]
switch kind {
case core.KindVideo:
videos = append(videos, name)
case core.KindAudio:
audios = append(audios, name)
}
streams = append(streams, api.Stream{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
}
}

View File

@@ -0,0 +1,60 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"os"
"os/exec"
"regexp"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(video, audio string) string {
if video != "" {
return video
}
return ""
}
func initDevices() {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
name := "/dev/" + file.Name()
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
b, _ := cmd.CombinedOutput()
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: mjpeg : Motion-JPEG : 640x360 1280x720 1920x1080
// [video4linux2,v4l2 @ 0x204e1c0] Raw : yuyv422 : YUYV 4:2:2 : 640x360 1280x720 1920x1080
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: h264 : H.264 : 640x360 1280x720 1920x1080
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := api.Stream{
Name: i[3] + " | " + i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
if i[1] != "Compressed" {
stream.URL += "#video=h264#hardware"
}
videos = append(videos, name)
streams = append(streams, stream)
}
}
}

View File

@@ -0,0 +1,50 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"regexp"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `video="` + video + `":audio=` + audio + `"`
case video != "":
return `video="` + video + `"`
case audio != "":
return `audio="` + audio + `"`
}
return ""
}
func initDevices() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
)
b, _ := cmd.CombinedOutput()
re := regexp.MustCompile(`"([^"]+)" \((video|audio)\)`)
for _, m := range re.FindAllStringSubmatch(string(b), -1) {
name := m[1]
kind := m[2]
stream := api.Stream{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
}
switch kind {
case core.KindVideo:
videos = append(videos, name)
stream.URL += "#video=h264#hardware"
case core.KindAudio:
audios = append(audios, name)
}
streams = append(streams, stream)
}
}

View File

@@ -0,0 +1,70 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
)
func Init(bin string) {
Bin = bin
api.HandleFunc("api/ffmpeg/devices", apiDevices)
}
func GetInput(src string) (string, error) {
runonce.Do(initDevices)
input := deviceInputPrefix
var video, audio string
if i := strings.IndexByte(src, '?'); i > 0 {
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
for key, value := range query {
switch key {
case "video":
video = value[0]
case "audio":
audio = value[0]
case "resolution":
input += " -video_size " + value[0]
default: // "input_format", "framerate", "video_size"
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
if i, err := strconv.Atoi(video); err == nil && i < len(videos) {
video = videos[i]
}
}
if audio != "" {
if i, err := strconv.Atoi(audio); err == nil && i < len(audios) {
audio = audios[i]
}
}
input += " -i " + deviceInputSuffix(video, audio)
return input, nil
}
var Bin string
var videos, audios []string
var streams []api.Stream
var runonce sync.Once
func apiDevices(w http.ResponseWriter, r *http.Request) {
runonce.Do(initDevices)
api.ResponseStreams(w, streams)
}

View File

@@ -1,16 +1,15 @@
package ffmpeg
import (
"bytes"
"errors"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/url"
"strconv"
"strings"
)
@@ -32,11 +31,11 @@ func Init() {
if args == nil {
return nil, errors.New("can't generate ffmpeg command")
}
return exec.Handle("exec:" + args.String())
return streams.GetProducer("exec:" + args.String())
})
device.Bin = defaults["bin"]
device.Init()
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
var defaults = map[string]string{
@@ -51,16 +50,19 @@ var defaults = map[string]string{
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
// 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 -",
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile high -level 4.1` - most used streaming profile
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
@@ -70,8 +72,7 @@ var defaults = map[string]string{
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"pcm": "-c:a pcm_s16be",
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
@@ -116,19 +117,19 @@ func inputTemplate(name, s string, query url.Values) string {
return strings.Replace(template, "{input}", s, 1)
}
func parseArgs(s string) *Args {
func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments
args := &Args{
bin: defaults["bin"],
global: defaults["global"],
output: defaults["output"],
args := &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
}
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
args.video = len(query["video"])
args.audio = len(query["audio"])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
s = s[:i]
}
@@ -139,46 +140,46 @@ func parseArgs(s string) *Args {
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
args.input = inputTemplate("http", s, query)
args.Input = inputTemplate("http", s, query)
case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
args.input = "-allowed_media_types video+audio "
case args.video > 0:
args.input = "-allowed_media_types video "
case args.audio > 0:
args.input = "-allowed_media_types audio "
case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0):
args.Input = "-allowed_media_types video+audio "
case args.Video > 0:
args.Input = "-allowed_media_types video "
case args.Audio > 0:
args.Input = "-allowed_media_types audio "
}
args.input += inputTemplate("rtsp", s, query)
args.Input += inputTemplate("rtsp", s, query)
default:
args.input = "-i " + s
args.Input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
switch {
case args.video > 0 && args.audio == 0:
case args.Video > 0 && args.Audio == 0:
s += "?video"
case args.audio > 0 && args.video == 0:
case args.Audio > 0 && args.Video == 0:
s += "?audio"
default:
s += "?video&audio"
}
args.input = inputTemplate("rtsp", s, query)
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.input, err = device.GetInput(s)
args.Input, err = device.GetInput(s)
if err != nil {
return nil
}
} else {
args.input = inputTemplate("file", s, query)
args.Input = inputTemplate("file", s, query)
}
if query["async"] != nil {
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
}
// Parse query params:
@@ -226,7 +227,7 @@ func parseArgs(s string) *Args {
}
// 3. Process video codecs
if args.video > 0 {
if args.Video > 0 {
for _, video := range query["video"] {
if video != "copy" {
if codec := defaults[video]; codec != "" {
@@ -243,7 +244,7 @@ func parseArgs(s string) *Args {
}
// 4. Process audio codecs
if args.audio > 0 {
if args.Audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
if codec := defaults[audio]; codec != "" {
@@ -260,14 +261,21 @@ func parseArgs(s string) *Args {
}
if query["hardware"] != nil {
MakeHardware(args, query["hardware"][0])
hardware.MakeHardware(args, query["hardware"][0], defaults)
}
}
if args.codecs == nil {
if args.Codecs == nil {
args.AddCodec("-c copy")
}
// transcoding to only mjpeg
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
// no transcoding from mjpeg input
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
args.Output = defaults["output/mjpeg"]
}
return args
}
@@ -283,76 +291,3 @@ func parseQuery(s string) map[string][]string {
}
return query
}
type Args struct {
bin string // ffmpeg
global string // -hide_banner -v error
input string // -re -stream_loop -1 -i /media/bunny.mp4
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
filters []string // scale=1920:1080
output string // -f rtsp {output}
video, audio int // count of video and audio params
}
func (a *Args) AddCodec(codec string) {
a.codecs = append(a.codecs, codec)
}
func (a *Args) AddFilter(filter string) {
a.filters = append(a.filters, filter)
}
func (a *Args) InsertFilter(filter string) {
a.filters = append([]string{filter}, a.filters...)
}
func (a *Args) String() string {
b := bytes.NewBuffer(make([]byte, 0, 512))
b.WriteString(a.bin)
if a.global != "" {
b.WriteByte(' ')
b.WriteString(a.global)
}
b.WriteByte(' ')
b.WriteString(a.input)
multimode := a.video > 1 || a.audio > 1
var iv, ia int
for _, codec := range a.codecs {
// support multiple video and/or audio codecs
if multimode && len(codec) >= 5 {
switch codec[:5] {
case "-c:v ":
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
iv++
case "-c:a ":
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
ia++
}
}
b.WriteByte(' ')
b.WriteString(codec)
}
if a.filters != nil {
for i, filter := range a.filters {
if i == 0 {
b.WriteString(" -vf ")
} else {
b.WriteByte(',')
}
b.WriteString(filter)
}
}
b.WriteByte(' ')
b.WriteString(a.output)
return b.String()
}

View File

@@ -7,8 +7,17 @@ import (
func TestParseArgs(t *testing.T) {
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
args = parseArgs("/media/bbb.mp4#video=mjpeg")
assert.Equal(t, "ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -", args.String())
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf format=vaapi|nv12,hwupload -f mjpeg -", args.String())
args = parseArgs("device?video=0&input_format=mjpeg&video_size=1920x1080")
assert.Equal(t, `ffmpeg -hide_banner -f dshow -input_format mjpeg -video_size 1920x1080 -i video="0" -c copy -f mjpeg -`, args.String())
}

View File

@@ -0,0 +1,137 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/http"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
)
const (
EngineSoftware = "software"
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
)
func Init(bin string) {
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
api.ResponseStreams(w, ProbeAll(bin))
})
}
// MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect
func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) {
for i, codec := range args.Codecs {
if len(codec) < 10 {
continue // skip short line (-c:v mjpeg...)
}
// get current codec name
name := cut(codec, ' ', 1)
switch name {
case "libx264":
name = "h264"
case "libx265":
name = "h265"
case "mjpeg":
default:
continue // skip unsupported codec
}
// temporary disable probe for H265
if engine == "" && name != "h265" {
if engine = cache[name]; engine == "" {
engine = ProbeHardware(args.Bin, name)
cache[name] = engine
}
}
switch engine {
case EngineVAAPI:
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "transpose_vaapi=4" // reversal
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// fix if input doesn't support hwaccel, do nothing when support
args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA:
args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
}
}
case EngineDXVA2:
args.Input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_qsv=" + filter[6:]
}
}
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
case EngineVideoToolbox:
args.Input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
case EngineV4L2M2M:
args.Codecs[i] = defaults[name+"/"+engine]
}
}
}
var cache = map[string]string{}
func run(bin string, args string) bool {
err := exec.Command(bin, strings.Split(args, " ")...).Run()
log.Printf("%v %v", args, err)
return err == nil
}
func runToString(bin string, args string) string {
if run(bin, args) {
return "OK"
} else {
return "ERROR"
}
}
func cut(s string, sep byte, pos int) string {
for n := 0; n < pos; n++ {
if i := strings.IndexByte(s, sep); i > 0 {
s = s[i+1:]
} else {
return ""
}
}
if i := strings.IndexByte(s, sep); i > 0 {
return s[:i]
}
return s
}

View File

@@ -0,0 +1,37 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
{
Name: runToString(bin, ProbeVideoToolboxH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
},
{
Name: runToString(bin, ProbeVideoToolboxH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox,
},
}
}
func ProbeHardware(bin, name string) string {
switch name {
case "h264":
if run(bin, ProbeVideoToolboxH264) {
return EngineVideoToolbox
}
case "h265":
if run(bin, ProbeVideoToolboxH265) {
return EngineVideoToolbox
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,94 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"runtime"
)
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []api.Stream {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []api.Stream{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
}
}
return []api.Stream{
{
Name: runToString(bin, ProbeVAAPIH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeVAAPIH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeVAAPIJPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeCUDAH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
},
{
Name: runToString(bin, ProbeCUDAH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
},
}
}
func ProbeHardware(bin, name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
}
return EngineSoftware
}
switch name {
case "h264":
if run(bin, ProbeCUDAH264) {
return EngineCUDA
}
if run(bin, ProbeVAAPIH264) {
return EngineVAAPI
}
case "h265":
if run(bin, ProbeCUDAH265) {
return EngineCUDA
}
if run(bin, ProbeVAAPIH265) {
return EngineVAAPI
}
case "mjpeg":
if run(bin, ProbeVAAPIJPEG) {
return EngineVAAPI
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,61 @@
package hardware
import "github.com/AlexxIT/go2rtc/internal/api"
const ProbeDXVA2H264 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -"
const ProbeDXVA2H265 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -"
const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -"
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
{
Name: runToString(bin, ProbeDXVA2H264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
},
{
Name: runToString(bin, ProbeDXVA2H265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineDXVA2,
},
{
Name: runToString(bin, ProbeDXVA2JPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineDXVA2,
},
{
Name: runToString(bin, ProbeCUDAH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
},
{
Name: runToString(bin, ProbeCUDAH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
},
}
}
func ProbeHardware(bin, name string) string {
switch name {
case "h264":
if run(bin, ProbeCUDAH264) {
return EngineCUDA
}
if run(bin, ProbeDXVA2H264) {
return EngineDXVA2
}
case "h265":
if run(bin, ProbeCUDAH265) {
return EngineCUDA
}
if run(bin, ProbeDXVA2H265) {
return EngineDXVA2
}
case "mjpeg":
if run(bin, ProbeDXVA2JPEG) {
return EngineDXVA2
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,12 @@
package ffmpeg
import (
"bytes"
"os/exec"
)
func TranscodeToJPEG(b []byte) ([]byte, error) {
cmd := exec.Command("ffmpeg", "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}

View File

@@ -3,12 +3,11 @@ package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"net"
"net/http"
"net/url"
"strings"
)
@@ -25,6 +24,7 @@ func initAPI() {
api.HandleFunc("/streams", ok)
// api from RTSPtoWeb
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
@@ -40,13 +40,7 @@ func initAPI() {
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
// check if it is rtsp link to go2rtc
stream = rtspStream(v.Channels.First.Url)
if stream != nil {
streams.New(v.Name, stream)
} else {
stream = streams.New(v.Name, "{input}")
}
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
}
stream.SetSource(v.Channels.First.Url)
@@ -90,48 +84,6 @@ func initAPI() {
_, _ = w.Write([]byte(s))
}
})
// api from RTSPtoWebRTC
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return
}
src := r.FormValue("url")
src, err = url.QueryUnescape(src)
if err != nil {
return
}
stream := streams.Get(src)
if stream == nil {
if stream = rtspStream(src); stream != nil {
streams.New(src, stream)
} else {
stream = streams.New(src, src)
}
}
str, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
if err != nil {
return
}
v := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
})
}
func HassioAddr() string {
@@ -153,15 +105,6 @@ func HassioAddr() string {
return ""
}
func rtspStream(url string) *streams.Stream {
if strings.HasPrefix(url, "rtsp://") {
if i := strings.IndexByte(url[7:], '/'); i > 0 {
return streams.Get(url[8+i:])
}
}
return nil
}
type addJSON struct {
Name string `json:"name"`
Channels struct {

View File

@@ -4,10 +4,10 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/roborock"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/rs/zerolog"
"net/http"
@@ -131,6 +131,25 @@ func importEntries(config string) map[string]string {
case "roborock":
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
case "onvif":
var data struct {
Host string `json:"host" json:"host"`
Port uint16 `json:"port" json:"port"`
Username string `json:"username" json:"username"`
Password string `json:"password" json:"password"`
}
if err = json.Unmarshal(entrie.Data, &data); err != nil {
continue
}
if data.Username != "" && data.Password != "" {
urls[entrie.Title] = fmt.Sprintf(
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
)
} else {
urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
}
default:
continue
}

View File

@@ -2,15 +2,15 @@ package hls
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
@@ -29,6 +29,7 @@ func Init() {
type Consumer interface {
core.Consumer
Listen(f core.EventFunc)
Init() ([]byte, error)
MimeCodecs() string
Start()
@@ -48,6 +49,9 @@ const keepalive = 5 * time.Second
var sessions = map[string]*Session{}
// once I saw 404 on MP4 segment, so better to use mutex
var sessionsMu sync.RWMutex
func handlerStream(w http.ResponseWriter, r *http.Request) {
// CORS important for Chromecast
w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -84,7 +88,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
session := &Session{cons: cons}
cons.(any).(*core.Listener).Listen(func(msg any) {
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.segment = append(session.segment, data...)
@@ -104,7 +108,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
cons.Start()
sid := strconv.FormatInt(time.Now().UnixNano(), 10)
sid := core.RandString(8, 62)
// two segments important for Chromecast
if medias != nil {
@@ -128,11 +132,16 @@ segment.ts?id=` + sid + `&n=%d
segment.ts?id=` + sid + `&n=%d`
}
sessionsMu.Lock()
sessions[sid] = session
sessionsMu.Unlock()
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
@@ -150,7 +159,9 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
@@ -173,7 +184,9 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
@@ -212,7 +225,9 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
@@ -233,7 +248,9 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return

View File

@@ -3,8 +3,8 @@ package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"net/http"

View File

@@ -1,10 +1,10 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/rs/zerolog"

95
internal/http/http.go Normal file
View File

@@ -0,0 +1,95 @@
package http
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net"
"net/http"
"net/url"
"strings"
"time"
)
func Init() {
streams.HandleFunc("http", handleHTTP)
streams.HandleFunc("https", handleHTTP)
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
}
func handleHTTP(url string) (core.Producer, error) {
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
default: // "video/mpeg":
}
client := magic.NewClient(res.Body)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "HTTP active producer"
client.URL = url
return client, nil
}
func handleTCP(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
if err != nil {
return nil, err
}
client := magic.NewClient(conn)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "TCP active producer"
client.URL = rawURL
return client, nil
}

View File

@@ -1,7 +1,7 @@
package isapi
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/isapi"
)

View File

@@ -1,7 +1,7 @@
package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"strings"

View File

@@ -2,14 +2,18 @@ package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"io"
"net/http"
"strconv"
"time"
)
func Init() {
@@ -29,14 +33,16 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
exit := make(chan []byte)
cons := &mjpeg.Consumer{
cons := &magic.Keyframe{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
switch msg := msg.(type) {
case []byte:
exit <- msg
if b, ok := msg.([]byte); ok {
select {
case exit <- b:
default:
}
}
})
@@ -49,6 +55,17 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if data, err = ffmpeg.TranscodeToJPEG(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
}
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(data)))

View File

@@ -1,17 +1,17 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
@@ -44,18 +44,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
return
}
exit := make(chan []byte)
exit := make(chan []byte, 1)
cons := &mp4.Segment{OnlyKeyframe: true}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok && exit != nil {
exit <- data
select {
case exit <- data:
default:
}
exit = nil
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -75,18 +79,13 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
func handlerMP4(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
// Chrome has Safari in UA, so check first Chrome and later Safari
query := r.URL.Query()
ua := r.UserAgent()
if strings.Contains(ua, " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return
}
} else if strings.Contains(ua, " Safari/") {
if strings.Contains(ua, " Safari/") && !strings.Contains(ua, " Chrome/") && !query.Has("duration") {
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
url := "stream.m3u8?" + r.URL.RawQuery
if !r.URL.Query().Has("mp4") {
if !query.Has("mp4") {
url += "&mp4"
}
@@ -94,25 +93,31 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
src := r.URL.Query().Get("src")
src := query.Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan error)
exit := make(chan error, 1) // Add buffer to prevent blocking
cons := &mp4.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: core.ParseQuery(r.URL.Query()),
Medias: mp4.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg any) {
if exit == nil {
return
}
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
exit <- err
if _, err := w.Write(data); err != nil {
select {
case exit <- err:
default:
}
exit = nil
}
}
@@ -120,6 +125,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -130,22 +136,27 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cons.Start()
var duration *time.Timer
if s := r.URL.Query().Get("duration"); s != "" {
if s := query.Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
if exit != nil {
exit <- nil
select {
case exit <- nil:
default:
}
exit = nil
}
})
@@ -153,6 +164,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
}
err = <-exit
exit = nil
log.Trace().Err(err).Caller().Send()

View File

@@ -2,8 +2,8 @@ package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
@@ -110,6 +110,12 @@ func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
case mp4.MimeAAC:
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case mp4.MimeFlac:
audios = append(audios,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
)
case mp4.MimeOpus:
codec := &core.Codec{Name: core.CodecOpus}
audios = append(audios, codec)

View File

@@ -1,8 +1,8 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"net/http"
)

View File

@@ -2,8 +2,8 @@ package ngrok
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/ngrok"
"github.com/rs/zerolog"
"net"

194
internal/onvif/init.go Normal file
View File

@@ -0,0 +1,194 @@
package onvif
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
func Init() {
log = app.GetLogger("onvif")
streams.HandleFunc("onvif", streamOnvif)
// ONVIF server on all suburls
api.HandleFunc("/onvif/", onvifDeviceService)
// ONVIF client autodiscovery
api.HandleFunc("api/onvif", apiOnvif)
}
var log zerolog.Logger
func streamOnvif(rawURL string) (core.Producer, error) {
client, err := onvif.NewClient(rawURL)
if err != nil {
return nil, err
}
uri, err := client.GetURI()
if err != nil {
return nil, err
}
log.Debug().Msgf("[onvif] new uri=%s", uri)
return streams.GetProducer(uri)
}
func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
action := onvif.GetRequestAction(b)
if action == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
log.Trace().Msgf("[onvif] %s", action)
var res string
switch action {
case onvif.ActionGetCapabilities:
// important for Hass: Media section
res = onvif.GetCapabilitiesResponse(r.Host)
case onvif.ActionGetSystemDateAndTime:
// important for Hass
res = onvif.GetSystemDateAndTimeResponse()
case onvif.ActionGetNetworkInterfaces:
// important for Hass: none
res = onvif.GetNetworkInterfacesResponse()
case onvif.ActionGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ActionGetServiceCapabilities:
// important for Hass
res = onvif.GetServiceCapabilitiesResponse()
case onvif.ActionSystemReboot:
res = onvif.SystemRebootResponse()
time.AfterFunc(time.Second, func() {
os.Exit(0)
})
case onvif.ActionGetProfiles:
// important for Hass: H264 codec, width, height
res = onvif.GetProfilesResponse(streams.GetAll())
case onvif.ActionGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetStreamUriResponse(uri)
default:
http.Error(w, "unsupported action", http.StatusBadRequest)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write([]byte(res)); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func apiOnvif(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
var items []api.Stream
if src == "" {
urls, err := onvif.DiscoveryStreamingURLs()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, rawURL := range urls {
u, err := url.Parse(rawURL)
if err != nil {
log.Warn().Str("url", rawURL).Msg("[onvif] broken")
continue
}
if u.Scheme != "http" {
log.Warn().Str("url", rawURL).Msg("[onvif] unsupported")
continue
}
u.Scheme = "onvif"
u.User = url.UserPassword("user", "pass")
if u.Path == onvif.PathDevice {
u.Path = ""
}
items = append(items, api.Stream{Name: u.Host, URL: u.String()})
}
} else {
client, err := onvif.NewClient(src)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if l := log.Trace(); l.Enabled() {
b, _ := client.GetProfiles()
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}
name, err := client.GetName()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tokens, err := client.GetProfilesTokens()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for i, token := range tokens {
items = append(items, api.Stream{
Name: name + " stream" + strconv.Itoa(i),
URL: src + "?subtype=" + token,
})
}
if len(tokens) > 0 && client.HasSnapshots() {
items = append(items, api.Stream{
Name: name + " snapshot",
URL: src + "?subtype=" + tokens[0] + "&snapshot",
})
}
}
api.ResponseStreams(w, items)
}

View File

@@ -2,8 +2,8 @@ package roborock
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/roborock"
"net/http"

View File

@@ -1,8 +1,8 @@
package rtmp
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/rs/zerolog/log"

View File

@@ -1,16 +1,17 @@
package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"io"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
@@ -20,6 +21,7 @@ func Init() {
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
DefaultQuery string `yaml:"default_query" json:"default_query"`
PacketSize uint16 `yaml:"pkt_size"`
} `yaml:"rtsp"`
}
@@ -54,7 +56,7 @@ func Init() {
log.Info().Str("addr", address).Msg("[rtsp] listen")
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
defaultMedias = mp4.ParseQuery(query)
defaultMedias = ParseQuery(query)
}
go func() {
@@ -65,6 +67,7 @@ func Init() {
}
c := rtsp.NewServer(conn)
c.PacketSize = conf.Mod.PacketSize
// skip check auth for localhost
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
c.Auth(conf.Mod.Username, conf.Mod.Password)
@@ -123,6 +126,7 @@ func rtspHandler(url string) (core.Producer, error) {
if !backchannel {
return nil, err
}
log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", backchannel, err)
// second try without backchannel, we need to reconnect
conn.Backchannel = false
@@ -171,13 +175,18 @@ func tcpHandler(conn *rtsp.Conn) {
conn.SessionName = app.UserAgent
conn.Medias = mp4.ParseQuery(conn.URL.Query())
query := conn.URL.Query()
conn.Medias = ParseQuery(query)
if conn.Medias == nil {
for _, media := range defaultMedias {
conn.Medias = append(conn.Medias, media.Clone())
}
}
if s := query.Get("pkt_size"); s != "" {
conn.PacketSize = uint16(core.Atoi(s))
}
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
@@ -211,7 +220,9 @@ func tcpHandler(conn *rtsp.Conn) {
})
if err := conn.Accept(); err != nil {
log.Warn().Err(err).Caller().Send()
if err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
if closer != nil {
closer()
}
@@ -237,3 +248,27 @@ func tcpHandler(conn *rtsp.Conn) {
_ = conn.Close()
}
func ParseQuery(query map[string][]string) []*core.Media {
if v := query["mp4"]; v != nil {
return []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
}
return core.ParseQuery(query)
}

View File

@@ -1,7 +1,7 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"net"
)

View File

@@ -2,11 +2,12 @@ package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/rs/zerolog"
"net/http"
"net/url"
)
func Init() {
@@ -39,6 +40,20 @@ func New(name string, source any) *Stream {
return stream
}
func NewTemplate(name string, source any) *Stream {
// check if source links to some stream name from go2rtc
if rawURL, ok := source.(string); ok {
if u, err := url.Parse(rawURL); err == nil && u.Scheme == "rtsp" {
if stream, ok := streams[u.Path[1:]]; ok {
streams[name] = stream
return stream
}
}
}
return New(name, "{input}")
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
@@ -53,6 +68,13 @@ func GetOrNew(src string) *Stream {
return New(src, src)
}
func GetAll() (names []string) {
for name := range streams {
names = append(names, name)
}
return
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
src := query.Get("src")
@@ -85,11 +107,12 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
return
}
if stream := Get(name); stream != nil {
stream.SetSource(src)
} else {
New(name, src)
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
stream := Get(name)
if stream == nil {
stream = NewTemplate(name, src)
}
stream.SetSource(src)
case "POST":
// with dst - redirect source to dst

View File

@@ -30,8 +30,6 @@ type Producer struct {
receivers []*core.Receiver
senders []*core.Receiver
lastErr error
state state
mu sync.Mutex
workerID int
@@ -58,6 +56,10 @@ func (p *Producer) GetMedias() []*core.Media {
p.mu.Lock()
defer p.mu.Unlock()
if p.conn == nil {
return nil
}
return p.conn.GetMedias()
}

View File

@@ -3,7 +3,6 @@ package streams
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
@@ -31,8 +30,6 @@ func NewStream(source any) *Stream {
s.producers = append(s.producers, prod)
}
return s
case *Stream:
return source
case map[string]any:
return NewStream(source["url"])
case nil:
@@ -50,24 +47,28 @@ func (s *Stream) SetSource(source string) {
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
atomic.AddInt32(&s.requests, 1)
consN := atomic.AddInt32(&s.requests, 1) - 1
var producers []*Producer // matched producers for consumer
var codecs string
var statErrors []error
var statMedias []*core.Media
var statProds []*Producer // matched producers for consumer
// Step 1. Get consumer medias
for _, consMedia := range cons.GetMedias() {
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
producers:
for _, prod := range s.producers {
for prodN, prod := range s.producers {
if err = prod.Dial(); err != nil {
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
statErrors = append(statErrors, err)
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
collectCodecs(prodMedia, &codecs)
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
statMedias = append(statMedias, prodMedia)
// Step 3. Match consumer/producer codecs list
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
@@ -79,6 +80,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
switch prodMedia.Direction {
case core.DirectionRecvonly:
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
@@ -91,6 +94,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
}
case core.DirectionSendonly:
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
// Step 4. Get recvonly track from consumer (backchannel)
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
@@ -103,7 +108,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
}
}
producers = append(producers, prod)
statProds = append(statProds, prod)
if !consMedia.MatchAll() {
break producers
@@ -117,18 +122,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
s.stopProducers()
}
if len(producers) == 0 {
if len(codecs) > 0 {
return errors.New("codecs not match: " + codecs)
}
for i, producer := range s.producers {
if producer.lastErr != nil {
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
}
}
return fmt.Errorf("sources unavailable: %d", len(s.producers))
if len(statProds) == 0 {
return formatError(statMedias, statErrors)
}
s.mu.Lock()
@@ -136,7 +131,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range producers {
for _, prod := range statProds {
prod.start()
}
@@ -185,6 +180,11 @@ producers:
continue producers
}
}
for _, track := range producer.senders {
if len(track.Senders()) > 0 {
continue producers
}
}
producer.stop()
}
s.mu.Unlock()
@@ -208,22 +208,47 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
return json.Marshal(info)
}
func collectCodecs(media *core.Media, codecs *string) {
if media.Direction == core.DirectionRecvonly {
return
}
func formatError(statMedias []*core.Media, statErrors []error) error {
var text string
for _, codec := range media.Codecs {
name := codec.Name
if name == core.CodecAAC {
name = "AAC"
}
if strings.Contains(*codecs, name) {
for _, media := range statMedias {
if media.Direction == core.DirectionRecvonly {
continue
}
if len(*codecs) > 0 {
*codecs += ","
for _, codec := range media.Codecs {
name := codec.Name
if name == core.CodecAAC {
name = "AAC"
}
if strings.Contains(text, name) {
continue
}
if len(text) > 0 {
text += ","
}
text += name
}
*codecs += name
}
if text != "" {
return errors.New(text)
}
for _, err := range statErrors {
s := err.Error()
if strings.Contains(text, s) {
continue
}
if len(text) > 0 {
text += ","
}
text += s
}
if text != "" {
return errors.New(text)
}
return errors.New("unknown error")
}

View File

@@ -0,0 +1,19 @@
package streams
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestTemplate(t *testing.T) {
source1 := "does not matter"
stream1 := New("from_yaml", source1)
require.Len(t, streams, 1)
stream2 := NewTemplate("camera.from_hass", "rtsp://localhost:8554/from_yaml?video")
require.Equal(t, stream1, stream2)
require.Equal(t, stream2.producers[0].url, source1)
require.Len(t, streams, 2)
}

View File

@@ -1,7 +1,7 @@
package tapo
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tapo"
)

View File

@@ -1,7 +1,7 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"strconv"

View File

@@ -2,7 +2,7 @@ package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"

View File

@@ -2,9 +2,9 @@ package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"

View File

@@ -2,7 +2,7 @@ package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"

View File

@@ -3,10 +3,10 @@ package webtorrent
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/rs/zerolog"

52
main.go
View File

@@ -1,31 +1,32 @@
package main
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/dvrip"
"github.com/AlexxIT/go2rtc/cmd/echo"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/hls"
"github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/http"
"github.com/AlexxIT/go2rtc/cmd/isapi"
"github.com/AlexxIT/go2rtc/cmd/ivideon"
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
"github.com/AlexxIT/go2rtc/cmd/mp4"
"github.com/AlexxIT/go2rtc/cmd/mpegts"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/roborock"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/tapo"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/cmd/webtorrent"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/debug"
"github.com/AlexxIT/go2rtc/internal/dvrip"
"github.com/AlexxIT/go2rtc/internal/echo"
"github.com/AlexxIT/go2rtc/internal/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/hls"
"github.com/AlexxIT/go2rtc/internal/homekit"
"github.com/AlexxIT/go2rtc/internal/http"
"github.com/AlexxIT/go2rtc/internal/isapi"
"github.com/AlexxIT/go2rtc/internal/ivideon"
"github.com/AlexxIT/go2rtc/internal/mjpeg"
"github.com/AlexxIT/go2rtc/internal/mp4"
"github.com/AlexxIT/go2rtc/internal/mpegts"
"github.com/AlexxIT/go2rtc/internal/ngrok"
"github.com/AlexxIT/go2rtc/internal/onvif"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/rtmp"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/tapo"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"os"
"os/signal"
"syscall"
@@ -35,6 +36,7 @@ func main() {
app.Init() // init config and logs
api.Init() // init HTTP API server
streams.Init() // load streams list
onvif.Init()
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client

View File

@@ -68,7 +68,7 @@ func (c *Codec) Match(remote *Codec) bool {
}
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c := &Codec{PayloadType: byte(atoi(payloadType))}
c := &Codec{PayloadType: byte(Atoi(payloadType))}
for _, attr := range md.Attributes {
switch {
@@ -78,7 +78,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c.Name = strings.ToUpper(ss[0])
// fix tailing space: `a=rtpmap:96 H264/90000 `
c.ClockRate = uint32(atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
c.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
if len(ss) == 3 && ss[2] == "2" {
c.Channels = 2
@@ -99,9 +99,16 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
case "8":
c.Name = CodecPCMA
c.ClockRate = 8000
case "10":
c.Name = CodecPCM
c.ClockRate = 44100
c.Channels = 2
case "11":
c.Name = CodecPCM
c.ClockRate = 44100
case "14":
c.Name = CodecMP3
c.ClockRate = 44100
c.ClockRate = 90000 // it's not real sample rate
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
@@ -113,11 +120,6 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
return c
}
func atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
}
func DecodeH264(fmtp string) string {
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {

View File

@@ -27,7 +27,8 @@ const (
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
CodecPCM = "L16" // Linear PCM
CodecELD = "ELD" // AAC-ELD
CodecELD = "ELD" // AAC-ELD
CodecFLAC = "FLAC"
CodecAll = "ALL"
CodecAny = "ANY"

View File

@@ -6,8 +6,15 @@ import (
"runtime"
"strconv"
"strings"
"time"
)
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
// same as: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
func Now90000() uint32 {
return uint32(time.Duration(time.Now().UnixMilli()) * 90)
}
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
@@ -22,6 +29,15 @@ func RandString(size, base byte) string {
return string(b)
}
func Any(errs ...error) error {
for _, err := range errs {
if err != nil {
return err
}
}
return nil
}
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
if i < 0 {
@@ -41,6 +57,11 @@ func Between(s, sub1, sub2 string) string {
return s
}
func Atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
}
func Assert(ok bool) {
if !ok {
_, file, line, _ := runtime.Caller(1)

View File

@@ -82,11 +82,18 @@ func (m *Media) MatchAll() bool {
return false
}
func (m *Media) Equal(media *Media) bool {
if media.ID != "" {
return m.ID == media.ID
}
return m.String() == media.String()
}
func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC:
return KindAudio
}
return ""
@@ -129,6 +136,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
}
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
if media.ID != "" {
md.WithValueAttribute("control", media.ID)
}
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
}

View File

@@ -18,7 +18,7 @@ type Receiver struct {
ID byte // Channel for RTSP, PayloadType for MPEG-TS
senders map[*Sender]chan *rtp.Packet
mu sync.Mutex
mu sync.RWMutex
bytes int
}
@@ -32,9 +32,9 @@ func (t *Receiver) WriteRTP(packet *rtp.Packet) {
t.mu.Lock()
t.bytes += len(packet.Payload)
for sender, buffer := range t.senders {
if len(buffer) < cap(buffer) {
buffer <- packet
} else {
select {
case buffer <- packet:
default:
sender.overflow++
}
}
@@ -42,11 +42,11 @@ func (t *Receiver) WriteRTP(packet *rtp.Packet) {
}
func (t *Receiver) Senders() (senders []*Sender) {
t.mu.Lock()
t.mu.RLock()
for sender := range t.senders {
senders = append(senders, sender)
}
t.mu.Unlock()
t.mu.RUnlock()
return
}
@@ -73,12 +73,9 @@ func (t *Receiver) Replace(target *Receiver) {
func (t *Receiver) String() string {
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
if t.mu.TryLock() {
s += fmt.Sprintf(", senders=%d", len(t.senders))
t.mu.Unlock()
} else {
s += fmt.Sprintf(", senders=?")
}
t.mu.RLock()
s += fmt.Sprintf(", senders=%d", len(t.senders))
t.mu.RUnlock()
return s
}
@@ -93,7 +90,7 @@ type Sender struct {
Handler HandlerFunc
receivers []*Receiver
mu sync.Mutex
mu sync.RWMutex
bytes int
overflow int
@@ -127,7 +124,6 @@ func (s *Sender) HandleRTP(track *Receiver) {
}
track.senders[s] = buffer
track.mu.Unlock()
s.mu.Lock()
s.receivers = append(s.receivers, track)
s.mu.Unlock()
@@ -135,7 +131,9 @@ func (s *Sender) HandleRTP(track *Receiver) {
go func() {
// read packets from buffer channel until it will be closed
for packet := range buffer {
s.mu.Lock()
s.bytes += len(packet.Payload)
s.mu.Unlock()
s.Handler(packet)
}
@@ -171,12 +169,9 @@ func (s *Sender) Close() {
func (s *Sender) String() string {
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
if s.mu.TryLock() {
info += ", receivers=" + strconv.Itoa(len(s.receivers))
s.mu.Unlock()
} else {
info += ", receivers=?"
}
s.mu.RLock()
info += ", receivers=" + strconv.Itoa(len(s.receivers))
s.mu.RUnlock()
if s.overflow > 0 {
info += ", overflow=" + strconv.Itoa(s.overflow)
}

80
pkg/ffmpeg/ffmpeg.go Normal file
View File

@@ -0,0 +1,80 @@
package ffmpeg
import (
"bytes"
"strconv"
"strings"
)
type Args struct {
Bin string // ffmpeg
Global string // -hide_banner -v error
Input string // -re -stream_loop -1 -i /media/bunny.mp4
Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
Filters []string // scale=1920:1080
Output string // -f rtsp {output}
Video, Audio int // count of Video and Audio params
}
func (a *Args) AddCodec(codec string) {
a.Codecs = append(a.Codecs, codec)
}
func (a *Args) AddFilter(filter string) {
a.Filters = append(a.Filters, filter)
}
func (a *Args) InsertFilter(filter string) {
a.Filters = append([]string{filter}, a.Filters...)
}
func (a *Args) String() string {
b := bytes.NewBuffer(make([]byte, 0, 512))
b.WriteString(a.Bin)
if a.Global != "" {
b.WriteByte(' ')
b.WriteString(a.Global)
}
b.WriteByte(' ')
b.WriteString(a.Input)
multimode := a.Video > 1 || a.Audio > 1
var iv, ia int
for _, codec := range a.Codecs {
// support multiple video and/or audio codecs
if multimode && len(codec) >= 5 {
switch codec[:5] {
case "-c:v ":
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
iv++
case "-c:a ":
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
ia++
}
}
b.WriteByte(' ')
b.WriteString(codec)
}
if a.Filters != nil {
for i, filter := range a.Filters {
if i == 0 {
b.WriteString(" -vf ")
} else {
b.WriteByte(',')
}
b.WriteString(filter)
}
}
b.WriteByte(' ')
b.WriteString(a.Output)
return b.String()
}

View File

@@ -26,6 +26,19 @@ func AnnexB2AVC(b []byte) []byte {
return b
}
func AVCtoAnnexB(b []byte) []byte {
b = bytes.Clone(b)
for i := 0; i < len(b); {
size := int(binary.BigEndian.Uint32(b[i:]))
b[i] = 0
b[i+1] = 0
b[i+2] = 0
b[i+3] = 1
i += 4 + size
}
return b
}
const forbiddenZeroBit = 0x80
const nalUnitType = 0x1F

View File

@@ -94,6 +94,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
}
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
if mtu == 0 {
mtu = 1472
}
payloader := &Payloader{IsAVC: true}
sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size

54
pkg/h265/avc.go Normal file
View File

@@ -0,0 +1,54 @@
package h265
import "github.com/AlexxIT/go2rtc/pkg/h264"
const forbiddenZeroBit = 0x80
const nalUnitType = 0x3F
// DecodeStream - find and return first AU in AVC format
// useful for processing live streams with unknown separator size
func DecodeStream(annexb []byte) ([]byte, int) {
startPos := -1
i := 0
for {
// search next separator
if i = h264.IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 {
break
}
// move i to next AU
if i += 3; i >= len(annexb) {
break
}
// check if AU type valid
octet := annexb[i]
if octet&forbiddenZeroBit != 0 {
continue
}
nalType := (octet >> 1) & nalUnitType
if startPos >= 0 {
switch nalType {
case NALUTypeVPS, NALUTypePFrame:
if annexb[i-4] == 0 {
return h264.DecodeAnnexB(annexb[startPos : i-4]), i - 4
} else {
return h264.DecodeAnnexB(annexb[startPos : i-3]), i - 3
}
}
} else {
switch nalType {
case NALUTypeVPS, NALUTypePFrame:
if i >= 4 && annexb[i-4] == 0 {
startPos = i - 4
} else {
startPos = i - 3
}
}
}
}
return nil, 0
}

View File

@@ -76,6 +76,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
}
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
if mtu == 0 {
mtu = 1472
}
payloader := &Payloader{}
sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size

View File

@@ -32,6 +32,16 @@ const (
Mdat = "mdat"
)
const (
sampleIsNonSync = 0x10000
sampleDependsOn1 = 0x1000000
sampleDependsOn2 = 0x2000000
SampleVideoIFrame = sampleDependsOn2
SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync
SampleAudio = sampleIsNonSync
)
func (m *Movie) WriteFileType() {
m.StartAtom(Ftyp)
m.WriteString("iso5")
@@ -250,7 +260,7 @@ func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, chann
m.EndAtom() // TRAK
}
func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) {
func (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, time uint64) {
m.StartAtom(Moof)
m.StartAtom(MoofMfhd)
@@ -276,10 +286,10 @@ func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64)
TfhdDefaultSampleFlags |
TfhdDefaultBaseIsMoof,
)
m.WriteUint32(tid) // track id
m.WriteUint32(duration) // default sample duration
m.WriteUint32(size) // default sample size
m.WriteUint32(0x2000000) // default sample flags
m.WriteUint32(tid) // track id
m.WriteUint32(duration) // default sample duration
m.WriteUint32(size) // default sample size
m.WriteUint32(flags) // default sample flags
m.EndAtom()
m.StartAtom(MoofTrafTfdt)
@@ -314,5 +324,4 @@ func (m *Movie) WriteData(b []byte) {
m.StartAtom(Mdat)
m.Write(b)
m.EndAtom()
}

View File

@@ -2,6 +2,7 @@ package iso
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/pcm"
)
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
@@ -46,9 +47,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
switch codec {
case core.CodecAAC, core.CodecMP3:
m.StartAtom("mp4a")
m.StartAtom("mp4a") // supported in all players and browsers
case core.CodecFLAC:
m.StartAtom("fLaC") // supported in all players and browsers
case core.CodecOpus:
m.StartAtom("Opus")
m.StartAtom("Opus") // supported in Chrome and Firefox
case core.CodecPCMU:
m.StartAtom("ulaw")
case core.CodecPCMA:
@@ -56,6 +59,11 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
default:
panic("unsupported iso audio: " + codec)
}
if channels == 0 {
channels = 1
}
m.Skip(6)
m.WriteUint16(1) // data_reference_index
m.Skip(2) // version
@@ -72,6 +80,10 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
m.WriteEsdsAAC(conf)
case core.CodecMP3:
m.WriteEsdsMP3()
case core.CodecFLAC:
m.StartAtom("dfLa")
m.Write(pcm.FLACHeader(false, sampleRate))
m.EndAtom()
case core.CodecOpus:
// don't know what means this magic
m.StartAtom("dOps")
@@ -106,6 +118,7 @@ func (m *Movie) WriteEsdsAAC(conf []byte) {
m.Skip(2) // es id
m.Skip(1) // es flags
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#aac-audio
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
m.WriteBytes(0x40) // object id
m.WriteBytes(0x15) // stream type
@@ -139,6 +152,7 @@ func (m *Movie) WriteEsdsMP3() {
m.Skip(2) // es id
m.Skip(1) // es flags
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#mp3-audio
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
m.WriteBytes(0x6B) // object id
m.WriteBytes(0x15) // stream type

214
pkg/magic/client.go Normal file
View File

@@ -0,0 +1,214 @@
package magic
import (
"bytes"
"encoding/hex"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/pion/rtp"
"io"
)
// Client - can read unknown bytestream and autodetect format
type Client struct {
Desc string
URL string
Handle func() error
r io.ReadCloser
sniff []byte
medias []*core.Media
receiver *core.Receiver
recv int
}
func NewClient(r io.ReadCloser) *Client {
return &Client{r: r}
}
func (c *Client) Probe() (err error) {
c.sniff = make([]byte, mpegts.PacketSize*3) // MPEG-TS: SDT+PAT+PMT
c.recv, err = io.ReadFull(c.r, c.sniff)
if err != nil {
_ = c.Close()
return
}
var codec *core.Codec
if bytes.HasPrefix(c.sniff, []byte{0, 0, 0, 1}) {
switch {
case h264.NALUType(c.sniff) == h264.NALUTypeSPS:
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
}
c.Handle = c.ReadBitstreams
case h265.NALUType(c.sniff) == h265.NALUTypeVPS:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
}
c.Handle = c.ReadBitstreams
}
} else if bytes.HasPrefix(c.sniff, []byte{0xFF, 0xD8}) {
codec = &core.Codec{
Name: core.CodecJPEG,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
}
c.Handle = c.ReadMJPEG
} else if c.sniff[0] == mpegts.SyncByte {
ts := mpegts.NewReader()
ts.AppendBuffer(c.sniff)
_ = ts.GetPacket()
for _, streamType := range ts.GetStreamTypes() {
switch streamType {
case mpegts.StreamTypeH264:
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
}
c.Handle = c.ReadMPEGTS
case mpegts.StreamTypeH265:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
}
c.Handle = c.ReadMPEGTS
}
}
}
if codec == nil {
_ = c.Close()
return errors.New("unknown format: " + hex.EncodeToString(c.sniff[:8]))
}
c.medias = append(c.medias, &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
})
return
}
func (c *Client) ReadBitstreams() error {
buf := c.sniff // total bufer
b := make([]byte, 1024*1024) // reading buffer
var decodeStream func([]byte) ([]byte, int)
switch c.receiver.Codec.Name {
case core.CodecH264:
decodeStream = h264.DecodeStream
case core.CodecH265:
decodeStream = h265.DecodeStream
}
for {
payload, n := decodeStream(buf)
if payload == nil {
n, err := c.r.Read(b)
if err != nil {
return err
}
buf = append(buf, b[:n]...)
c.recv += n
continue
}
buf = buf[n:]
//log.Printf("[AVC] %v, len: %d", h264.Types(payload), len(payload))
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: payload,
}
c.receiver.WriteRTP(pkt)
}
}
func (c *Client) ReadMJPEG() error {
buf := c.sniff // total bufer
b := make([]byte, 1024*1024) // reading buffer
for {
// one JPEG end and next start
i := bytes.Index(buf, []byte{0xFF, 0xD9, 0xFF, 0xD8})
if i < 0 {
n, err := c.r.Read(b)
if err != nil {
return err
}
buf = append(buf, b[:n]...)
c.recv += n
// if we receive frame
if n >= 2 && b[n-2] == 0xFF && b[n-1] == 0xD9 {
i = len(buf)
} else {
continue
}
} else {
i += 2
}
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: buf[:i],
}
c.receiver.WriteRTP(pkt)
buf = buf[i:]
}
}
func (c *Client) ReadMPEGTS() error {
b := make([]byte, 1024*1024) // reading buffer
ts := mpegts.NewReader()
ts.AppendBuffer(c.sniff)
for {
packet := ts.GetPacket()
if packet == nil {
n, err := c.r.Read(b)
if err != nil {
return err
}
ts.AppendBuffer(b[:n])
c.recv += n
continue
}
//log.Printf("[AVC] %v, len: %d, ts: %10d", h264.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
switch packet.PayloadType {
case mpegts.StreamTypeH264, mpegts.StreamTypeH265:
c.receiver.WriteRTP(packet)
}
}
}
func (c *Client) Close() error {
return c.r.Close()
}

91
pkg/magic/keyframe.go Normal file
View File

@@ -0,0 +1,91 @@
package magic
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/pion/rtp"
)
type Keyframe struct {
core.Listener
UserAgent string
RemoteAddr string
medias []*core.Media
sender *core.Sender
}
func (k *Keyframe) GetMedias() []*core.Media {
if k.medias == nil {
k.medias = append(k.medias, &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
{Name: core.CodecJPEG},
},
})
}
return k.medias
}
func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
var handler core.HandlerFunc
switch track.Codec.Name {
case core.CodecH264:
handler = func(packet *rtp.Packet) {
if !h264.IsKeyframe(packet.Payload) {
return
}
b := h264.AVCtoAnnexB(packet.Payload)
k.Fire(b)
}
if track.Codec.IsRTP() {
handler = h264.RTPDepay(track.Codec, handler)
}
case core.CodecH265:
handler = func(packet *rtp.Packet) {
if !h265.IsKeyframe(packet.Payload) {
return
}
k.Fire(packet.Payload)
}
if track.Codec.IsRTP() {
handler = h265.RTPDepay(track.Codec, handler)
}
case core.CodecJPEG:
handler = func(packet *rtp.Packet) {
k.Fire(packet.Payload)
}
if track.Codec.IsRTP() {
handler = mjpeg.RTPDepay(handler)
}
}
k.sender = core.NewSender(media, track.Codec)
k.sender.Handler = handler
k.sender.HandleRTP(track)
return nil
}
func (k *Keyframe) CodecName() string {
if k.sender != nil {
return k.sender.Codec.Name
}
return ""
}
func (k *Keyframe) Stop() error {
if k.sender != nil {
k.sender.Close()
}
return nil
}

41
pkg/magic/producer.go Normal file
View File

@@ -0,0 +1,41 @@
package magic
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Client) GetMedias() []*core.Media {
return c.medias
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
if c.receiver == nil {
c.receiver = core.NewReceiver(media, codec)
}
return c.receiver, nil
}
func (c *Client) Start() error {
return c.Handle()
}
func (c *Client) Stop() (err error) {
if c.receiver != nil {
c.receiver.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: c.Desc,
URL: c.URL,
Medias: c.medias,
Recv: c.recv,
}
if c.receiver != nil {
info.Receivers = append(info.Receivers, c.receiver)
}
return json.Marshal(info)
}

View File

@@ -6,7 +6,9 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/pion/rtp"
"sync"
)
type Consumer struct {
@@ -19,6 +21,7 @@ type Consumer struct {
senders []*core.Sender
muxer *Muxer
mu sync.Mutex
wait byte
send int
@@ -52,7 +55,8 @@ func (c *Consumer) GetMedias() []*core.Media {
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
trackID := byte(len(c.senders))
handler := core.NewSender(media, track.Codec)
codec := track.Codec.Clone()
handler := core.NewSender(media, codec)
switch track.Codec.Name {
case core.CodecH264:
@@ -70,10 +74,12 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
c.wait = waitNone
}
// important to use Mutex because right fragment order
c.mu.Lock()
buf := c.muxer.Marshal(trackID, packet)
c.Fire(buf)
c.send += len(buf)
c.mu.Unlock()
}
if track.Codec.IsRTP() {
@@ -97,46 +103,48 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
c.wait = waitNone
}
c.mu.Lock()
buf := c.muxer.Marshal(trackID, packet)
c.Fire(buf)
c.send += len(buf)
c.mu.Unlock()
}
if track.Codec.IsRTP() {
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
}
case core.CodecAAC:
handler.Handler = func(packet *rtp.Packet) {
if c.wait != waitNone {
return
}
buf := c.muxer.Marshal(trackID, packet)
c.Fire(buf)
c.send += len(buf)
}
if track.Codec.IsRTP() {
handler.Handler = aac.RTPDepay(handler.Handler)
}
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
handler.Handler = func(packet *rtp.Packet) {
if c.wait != waitNone {
return
}
buf := c.muxer.Marshal(trackID, packet)
c.Fire(buf)
c.send += len(buf)
}
default:
panic("unsupported codec")
handler.Handler = func(packet *rtp.Packet) {
if c.wait != waitNone {
return
}
c.mu.Lock()
buf := c.muxer.Marshal(trackID, packet)
c.Fire(buf)
c.send += len(buf)
c.mu.Unlock()
}
switch track.Codec.Name {
case core.CodecAAC:
if track.Codec.IsRTP() {
handler.Handler = aac.RTPDepay(handler.Handler)
}
case core.CodecOpus, core.CodecMP3: // no changes
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler)
codec.Name = core.CodecFLAC
default:
handler.Handler = nil
}
}
if handler.Handler == nil {
println("ERROR: MP4 unsupported codec: " + track.Codec.String())
return nil
}
handler.HandleRTP(track)

View File

@@ -4,9 +4,45 @@ import "github.com/AlexxIT/go2rtc/pkg/core"
// ParseQuery - like usual parse, but with mp4 param handler
func ParseQuery(query map[string][]string) []*core.Media {
if query["mp4"] != nil {
cons := Consumer{}
return cons.GetMedias()
if v := query["mp4"]; len(v) != 0 {
medias := []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
if v[0] == "" {
return medias // legacy
}
medias[1].Codecs = append(medias[1].Codecs,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
)
if v[0] == "flac" {
return medias // modern browsers
}
medias[1].Codecs = append(medias[1].Codecs,
&core.Codec{Name: core.CodecOpus},
&core.Codec{Name: core.CodecMP3},
)
return medias // Chrome, FFmpeg, VLC
}
return core.ParseQuery(query)

View File

@@ -15,12 +15,14 @@ type Muxer struct {
fragIndex uint32
dts []uint64
pts []uint32
codecs []*core.Codec
}
const (
MimeH264 = "avc1.640029"
MimeH265 = "hvc1.1.6.L153.B0"
MimeAAC = "mp4a.40.2"
MimeFlac = "flac"
MimeOpus = "opus"
)
@@ -43,6 +45,8 @@ func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
s += MimeAAC
case core.CodecOpus:
s += MimeOpus
case core.CodecFLAC:
s += MimeFlac
}
}
@@ -60,9 +64,11 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
switch codec.Name {
case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
// some dummy SPS and PPS not a problem
if len(sps) == 0 {
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
}
if len(pps) == 0 {
pps = []byte{0x68, 0xce, 0x38, 0x80}
}
@@ -79,10 +85,14 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
case core.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
// some dummy SPS and PPS not a problem
if len(vps) == 0 {
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
}
if len(sps) == 0 {
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
}
if len(pps) == 0 {
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
}
@@ -108,14 +118,15 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
)
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC:
mv.WriteAudioTrack(
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
)
}
m.pts = append(m.pts, 0)
m.dts = append(m.dts, 0)
m.pts = append(m.pts, 0)
m.codecs = append(m.codecs, codec)
}
mv.StartAtom(iso.MoovMvex)
@@ -138,28 +149,49 @@ func (m *Muxer) Reset() {
}
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
// important before increment
time := m.dts[trackID]
codec := m.codecs[trackID]
duration := packet.Timestamp - m.pts[trackID]
m.pts[trackID] = packet.Timestamp
// minumum duration important for MSE in Apple Safari
if duration == 0 || duration > codec.ClockRate {
duration = codec.ClockRate/1000 + 1
m.pts[trackID] += duration
}
size := len(packet.Payload)
// flags important for Apple Finder video preview
var flags uint32
switch codec.Name {
case core.CodecH264:
if h264.IsKeyframe(packet.Payload) {
flags = iso.SampleVideoIFrame
} else {
flags = iso.SampleVideoNonIFrame
}
case core.CodecH265:
if h265.IsKeyframe(packet.Payload) {
flags = iso.SampleVideoIFrame
} else {
flags = iso.SampleVideoNonIFrame
}
default:
flags = iso.SampleAudio // not important
}
m.fragIndex++
var duration uint32
newTime := packet.Timestamp
if m.pts[trackID] > 0 {
duration = newTime - m.pts[trackID]
m.dts[trackID] += uint64(duration)
} else {
// important, or Safari will fail with first frame
duration = 1
}
m.pts[trackID] = newTime
mv := iso.NewMovie(1024 + len(packet.Payload))
mv := iso.NewMovie(1024 + size)
mv.WriteMovieFragment(
m.fragIndex, uint32(trackID+1), duration,
uint32(len(packet.Payload)), time,
m.fragIndex, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID],
)
mv.WriteData(packet.Payload)
//log.Printf("[MP4] track=%d ts=%6d dur=%5d idx=%3d len=%d", trackID+1, m.dts[trackID], duration, m.fragIndex, len(packet.Payload))
m.dts[trackID] += uint64(duration)
return mv.Bytes()
}

View File

@@ -23,7 +23,7 @@ func NewClient(res *http.Response) *Client {
func (c *Client) Handle() error {
reader := NewReader()
b := make([]byte, 1024*1024*256) // 256K
b := make([]byte, 1024*256) // 256K
probe := core.NewProbe(c.medias == nil)
for probe == nil || probe.Active() {

View File

@@ -3,6 +3,7 @@ package mpegts
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
"time"
)
@@ -16,6 +17,7 @@ const (
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
StreamTypeAAC = 0x0F
StreamTypeH264 = 0x1B
StreamTypeH265 = 0x24
StreamTypePCMATapo = 0x90
)
@@ -36,6 +38,8 @@ type PES struct {
Sequence uint16
Timestamp uint32
decodeStream func([]byte) ([]byte, int)
}
const (
@@ -52,9 +56,14 @@ func (p *PES) SetBuffer(size uint16, b []byte) {
optSize := b[2] // optional fields
b = b[minHeaderSize+optSize:]
if p.StreamType == StreamTypeH264 {
switch p.StreamType {
case StreamTypeH264:
p.Mode = ModeStream
} else {
p.decodeStream = h264.DecodeStream
case StreamTypeH265:
p.Mode = ModeStream
p.decodeStream = h265.DecodeStream
default:
println("WARNING: mpegts: unknown zero-size stream")
}
} else {
@@ -91,7 +100,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
payload := p.Payload[minHeaderSize+optSize:]
switch p.StreamType {
case StreamTypeH264:
case StreamTypeH264, StreamTypeH265:
var ts uint32
const hasPTS = 0b1000_0000
@@ -125,7 +134,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
p.Payload = nil
case ModeStream:
payload, i := h264.DecodeStream(p.Payload)
payload, i := p.decodeStream(p.Payload)
if payload == nil {
return
}
@@ -137,7 +146,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
pkt = &rtp.Packet{
Header: rtp.Header{
PayloadType: p.StreamType,
Timestamp: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second),
Timestamp: core.Now90000(),
},
Payload: payload,
}

View File

@@ -129,6 +129,14 @@ func (r *Reader) GetPacket() *rtp.Packet {
return nil
}
func (r *Reader) GetStreamTypes() []byte {
types := make([]byte, 0, len(r.pes))
for _, pes := range r.pes {
types = append(types, pes.StreamType)
}
return types
}
// Sync - search sync byte
func (r *Reader) Sync() bool {
// drop previous readed packet

255
pkg/onvif/client.go Normal file
View File

@@ -0,0 +1,255 @@
package onvif
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"html"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
type Client struct {
url *url.URL
deviceURL string
mediaURL string
imaginURL string
}
func NewClient(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
baseURL := "http://" + u.Host
client := &Client{url: u}
if u.Path == "" {
client.deviceURL = baseURL + PathDevice
} else {
client.deviceURL = baseURL + u.Path
}
b, err := client.GetCapabilities()
if err != nil {
return nil, err
}
client.mediaURL = FindTagValue(b, "Media.+?XAddr")
client.imaginURL = FindTagValue(b, "Imaging.+?XAddr")
return client, nil
}
func (c *Client) GetURI() (string, error) {
query := c.url.Query()
token := query.Get("subtype")
// support empty
if i := atoi(token); i >= 0 {
tokens, err := c.GetProfilesTokens()
if err != nil {
return "", err
}
if i >= len(tokens) {
return "", errors.New("onvif: wrong subtype")
}
token = tokens[i]
}
getUri := c.GetStreamUri
if query.Has("snapshot") {
getUri = c.GetSnapshotUri
}
b, err := getUri(token)
if err != nil {
return "", err
}
uri := FindTagValue(b, "Uri")
uri = html.UnescapeString(uri)
u, err := url.Parse(uri)
if err != nil {
return "", err
}
if u.User == nil && c.url.User != nil {
u.User = c.url.User
}
return u.String(), nil
}
func (c *Client) GetName() (string, error) {
b, err := c.GetDeviceInformation()
if err != nil {
return "", err
}
return FindTagValue(b, "Manufacturer") + " " + FindTagValue(b, "Model"), nil
}
func (c *Client) GetProfilesTokens() ([]string, error) {
b, err := c.GetProfiles()
if err != nil {
return nil, err
}
var tokens []string
re := regexp.MustCompile(`Profiles.+?token="([^"]+)`)
for _, s := range re.FindAllStringSubmatch(string(b), 10) {
tokens = append(tokens, s[1])
}
return tokens, nil
}
func (c *Client) HasSnapshots() bool {
b, err := c.GetServiceCapabilities()
if err != nil {
return false
}
return strings.Contains(string(b), `SnapshotUri="true"`)
}
func (c *Client) GetCapabilities() ([]byte, error) {
return c.Request(
c.deviceURL,
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Category>All</tds:Category>
</tds:GetCapabilities>`,
)
}
func (c *Client) GetNetworkInterfaces() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetDeviceInformation() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetProfiles() ([]byte, error) {
return c.Request(
c.mediaURL, `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
)
}
func (c *Client) GetStreamUri(token string) ([]byte, error) {
return c.Request(
c.mediaURL,
`<trt:GetStreamUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
<trt:StreamSetup>
<tt:Stream>RTP-Unicast</tt:Stream>
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>
</trt:StreamSetup>
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetStreamUri>`,
)
}
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
return c.Request(
c.imaginURL,
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetSnapshotUri>`,
)
}
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetServiceCapabilities() ([]byte, error) {
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
return c.Request(
c.mediaURL, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
)
}
func (c *Client) SystemReboot() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetServices() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:IncludeCapability>true</tds:IncludeCapability>
</tds:GetServices>`,
)
}
func (c *Client) GetScopes() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
)
}
func (c *Client) Request(url, body string) ([]byte, error) {
if url == "" {
return nil, errors.New("onvif: unsupported service")
}
buf := bytes.NewBuffer(nil)
buf.WriteString(
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
)
if user := c.url.User; user != nil {
nonce := core.RandString(16, 36)
created := time.Now().UTC().Format(time.RFC3339Nano)
pass, _ := user.Password()
h := sha1.New()
h.Write([]byte(nonce + created + pass))
buf.WriteString(`<s:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>` + user.Username() + `</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` + created + `</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>`)
}
buf.WriteString(`<s:Body>` + body + `</s:Body></s:Envelope>`)
client := &http.Client{Timeout: time.Second * 5000}
res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf)
if err != nil {
return nil, err
}
// need to close body with eny response status
b, err := io.ReadAll(res.Body)
if err == nil && res.StatusCode != http.StatusOK {
err = errors.New("onvif: " + res.Status + " for " + url)
}
return b, err
}

Some files were not shown because too many files have changed in this diff Show More