Compare commits

...

32 Commits

Author SHA1 Message Date
Alexey Khit a8526ae4eb Update version to 1.6.1 2023-07-20 08:12:10 +03:00
Alexey Khit 966fbe7d61 Update readme about webrtc wyze and kinesis sources 2023-07-20 08:11:26 +03:00
Alexey Khit a77c2ef71f Update readme about new bubble source 2023-07-20 08:06:04 +03:00
Alexey Khit 61a194e396 Update readme about JPEG snapshot query params 2023-07-20 08:05:42 +03:00
Alexey Khit ae25784d72 Update readme about MP4 stream query params 2023-07-20 08:04:58 +03:00
Alexey Khit 3343c78699 Add WebRTC sources for Amazon Kinesis and Wyze 2023-07-19 23:36:31 +03:00
Alexey Khit 7928f54a95 Fix handling bubble source 2023-07-17 18:28:21 +03:00
Alexey Khit e4b68518e5 Remove all listeners from IPv6 interface 2023-07-17 18:28:15 +03:00
Alexey Khit 14ed1cdee8 Add restriction on symbols in dynamic source 2023-07-17 18:28:06 +03:00
Alexey Khit 72f159be88 Update Windows USB audio default settings 2023-07-16 18:40:54 +03:00
Alexey Khit 144954b979 Add default params to Linux ALSA 2023-07-16 14:09:13 +03:00
Alexey Khit 9e15391471 Code refactoring after #517 2023-07-16 13:43:27 +03:00
Alexey Khit d62b1e445a Merge pull request #517 from skrashevich/230711-jpg-resize 2023-07-16 07:01:40 +03:00
Alexey Khit ade4c035b7 Fix resample to G711 for WebRTC 2023-07-16 06:36:26 +03:00
Alexey Khit 13ca991c37 Add support pcm_s16le audio 2023-07-15 15:06:49 +03:00
Alexey Khit e48459f49d Add channels and sample rate params to ALSA 2023-07-15 14:43:47 +03:00
Alexey Khit facf18e0df Code refactoring for source bubble 2023-07-15 14:43:47 +03:00
Alexey Khit 5c93dc62bd Add support source Bubble (Eseenet/dvr163) 2023-07-15 11:46:10 +03:00
Alexey Khit d272d4b6c3 Fix FLAC mime type for Chrome 2023-07-15 11:42:50 +03:00
Alexey Khit 1b41edfc7e Fix empty SPS/PPS for HLS/TS 2023-07-15 11:42:12 +03:00
Alexey Khit d55270bd64 Fix tests 2023-07-13 23:49:17 +03:00
Alexey Khit 85225917f5 Rewritten streams creation 2023-07-13 23:32:01 +03:00
Alexey Khit eaef62a775 Update RTSPtoWebRTC errors output 2023-07-13 22:52:03 +03:00
Alexey Khit f6c8d63658 Another fix for OPUS audio quality 2023-07-13 20:31:59 +03:00
Alexey Khit ea82d7ec2b Add support rotate and scale to MP4 stream 2023-07-13 19:32:55 +03:00
Alexey Khit e8a7ba056c Add Wyze project to readme 2023-07-13 18:38:08 +03:00
Alexey Khit 9fd40467f2 Update codecs detection for Safari browsers 2023-07-13 16:16:37 +03:00
Alexey Khit c81e29fe54 Fix FLAC mime type for HLS 2023-07-13 16:14:50 +03:00
Alexey Khit b9b7bb5489 Adds README for API 2023-07-13 16:10:23 +03:00
Alexey Khit 8036278e29 Fix complex Content-Type for image/jpeg #278 2023-07-11 15:57:21 +03:00
Alexey Khit 39c25215ba Update readme 2023-07-11 15:03:27 +03:00
Sergey Krashevich 490a48cd50 Refactored code to resize JPEG snapshot if "h" parameter exists in the URL query 2023-07-11 10:35:53 +03:00
52 changed files with 1482 additions and 244 deletions
+123 -48
View File
@@ -52,11 +52,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: HomeKit](#source-homekit)
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
* [Source: Nest](#source-nest)
* [Source: Roborock](#source-roborock)
* [Source: WebRTC](#source-webrtc)
* [Source: WebTorrent](#source-webtorrent)
@@ -170,6 +172,7 @@ Available source types:
- [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
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
@@ -219,6 +222,16 @@ streams:
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
**RTSP over WebSocket**
```yaml
streams:
# WebSocket with authorization, RTSP - without
axis-rtsp-ws: rtsp://192.168.1.123:4567/axis-media/media.amp?overview=0&camera=1&resolution=1280x720&videoframeskipmode=empty&Axis-Orig-Sw=true#transport=ws://user:pass@192.168.1.123:4567/rtsp-over-websocket
# WebSocket without authorization, RTSP - with
dahua-rtsp-ws: rtsp://user:pass@192.168.1.123/cam/realmonitor?channel=1&subtype=1&proto=Private3#transport=ws://192.168.1.123/rtspoverwebsocket
```
#### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
@@ -307,20 +320,25 @@ But you can override them via YAML config. You can also add your own formats to
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..."
mycodec: "-any args that supported by ffmpeg..."
myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}"
myraw: "-ss 00:00:20"
```
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use `rotate` param with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
- This will greatly increase the CPU of the server, even with hardware acceleration
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
- You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`)
- You can add your own input templates
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
**PS.** It is recommended to check the available hardware in the WebUI add page.
#### Source: FFmpeg Device
@@ -341,6 +359,8 @@ streams:
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
```
**PS.** It is recommended to check the available devices in the WebUI add page.
#### Source: Exec
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
@@ -410,6 +430,18 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
#### Source: Bubble
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
- setup separate streams for different channels and streams
```yaml
streams:
camera1: bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0
```
#### Source: DVRIP
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
@@ -467,7 +499,23 @@ streams:
aqara_g3: hass:Camera-Hub-G3-AB12
```
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
**WebRTC Cameras**
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
The Nest API only allows you to get a link to a stream for 5 minutes. So every 5 minutes the stream will be reconnected.
```yaml
streams:
# link to Home Assistant Supervised
hass-webrtc1: hass://supervisor?entity_id=camera.nest_doorbell
# link to external Hass with Long-Lived Access Tokens
hass-webrtc2: hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=eyXYZ...
```
**RTSP Cameras**
By default, the Home Assistant API does not allow you to get dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
#### Source: ISAPI
@@ -480,6 +528,17 @@ streams:
- isapi://admin:password@192.168.1.123:80/
```
#### Source: Nest
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
```yaml
streams:
nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***
```
#### Source: Roborock
This source type support Roborock vacuums with cameras. Known working models:
@@ -493,17 +552,34 @@ If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456
#### Source: WebRTC
This source type support two connection formats:
This source type support four connection formats.
- [WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
- `go2rtc/WebSocket` - This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**whep**
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
**go2rtc**
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**wyze**
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
**kinesis**
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
```yaml
streams:
webrtc1: webrtc:http://192.168.1.123:1984/api/webrtc?src=dahua1
webrtc2: webrtc:ws://192.168.1.123:1984/api/ws?src=dahua1
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
```
**PS.** For `wyze` and `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
#### Source: WebTorrent
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
@@ -582,33 +658,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
go2rtc has its own JS video player (`video-rtc.js`) with:
- support technologies:
- WebRTC over UDP or TCP
- MSE or MP4 or MJPEG over WebSocket
- automatic selection best technology according on:
- codecs inside your stream
- current browser capabilities
- current network configuration
- automatic stop stream while browser or page not active
- automatic stop stream while player not inside page viewport
- automatic reconnection
Technology selection based on priorities:
1. Video and Audio better than just Video
2. H265 better than H264
3. WebRTC better than MSE, than MP4, than MJPEG
go2rtc has simple HTML page (`stream.html`) with support params in URL:
- multiple streams on page `src=camera1&src=camera2...`
- stream technology autoselection `mode=webrtc,mse,mp4,mjpeg`
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
- player width setting in pixels `width=320px` or percents `width=50%`
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
**Module config**
@@ -625,6 +675,15 @@ api:
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
tls_listen: ":443" # default "", enable HTTPS server
tls_cert: | # default "", PEM-encoded fullchain certificate for HTTPS
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
tls_key: | # default "", PEM-encoded private key for HTTPS
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
```
**PS:**
@@ -852,9 +911,17 @@ API examples:
- 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)
- MP4 file: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM)
- You can use `mp4`, `mp4=flac` and `mp4=all` param for codec filters
- You can use `duration` param in seconds (ex. `duration=15`)
- You can use `filename` param (ex. `filename=record.mp4`)
- You can use `rotate` param with `90`, `180` or `270` values
- You can use `scale` param with positive integer values (ex. `scale=4:3`)
Read more about [codecs filters](#codecs-filters).
**PS.** Rotate and scale params don't use transcoding and change video using metadata.
### Module: HLS
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
@@ -892,6 +959,9 @@ API examples:
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
- You can use `width`/`w` and/or `height`/`h` params
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
### Module: Log
@@ -961,17 +1031,21 @@ Some examples:
`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, 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 |
| Device | WebRTC | MSE | HTTP | HLS |
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
| *latency* | best | medium | bad | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
[1]: https://apps.apple.com/app/home-assistant/id1099568401
`HTTP*` - HTTP Progressive Streaming, not related with [Progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
- 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/)
@@ -1065,6 +1139,7 @@ streams:
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
+117
View File
@@ -0,0 +1,117 @@
# API
Fill free to make any API design proposals.
## HTTP API
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
`www/stream.html` - universal viewer with support params in URL:
- multiple streams on page `src=camera1&src=camera2...`
- stream technology autoselection `mode=webrtc,webrtc/tcp,mse,hls,mp4,mjpeg`
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
- player width setting in pixels `width=320px` or percents `width=50%`
`www/webrtc.html` - WebRTC viewer with support two way audio and params in URL:
- `media=video+audio` - simple viewer
- `media=video+audio+microphone` - two way audio from camera
- `media=camera+microphone` - stream from browser
- `media=display+speaker` - stream from desktop
## JavaScript API
- You can write your viewer from the scratch
- You can extend the built-in viewer - `www/video-rtc.js`
- Check example - `www/video-stream.js`
- Check example - https://github.com/AlexxIT/WebRTC
`video-rtc.js` features:
- support technologies:
- WebRTC over UDP or TCP
- MSE or HLS or MP4 or MJPEG over WebSocket
- automatic selection best technology according on:
- codecs inside your stream
- current browser capabilities
- current network configuration
- automatic stop stream while browser or page not active
- automatic stop stream while player not inside page viewport
- automatic reconnection
Technology selection based on priorities:
1. Video and Audio better than just Video
2. H265 better than H264
3. WebRTC better than MSE, than HLS, than MJPEG
## WebSocket API
Endpoint: `/api/ws`
Query parameters:
- `src` (required) - Stream name
### WebRTC
Request SDP:
```json
{"type":"webrtc/offer","value":"v=0\r\n..."}
```
Response SDP:
```json
{"type":"webrtc/answer","value":"v=0\r\n..."}
```
Request/response candidate:
- empty value also allowed and optional
```json
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
```
### MSE
Request:
- codecs list optional
```json
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
```
Response:
```json
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
```
### HLS
Request:
```json
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
```
Response:
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
```json
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
```
### MJPEG
Request/response:
```json
{"type":"mjpeg"}
```
+2 -2
View File
@@ -49,7 +49,7 @@ func Init() {
HandleFunc("api/exit", exitHandler)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
listener, err := net.Listen("tcp4", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
@@ -169,7 +169,7 @@ func middlewareLog(next http.Handler) http.Handler {
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
if !strings.HasPrefix(r.RemoteAddr, "127.") {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"gopkg.in/yaml.v3"
)
var Version = "1.6.0"
var Version = "1.6.1"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
+19
View File
@@ -0,0 +1,19 @@
package bubble
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/bubble"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func Init() {
streams.HandleFunc("bubble", handle)
}
func handle(url string) (core.Producer, error) {
conn := bubble.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
return conn, nil
}
+12 -3
View File
@@ -1,13 +1,14 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
@@ -28,8 +29,16 @@ func queryToInput(query url.Values) string {
}
if audio := query.Get("audio"); audio != "" {
// https://trac.ffmpeg.org/wiki/Capture/ALSA
input := "-f alsa"
for key, value := range query {
switch key {
case "channels", "sample_rate":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(audios, audio)
}
@@ -79,7 +88,7 @@ func initDevices() {
if err == nil {
stream := api.Stream{
Name: "ALSA default",
URL: "ffmpeg:device?audio=default#audio=opus",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}
audios = append(audios, "default")
+1
View File
@@ -89,6 +89,7 @@ func initDevices() {
stream.URL += "#video=h264#hardware"
case core.KindAudio:
audios = append(audios, name)
stream.URL += "&channels=1&sample_rate=16000&audio_buffer_size=10"
}
streams = append(streams, stream)
+5 -2
View File
@@ -64,8 +64,9 @@ var defaults = map[string]string{
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
// https://github.com/pion/webrtc/issues/1514
// `-af adelay=0|0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -af adelay=0|0",
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 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",
@@ -78,6 +79,8 @@ var defaults = map[string]string{
"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",
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
+1 -1
View File
@@ -83,7 +83,7 @@ func TestParseArgsAudio(t *testing.T) {
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
args = parseArgs("rtsp:///example.com#audio=opus")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -af adelay=0|0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu")
-12
View File
@@ -1,12 +0,0 @@
package ffmpeg
import (
"bytes"
"os/exec"
)
func TranscodeToJPEG(b []byte) ([]byte, error) {
cmd := exec.Command(defaults["bin"], "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
+69
View File
@@ -0,0 +1,69 @@
package ffmpeg
import (
"bytes"
"fmt"
"net/url"
"os/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func TranscodeToJPEG(b []byte, query url.Values) ([]byte, error) {
ffmpegArgs := parseQuery(query)
cmdArgs := shell.QuoteSplit(ffmpegArgs.String())
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
func parseQuery(query url.Values) *ffmpeg.Args {
args := &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Input: "-i -",
Codecs: []string{defaults["mjpeg"]},
Output: defaults["output/mjpeg"],
}
var width = -1
var height = -1
var r, hw string
for k, v := range query {
switch k {
case "width", "w":
width = core.Atoi(v[0])
case "height", "h":
height = core.Atoi(v[0])
case "rotate":
r = v[0]
case "hardware", "hw":
hw = v[0]
}
}
if width > 0 || height > 0 {
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
}
if r != "" {
switch r {
case "90":
args.AddFilter("transpose=1") // 90 degrees clockwise
case "180":
args.AddFilter("transpose=1,transpose=1")
case "-90", "270":
args.AddFilter("transpose=2") // 90 degrees counterclockwise
}
}
if hw != "" {
hardware.MakeHardware(args, hw, defaults)
}
return args
}
+23
View File
@@ -0,0 +1,23 @@
package ffmpeg
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseQuery(t *testing.T) {
args := parseQuery(nil)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())
query, err := url.ParseQuery("h=480")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf "scale=-1:480" -f mjpeg -`, args.String())
query, err = url.ParseQuery("hw=vaapi")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
}
+15 -16
View File
@@ -3,12 +3,13 @@ package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"net"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
)
func apiOK(w http.ResponseWriter, r *http.Request) {
@@ -21,6 +22,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -28,45 +30,42 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
apiOK(w, r)
} else {
http.Error(w, "", http.StatusBadRequest)
}
stream.SetSource(v.Channels.First.Url)
apiOK(w, r)
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
log.Warn().Msgf("wrong request: %s", r.RequestURI)
http.Error(w, "", http.StatusBadRequest)
return
}
name := r.RequestURI[8 : 8+i]
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+7 -7
View File
@@ -1,6 +1,11 @@
package hls
import (
"net/http"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -10,10 +15,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net/http"
"strings"
"sync"
"time"
)
func Init() {
@@ -138,12 +139,11 @@ segment.ts?id=` + sid + `&n=%d`
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)
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, "fLaC", 1)
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
+6 -7
View File
@@ -2,19 +2,19 @@ package hls
import (
"errors"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"strings"
"time"
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src)
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -69,12 +69,11 @@ segment.m4s?id=` + sid + `&n=%d`
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)
codecs = strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, "fLaC", 1)
// bandwidth important for Safari, codecs useful for smooth playback
data := `#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid
tr.Write(&ws.Message{Type: "hls", Value: data})
+7 -7
View File
@@ -2,6 +2,11 @@ package mjpeg
import (
"errors"
"io"
"net/http"
"strconv"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
@@ -11,10 +16,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"io"
"net/http"
"strconv"
"time"
)
func Init() {
@@ -60,7 +61,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if data, err = ffmpeg.TranscodeToJPEG(data); err != nil {
if data, err = ffmpeg.TranscodeToJPEG(data, r.URL.Query()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -158,8 +159,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
}
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src)
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+12 -1
View File
@@ -1,15 +1,16 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/internal/api/ws"
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
@@ -153,6 +154,16 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if rotate := query.Get("rotate"); rotate != "" {
mp4.PatchVideoRotate(data, core.Atoi(rotate))
}
if scale := query.Get("scale"); scale != "" {
if sx, sy, ok := strings.Cut(scale, ":"); ok {
mp4.PatchVideoScale(data, core.Atoi(sx), core.Atoi(sy))
}
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
+3 -4
View File
@@ -2,6 +2,7 @@ package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -10,8 +11,7 @@ import (
)
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src)
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -58,8 +58,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
}
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src)
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+1 -1
View File
@@ -45,7 +45,7 @@ func Init() {
return
}
ln, err := net.Listen("tcp", address)
ln, err := net.Listen("tcp4", address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
+20 -8
View File
@@ -3,10 +3,11 @@ package streams
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type state byte
@@ -35,6 +36,24 @@ type Producer struct {
workerID int
}
const SourceTemplate = "{input}"
func NewProducer(source string) *Producer {
if strings.Contains(source, SourceTemplate) {
return &Producer{template: source}
}
return &Producer{url: source}
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.url = s
} else {
p.url = strings.Replace(p.template, SourceTemplate, s, 1)
}
}
func (p *Producer) Dial() error {
p.mu.Lock()
defer p.mu.Unlock()
@@ -112,13 +131,6 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
return nil
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.template = p.url
}
p.url = strings.Replace(p.template, "{input}", s, 1)
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.conn != nil {
return json.Marshal(p.conn)
+6 -7
View File
@@ -3,10 +3,11 @@ package streams
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Stream struct {
@@ -19,15 +20,13 @@ type Stream struct {
func NewStream(source any) *Stream {
switch source := source.(type) {
case string:
s := new(Stream)
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
return s
return &Stream{
producers: []*Producer{NewProducer(source)},
}
case []any:
s := new(Stream)
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
s.producers = append(s.producers, NewProducer(source.(string)))
}
return s
case map[string]any:
+26 -7
View File
@@ -1,19 +1,38 @@
package streams
import (
"github.com/stretchr/testify/require"
"net/url"
"testing"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/stretchr/testify/require"
)
func TestTemplate(t *testing.T) {
source1 := "does not matter"
stream1 := New("from_yaml", source1)
func TestRecursion(t *testing.T) {
// create stream with some source
stream1 := New("from_yaml", "does not matter")
require.Len(t, streams, 1)
stream2 := NewTemplate("camera.from_hass", "rtsp://localhost:8554/from_yaml?video")
// ask another unnamed stream that links go2rtc
query, err := url.ParseQuery("src=rtsp://localhost:8554/from_yaml?video")
require.Nil(t, err)
stream2 := GetOrPatch(query)
// check stream is same
require.Equal(t, stream1, stream2)
require.Equal(t, stream2.producers[0].url, source1)
// check stream urls is same
require.Equal(t, stream1.producers[0].url, stream2.producers[0].url)
require.Len(t, streams, 2)
}
func TestTempate(t *testing.T) {
HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil }) // bypass HasProducer
// config from yaml
stream1 := New("camera.from_hass", "ffmpeg:{input}#video=copy")
// request from hass
stream2 := Patch("camera.from_hass", "rtsp://example.com")
require.Equal(t, stream1, stream2)
require.Equal(t, "ffmpeg:rtsp://example.com#video=copy", stream1.producers[0].url)
}
@@ -1,12 +1,15 @@
package streams
import (
"net/http"
"net/url"
"regexp"
"sync"
"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() {
@@ -33,24 +36,69 @@ func Get(name string) *Stream {
return streams[name]
}
func New(name string, source any) *Stream {
var sanitize = regexp.MustCompile(`\s`)
func New(name string, source string) *Stream {
// not allow creating dynamic streams with spaces in the source
if sanitize.MatchString(source) {
return nil
}
stream := NewStream(source)
streams[name] = stream
return stream
}
func NewTemplate(name string, source any) *Stream {
func Patch(name string, source string) *Stream {
streamsMu.Lock()
defer streamsMu.Unlock()
// 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" && len(u.Path) > 1 {
if stream, ok := streams[u.Path[1:]]; ok {
streams[name] = stream
return stream
}
if u, err := url.Parse(source); err == nil && u.Scheme == "rtsp" && len(u.Path) > 1 {
rtspName := u.Path[1:]
if stream, ok := streams[rtspName]; ok {
// link (alias) stream[name] to stream[rtspName]
streams[name] = stream
return stream
}
}
return New(name, "{input}")
// check if src has supported scheme
if !HasProducer(source) {
return nil
}
// check an existing stream with this name
if stream, ok := streams[name]; ok {
stream.SetSource(source)
return stream
}
// create new stream with this name
return New(name, source)
}
func GetOrPatch(query url.Values) *Stream {
// check if src param exists
source := query.Get("src")
if source == "" {
return nil
}
// check if src is stream name
if stream, ok := streams[source]; ok {
return stream
}
// check if name param provided
if name := query.Get("name"); name == "" {
log.Info().Msgf("[streams] create new stream url=%s", source)
return Patch(name, source)
}
// return new stream with src as name
return Patch(source, source)
}
func GetAll() (names []string) {
@@ -81,7 +129,9 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
name = src
}
New(name, src)
if New(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
}
case "PATCH":
name := query.Get("name")
@@ -91,11 +141,9 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
stream := Get(name)
if stream == nil {
stream = NewTemplate(name, src)
if Patch(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
}
stream.SetSource(src)
case "POST":
// with dst - redirect source to dst
@@ -120,3 +168,4 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
var log zerolog.Logger
var streams = map[string]*Stream{}
var streamsMu sync.Mutex
+274 -34
View File
@@ -1,44 +1,74 @@
package webrtc
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
"io"
"net/http"
"strings"
"time"
)
func streamsHandler(url string) (core.Producer, error) {
url = url[7:]
if i := strings.Index(url, "://"); i > 0 {
switch url[:i] {
// streamsHandler supports:
// 1. WHEP: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
// 2. go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
// 3. Wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
// 4. Kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
func streamsHandler(rawURL string) (core.Producer, error) {
var query url.Values
if i := strings.IndexByte(rawURL, '#'); i > 0 {
query = streams.ParseQuery(rawURL[i+1:])
rawURL = rawURL[:i]
}
rawURL = rawURL[7:] // remove webrtc:
if i := strings.IndexByte(rawURL, ':'); i > 0 {
scheme := rawURL[:i]
format := query.Get("format")
switch scheme {
case "ws", "wss":
return asyncClient(url)
if format == "kinesis" {
// https://aws.amazon.com/kinesis/video-streams/
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
} else {
return go2rtcClient(rawURL)
}
case "http", "https":
return syncClient(url)
if format == "wyze" {
// https://github.com/mrlt8/docker-wyze-bridge
return wyzeClient(rawURL)
} else {
return whepClient(rawURL)
}
}
}
return nil, errors.New("unsupported url: " + url)
return nil, errors.New("unsupported url: " + rawURL)
}
// asyncClient can connect only to go2rtc server
// go2rtcClient can connect only to go2rtc server
// ex: ws://localhost:1984/api/ws?src=camera1
func asyncClient(url string) (core.Producer, error) {
func go2rtcClient(url string) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
_ = conn.Close()
}
}()
// close websocket when we ready return Producer or connection error
defer conn.Close()
// 2. Create PeerConnection
pc, err := PeerConnection(true)
@@ -47,22 +77,27 @@ func asyncClient(url string) (core.Producer, error) {
return nil, err
}
var sendOffer core.Waiter
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WebSocket async"
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
_ = conn.Close()
case *pion.ICECandidate:
sendOffer.Wait()
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
case pion.PeerConnectionState:
switch msg {
case pion.PeerConnectionStateConnecting:
case pion.PeerConnectionStateConnected:
connState.Done(nil)
default:
connState.Done(errors.New("webrtc: " + msg.String()))
}
}
})
@@ -84,8 +119,6 @@ func asyncClient(url string) (core.Producer, error) {
return nil, err
}
sendOffer.Done()
// 5. Get answer
if err = conn.ReadJSON(msg); err != nil {
return nil, err
@@ -102,13 +135,12 @@ func asyncClient(url string) (core.Producer, error) {
// 6. Continue to receiving candidates
go func() {
var err error
for {
// receive data from remote
msg := new(ws.Message)
if err = conn.ReadJSON(msg); err != nil {
if cerr, ok := err.(*websocket.CloseError); ok {
log.Trace().Err(err).Caller().Msgf("[webrtc] ws code=%d", cerr.Code)
}
var msg ws.Message
if err = conn.ReadJSON(&msg); err != nil {
break
}
@@ -120,15 +152,19 @@ func asyncClient(url string) (core.Producer, error) {
}
}
_ = conn.Close()
connState.Done(err)
}()
if err = connState.Wait(); err != nil {
return nil, err
}
return prod, nil
}
// syncClient - support WebRTC-HTTP Egress Protocol (WHEP)
// whepClient - support WebRTC-HTTP Egress Protocol (WHEP)
// ex: http://localhost:1984/api/webrtc?src=camera1
func syncClient(url string) (core.Producer, error) {
func whepClient(url string) (core.Producer, error) {
// 2. Create PeerConnection
pc, err := PeerConnection(true)
if err != nil {
@@ -176,3 +212,207 @@ func syncClient(url string) (core.Producer, error) {
return prod, nil
}
type KinesisRequest struct {
Action string `json:"action"`
ClientID string `json:"recipientClientId"`
Payload []byte `json:"messagePayload"`
}
func (k KinesisRequest) String() string {
return fmt.Sprintf("action=%s, payload=%s", k.Action, k.Payload)
}
type KinesisResponse struct {
Payload []byte `json:"messagePayload"`
Type string `json:"messageType"`
}
func (k KinesisResponse) String() string {
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
}
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
if err != nil {
return nil, err
}
// 2. Load ICEServers from query param (base64 json)
conf := pion.Configuration{}
if s := query.Get("ice_servers"); s != "" {
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
if err != nil {
log.Warn().Err(err).Caller().Send()
}
}
// close websocket when we ready return Producer or connection error
defer conn.Close()
// 3. Create Peer Connection
api, err := webrtc.NewAPI("")
if err != nil {
return nil, err
}
pc, err := api.NewPeerConnection(conf)
if err != nil {
return nil, err
}
// protect from sending ICE candidate before Offer
var sendOffer core.Waiter
// protect from blocking on errors
defer sendOffer.Done(nil)
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
req := KinesisRequest{
ClientID: query.Get("client_id"),
}
prod := webrtc.NewConn(pc)
prod.Desc = desc
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
_ = sendOffer.Wait()
req.Action = "ICE_CANDIDATE"
req.Payload, _ = json.Marshal(msg.ToJSON())
if err = conn.WriteJSON(&req); err != nil {
connState.Done(err)
return
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
case pion.PeerConnectionState:
switch msg {
case pion.PeerConnectionStateConnecting:
case pion.PeerConnectionStateConnected:
connState.Done(nil)
default:
connState.Done(errors.New("webrtc: " + msg.String()))
}
}
})
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// 4. Create offer
offer, err := prod.CreateOffer(medias)
if err != nil {
return nil, err
}
// 5. Send offer
req.Action = "SDP_OFFER"
req.Payload, _ = json.Marshal(pion.SessionDescription{
Type: pion.SDPTypeOffer,
SDP: offer,
})
if err = conn.WriteJSON(req); err != nil {
return nil, err
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
sendOffer.Done(nil)
go func() {
var err error
// will be closed when conn will be closed
for {
var res KinesisResponse
if err = conn.ReadJSON(&res); err != nil {
// some buggy messages from Amazon servers
if errors.Is(err, io.ErrUnexpectedEOF) {
continue
}
break
}
log.Trace().Msgf("[webrtc] kinesis recv: %s", res)
switch res.Type {
case "SDP_ANSWER":
// 6. Get answer
var sd pion.SessionDescription
if err = json.Unmarshal(res.Payload, &sd); err != nil {
break
}
if err = prod.SetAnswer(sd.SDP); err != nil {
break
}
case "ICE_CANDIDATE":
// 7. Continue to receiving candidates
var ci pion.ICECandidateInit
if err = json.Unmarshal(res.Payload, &ci); err != nil {
break
}
if err = prod.AddCandidate(ci.Candidate); err != nil {
break
}
}
}
connState.Done(err)
}()
if err = connState.Wait(); err != nil {
return nil, err
}
return prod, nil
}
type WyzeKVS struct {
ClientId string `json:"ClientId"`
Cam string `json:"cam"`
Result string `json:"result"`
Servers json.RawMessage `json:"servers"`
URL string `json:"signalingUrl"`
}
func wyzeClient(rawURL string) (core.Producer, error) {
client := http.Client{Timeout: 5 * time.Second}
res, err := client.Get(rawURL)
if err != nil {
return nil, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var kvs WyzeKVS
if err = json.Unmarshal(b, &kvs); err != nil {
return nil, err
}
if kvs.Result != "ok" {
return nil, errors.New("wyse: wrong result: " + kvs.Result)
}
query := url.Values{
"client_id": []string{kvs.ClientId},
"ice_servers": []string{string(kvs.Servers)},
}
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
}
+8 -5
View File
@@ -2,15 +2,17 @@ package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
)
const MimeSDP = "application/sdp"
@@ -140,7 +142,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
stream = streams.New(dst, nil)
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
// 1. Get offer
@@ -2,6 +2,8 @@ package webrtc
import (
"errors"
"net"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -10,7 +12,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"net"
)
func Init() {
@@ -91,7 +92,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
query := tr.Request.URL.Query()
if name := query.Get("src"); name != "" {
stream = streams.Get(name)
stream = streams.GetOrPatch(query)
mode = core.ModePassiveConsumer
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
} else if name = query.Get("dst"); name != "" {
@@ -113,6 +114,9 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
var sendAnswer core.Waiter
// protect from blocking on errors
defer sendAnswer.Done(nil)
conn := webrtc.NewConn(pc)
conn.Desc = "WebRTC/WebSocket async"
conn.Mode = mode
@@ -131,7 +135,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
}
case *pion.ICECandidate:
sendAnswer.Wait()
_ = sendAnswer.Wait()
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
@@ -185,7 +189,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
tr.Write(&ws.Message{Type: "webrtc/answer", Value: answer})
}
sendAnswer.Done()
sendAnswer.Done(nil)
asyncCandidates(tr, conn)
+2
View File
@@ -4,6 +4,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/bubble"
"github.com/AlexxIT/go2rtc/internal/debug"
"github.com/AlexxIT/go2rtc/internal/dvrip"
"github.com/AlexxIT/go2rtc/internal/echo"
@@ -74,6 +75,7 @@ func main() {
roborock.Init() // roborock source
homekit.Init() // homekit source
nest.Init() // nest source
bubble.Init() // bubble source
// 6. Helper modules
+260
View File
@@ -0,0 +1,260 @@
// Package bubble, because:
// Request URL: /bubble/live?ch=0&stream=0
// Response Conten-Type: video/bubble
// https://github.com/Lynch234ok/lynch-git/blob/master/app_rebulid/src/bubble.c
package bubble
import (
"bufio"
"encoding/binary"
"errors"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtp"
)
type Client struct {
core.Listener
url string
conn net.Conn
videoCodec string
channel int
stream int
r *bufio.Reader
medias []*core.Media
receivers []*core.Receiver
videoTrack *core.Receiver
audioTrack *core.Receiver
recv int
}
func NewClient(url string) *Client {
return &Client{url: url}
}
const (
SyncByte = 0xAA
PacketAuth = 0x00
PacketMedia = 0x01
PacketStart = 0x0A
)
const Timeout = time.Second * 5
func (c *Client) Dial() (err error) {
u, err := url.Parse(c.url)
if err != nil {
return
}
if c.conn, err = net.DialTimeout("tcp4", u.Host, Timeout); err != nil {
return
}
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
return
}
req := &tcp.Request{Method: "GET", URL: &url.URL{Path: u.Path, RawQuery: u.RawQuery}, Proto: "HTTP/1.1"}
if err = req.Write(c.conn); err != nil {
return
}
c.r = bufio.NewReader(c.conn)
res, err := tcp.ReadResponse(c.r)
if err != nil {
return
}
if res.StatusCode != http.StatusOK {
return errors.New("wrong response: " + res.Status)
}
// 1. Read 1024 bytes with XML, some cameras returns exact 1024, but some - 923
xml := make([]byte, 1024)
if _, err = c.r.Read(xml); err != nil {
return
}
// 2. Write size uint32 + unknown 4b + user 20b + pass 20b
b := make([]byte, 48)
binary.BigEndian.PutUint32(b, 44)
if u.User != nil {
copy(b[8:], u.User.Username())
pass, _ := u.User.Password()
copy(b[28:], pass)
} else {
copy(b[8:], "admin")
}
if err = c.Write(PacketAuth, 0x0E16C271, b); err != nil {
return
}
// 3. Read response
cmd, b, err := c.Read()
if err != nil {
return
}
if cmd != PacketAuth || len(b) != 44 || b[4] != 3 || b[8] != 1 {
return errors.New("wrong auth response")
}
// 4. Parse XML (from 1)
query := u.Query()
stream := query.Get("stream")
if stream != "" {
c.stream = core.Atoi(stream)
} else {
stream = "0"
}
// <bubble version="1.0" vin="1"><vin0 stream="2">
// <stream0 name="720p.264" size="2304x1296" x1="yes" x2="yes" x4="yes" />
// <stream1 name="360p.265" size="640x360" x1="yes" x2="yes" x4="yes" />
// <vin0>
// </bubble>
re := regexp.MustCompile("<stream " + stream + `[^>]+`)
stream = re.FindString(string(xml))
if strings.Contains(stream, ".265") {
c.videoCodec = core.CodecH265
} else {
c.videoCodec = core.CodecH264
}
if ch := query.Get("ch"); ch != "" {
c.channel = core.Atoi(ch)
}
return
}
func (c *Client) Write(command byte, timestamp uint32, payload []byte) error {
if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {
return err
}
// 0xAA + size uint32 + cmd byte + ts uint32 + payload
b := make([]byte, 14+len(payload))
b[0] = SyncByte
binary.BigEndian.PutUint32(b[1:], uint32(5+len(payload)))
b[5] = command
binary.BigEndian.PutUint32(b[6:], timestamp)
copy(b[10:], payload)
_, err := c.conn.Write(b)
return err
}
func (c *Client) Read() (byte, []byte, error) {
if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {
return 0, nil, err
}
// 0xAA + size uint32 + cmd byte + ts uint32 + payload
b := make([]byte, 10)
if _, err := io.ReadFull(c.r, b); err != nil {
return 0, nil, err
}
if b[0] != SyncByte {
return 0, nil, errors.New("wrong start byte")
}
size := binary.BigEndian.Uint32(b[1:])
payload := make([]byte, size-1-4)
if _, err := io.ReadFull(c.r, payload); err != nil {
return 0, nil, err
}
//timestamp := binary.BigEndian.Uint32(b[6:]) // in ms
return b[5], payload, nil
}
func (c *Client) Play() error {
// yeah, there's no mistake about the little endian
b := make([]byte, 16)
binary.LittleEndian.PutUint32(b, uint32(c.channel))
binary.LittleEndian.PutUint32(b[4:], uint32(c.stream))
binary.LittleEndian.PutUint32(b[8:], 1) // opened
return c.Write(PacketStart, 0x0E16C2DF, b)
}
func (c *Client) Handle() error {
var audioTS uint32
for {
cmd, b, err := c.Read()
if err != nil {
return err
}
c.recv += len(b)
if cmd != PacketMedia {
continue
}
// size uint32 + type 1b + channel 1b
// type = 1 for keyframe, 2 for other frame, 0 for audio
if b[4] > 0 {
if c.videoTrack == nil {
continue
}
pkt := &rtp.Packet{
Header: rtp.Header{
Timestamp: core.Now90000(),
},
Payload: h264.AnnexB2AVC(b[6:]),
}
c.videoTrack.WriteRTP(pkt)
} else {
if c.audioTrack == nil {
continue
}
//binary.LittleEndian.Uint32(b[6:]) // entries (always 1)
//size := binary.LittleEndian.Uint32(b[10:]) // size
//mk := binary.LittleEndian.Uint64(b[14:]) // pts (uint64_t)
//binary.LittleEndian.Uint32(b[22:]) // gtime (time_t)
//name := b[26:34] // g711
//rate := binary.LittleEndian.Uint32(b[34:]) // sample rate
//width := binary.LittleEndian.Uint32(b[38:]) // samplewidth
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Timestamp: audioTS,
},
Payload: b[6+36:],
}
audioTS += uint32(len(pkt.Payload))
c.audioTrack.WriteRTP(pkt)
}
}
}
func (c *Client) Close() error {
return c.conn.Close()
}
+75
View File
@@ -0,0 +1,75 @@
package bubble
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Client) GetMedias() []*core.Media {
if c.medias == nil {
c.medias = []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: c.videoCodec, ClockRate: 90000, PayloadType: core.PayloadTypeRAW},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
},
},
}
}
return c.medias
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track, nil
}
}
track := core.NewReceiver(media, codec)
switch media.Kind {
case core.KindVideo:
c.videoTrack = track
case core.KindAudio:
c.audioTrack = track
}
c.receivers = append(c.receivers, track)
return track, nil
}
func (c *Client) Start() error {
if err := c.Play(); err != nil {
return err
}
return c.Handle()
}
func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "Bubble active producer",
Medias: c.medias,
Recv: c.recv,
Receivers: c.receivers,
}
return json.Marshal(info)
}
+40
View File
@@ -0,0 +1,40 @@
## PCM
**RTSP**
- PayloadType=10 - L16/44100/2 - Linear PCM 16-bit big endian
- PayloadType=11 - L16/44100/1 - Linear PCM 16-bit big endian
https://en.wikipedia.org/wiki/RTP_payload_formats
**Apple QuickTime**
- `raw` - 16-bit data is stored in little endian format
- `twos` - 16-bit data is stored in big endian format
- `sowt` - 16-bit data is stored in little endian format
- `in24` - denotes 24-bit, big endian
- `in32` - denotes 32-bit, big endian
- `fl32` - denotes 32-bit floating point PCM
- `fl64` - denotes 64-bit floating point PCM
- `alaw` - denotes A-law logarithmic PCM
- `ulaw` - denotes mu-law logarithmic PCM
https://wiki.multimedia.cx/index.php/PCM
**FFmpeg RTSP**
```
pcm_s16be, 44100 Hz, stereo => 10
pcm_s16be, 48000 Hz, stereo => 96 L16/48000/2
pcm_s16be, 44100 Hz, mono => 11
pcm_s16le, 48000 Hz, stereo => 96 (b=AS:1536)
pcm_s16le, 44100 Hz, stereo => 96 (b=AS:1411)
pcm_s16le, 16000 Hz, stereo => 96 (b=AS:512)
pcm_s16le, 8000 Hz, stereo => 96 (b=AS:256)
pcm_s16le, 48000 Hz, mono => 96 (b=AS:768)
pcm_s16le, 44100 Hz, mono => 96 (b=AS:705)
pcm_s16le, 16000 Hz, mono => 96 (b=AS:256)
pcm_s16le, 8000 Hz, mono => 96 (b=AS:128)
```
+38 -1
View File
@@ -3,10 +3,11 @@ package core
import (
"encoding/base64"
"fmt"
"github.com/pion/sdp/v3"
"strconv"
"strings"
"unicode"
"github.com/pion/sdp/v3"
)
type Codec struct {
@@ -112,6 +113,42 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
case "96", "97", "98":
if len(md.Bandwidth) == 0 {
c.Name = payloadType
break
}
// FFmpeg + RTSP + pcm_s16le = doesn't pass info about codec name and params
// so try to guess the codec based on bitrate
// https://github.com/AlexxIT/go2rtc/issues/523
switch md.Bandwidth[0].Bandwidth {
case 128:
c.ClockRate = 8000
case 256:
c.ClockRate = 16000
case 384:
c.ClockRate = 24000
case 512:
c.ClockRate = 32000
case 705:
c.ClockRate = 44100
case 768:
c.ClockRate = 48000
case 1411:
// default Windows DShow
c.ClockRate = 44100
c.Channels = 2
case 1536:
// default Linux ALSA
c.ClockRate = 48000
c.Channels = 2
default:
c.Name = payloadType
break
}
c.Name = CodecPCML
default:
c.Name = payloadType
}
+3 -1
View File
@@ -25,7 +25,9 @@ const (
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
CodecPCM = "L16" // Linear PCM
CodecPCM = "L16" // Linear PCM (big endian)
CodecPCML = "PCML" // Linear PCM (little endian)
CodecELD = "ELD" // AAC-ELD
CodecFLAC = "FLAC"
+3 -2
View File
@@ -3,8 +3,9 @@ package core
import (
"encoding/json"
"fmt"
"github.com/pion/sdp/v3"
"strings"
"github.com/pion/sdp/v3"
)
// Media take best from:
@@ -93,7 +94,7 @@ func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC:
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
return KindAudio
}
return ""
+4 -3
View File
@@ -2,11 +2,12 @@ package core
import (
"fmt"
"net/url"
"testing"
"github.com/pion/sdp/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/url"
"testing"
)
func TestSDP(t *testing.T) {
@@ -38,7 +39,7 @@ func TestParseQuery(t *testing.T) {
u, _ = url.Parse(rawULR)
medias = ParseQuery(u.Query())
assert.Equal(t, []*Media{
{Kind: KindVideo, Direction: DirectionRecvonly, Codecs: []*Codec{{Name: CodecAny}}},
{Kind: KindVideo, Direction: DirectionSendonly, Codecs: []*Codec{{Name: CodecAny}}},
}, medias)
}
}
+10 -7
View File
@@ -12,6 +12,7 @@ type Waiter struct {
sync.WaitGroup
mu sync.Mutex
state int // state < 0 means finish
err error
}
func (w *Waiter) Add(delta int) {
@@ -23,7 +24,7 @@ func (w *Waiter) Add(delta int) {
w.mu.Unlock()
}
func (w *Waiter) Wait() {
func (w *Waiter) Wait() error {
w.mu.Lock()
// first wait auto start waiter
if w.state == 0 {
@@ -33,9 +34,11 @@ func (w *Waiter) Wait() {
w.mu.Unlock()
w.WaitGroup.Wait()
return w.err
}
func (w *Waiter) Done() {
func (w *Waiter) Done(err error) {
w.mu.Lock()
// safe run Done only when have tasks
@@ -47,21 +50,21 @@ func (w *Waiter) Done() {
// block waiter for any operations after last done
if w.state == 0 {
w.state = -1
w.err = err
}
w.mu.Unlock()
}
func (w *Waiter) WaitChan() <-chan struct{} {
var ch chan struct{}
func (w *Waiter) WaitChan() <-chan error {
var ch chan error
w.mu.Lock()
if w.state >= 0 {
ch = make(chan struct{})
ch = make(chan error)
go func() {
w.Wait()
ch <- struct{}{}
ch <- w.Wait()
}()
}
+1 -1
View File
@@ -44,7 +44,7 @@ func NewServer(name string) *Server {
func (s *Server) Serve(address string) (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", address); err != nil {
if ln, err = net.Listen("tcp4", address); err != nil {
return
}
+5
View File
@@ -41,6 +41,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
m.Write(conf)
m.EndAtom() // AVCC
m.StartAtom("pasp") // Pixel Aspect Ratio
m.WriteUint32(1) // hSpacing
m.WriteUint32(1) // vSpacing
m.EndAtom()
m.EndAtom() // AVC1
}
+2 -1
View File
@@ -32,7 +32,8 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver,
func (c *Client) Start() error {
ct := c.res.Header.Get("Content-Type")
if ct == "image/jpeg" {
// https://github.com/AlexxIT/go2rtc/issues/278
if strings.HasPrefix(ct, "image/jpeg") {
return c.startJPEG()
}
+3 -2
View File
@@ -2,13 +2,14 @@ package mp4
import (
"encoding/json"
"sync"
"github.com/AlexxIT/go2rtc/pkg/aac"
"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 {
@@ -131,7 +132,7 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
handler.Handler = aac.RTPDepay(handler.Handler)
}
case core.CodecOpus, core.CodecMP3: // no changes
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:
handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler)
codec.Name = core.CodecFLAC
+67 -6
View File
@@ -1,8 +1,11 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"bytes"
"encoding/binary"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// ParseQuery - like usual parse, but with mp4 param handler
@@ -34,6 +37,7 @@ func ParseQuery(query map[string][]string) []*core.Media {
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
&core.Codec{Name: core.CodecPCML},
)
if v[0] == "flac" {
@@ -55,7 +59,6 @@ func ParseCodecs(codecs string, parseAudio bool) (medias []*core.Media) {
var videos []*core.Codec
var audios []*core.Codec
loop:
for _, name := range strings.Split(codecs, ",") {
switch name {
case MimeH264:
@@ -67,15 +70,12 @@ loop:
case MimeAAC:
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case "null":
// this means that the browser is lying about the codecs it can play
// and we are not supposed to believe that it can flac or opus
break loop
case MimeFlac:
audios = append(audios,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
&core.Codec{Name: core.CodecPCML},
)
case MimeOpus:
codec := &core.Codec{Name: core.CodecOpus}
@@ -104,6 +104,67 @@ loop:
return
}
// PatchVideoRotate - update video track transformation matrix.
// Rotation supported by many players and browsers (except Safari).
// Scale has low support and better not to use it.
// Supported only 0, 90, 180, 270 degrees.
func PatchVideoRotate(init []byte, degrees int) bool {
// search video atom
i := bytes.Index(init, []byte("vide"))
if i < 0 {
return false
}
// seek to video matrix position
i -= 4 + 3 + 1 + 8 + 32 + 8 + 4 + 4 + 4*9
// Rotation matrix:
// [ cos sin 0]
// [ -sin cos 0]
// [ 0 0 16384]
var cos, sin uint16
switch degrees {
case 0:
cos = 1
sin = 0
case 90:
cos = 0
sin = 1
case 180:
cos = 0xFFFF // -1
sin = 0
case 270:
cos = 0
sin = 0xFFFF // -1
default:
return false
}
binary.BigEndian.PutUint16(init[i:], cos)
binary.BigEndian.PutUint16(init[i+4:], sin)
binary.BigEndian.PutUint16(init[i+12:], -sin)
binary.BigEndian.PutUint16(init[i+16:], cos)
return true
}
// PatchVideoScale - update "Pixel Aspect Ratio" atom.
// Supported by many players and browsers (except Firefox).
// Supported only positive integers.
func PatchVideoScale(init []byte, scaleX, scaleY int) bool {
// search video atom
i := bytes.Index(init, []byte("pasp"))
if i < 0 {
return false
}
binary.BigEndian.PutUint32(init[i+4:], uint32(scaleX))
binary.BigEndian.PutUint32(init[i+8:], uint32(scaleY))
return true
}
const (
stateNone byte = iota
stateInit
+3 -2
View File
@@ -2,6 +2,7 @@ package mp4
import (
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
@@ -64,7 +65,7 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
switch codec.Name {
case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
// some dummy SPS and PPS not a problem
// some dummy SPS and PPS not a problem for MP4, but problem for HLS :(
if len(sps) == 0 {
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
}
@@ -118,7 +119,7 @@ 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.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC:
case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecFLAC:
mv.WriteAudioTrack(
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
)
+10 -1
View File
@@ -4,6 +4,8 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"time"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
@@ -12,7 +14,6 @@ import (
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/ts"
"github.com/pion/rtp"
"time"
)
type Consumer struct {
@@ -60,6 +61,14 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
switch track.Codec.Name {
case core.CodecH264:
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
// 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}
}
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
+11 -2
View File
@@ -6,11 +6,12 @@ package pcm
import (
"encoding/binary"
"unicode/utf8"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"github.com/sigurn/crc16"
"github.com/sigurn/crc8"
"unicode/utf8"
)
func FLACHeader(magic bool, sampleRate uint32) []byte {
@@ -86,7 +87,7 @@ func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
return func(packet *rtp.Packet) {
samples := uint16(len(packet.Payload))
if codec.Name == core.CodecPCM {
if codec.Name == core.CodecPCM || codec.Name == core.CodecPCML {
samples /= 2
}
@@ -131,6 +132,14 @@ func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
}
case core.CodecPCM:
n += copy(buf[n:], packet.Payload)
case core.CodecPCML:
// reverse endian from little to big
size := len(packet.Payload)
for i := 0; i < size; i += 2 {
buf[n] = packet.Payload[i+1]
buf[n+1] = packet.Payload[i]
n += 2
}
}
// 4. Frame footer
+33 -5
View File
@@ -1,12 +1,14 @@
package pcm
import (
"sync"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"sync"
)
func Resample(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc {
// ResampleToG711 - convert PCMA/PCM/PCML to PCMA and PCMU to PCMU with decreasing sample rate
func ResampleToG711(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc {
n := float32(codec.ClockRate) / float32(sampleRate)
switch codec.Name {
@@ -14,16 +16,24 @@ func Resample(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) co
return DownsampleByte(PCMAtoPCM, PCMtoPCMA, n, handler)
case core.CodecPCMU:
return DownsampleByte(PCMUtoPCM, PCMtoPCMU, n, handler)
case core.CodecPCM:
case core.CodecPCM, core.CodecPCML:
if n == 1 {
return ResamplePCM(PCMtoPCMA, handler)
handler = ResamplePCM(PCMtoPCMA, handler)
} else {
handler = DownsamplePCM(PCMtoPCMA, n, handler)
}
return DownsamplePCM(PCMtoPCMA, n, handler)
if codec.Name == core.CodecPCML {
return LittleToBig(handler)
}
return handler
}
panic(core.Caller())
}
// DownsampleByte - convert PCMA/PCMU to PCMA/PCMU with decreasing sample rate (N times)
func DownsampleByte(
toPCM func(byte) int16, fromPCM func(int16) byte, n float32, handler core.HandlerFunc,
) core.HandlerFunc {
@@ -58,6 +68,23 @@ func DownsampleByte(
}
}
// LittleToBig - conver PCM little endian to PCM big endian
func LittleToBig(handler core.HandlerFunc) core.HandlerFunc {
return func(packet *rtp.Packet) {
size := len(packet.Payload)
b := make([]byte, size)
for i := 0; i < size; i += 2 {
b[i] = packet.Payload[i+1]
b[i+1] = packet.Payload[i]
}
clone := *packet
clone.Payload = b
handler(&clone)
}
}
// ResamplePCM - convert PCM to PCMA/PCMU with same sample rate
func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.HandlerFunc {
var ts uint32
@@ -84,6 +111,7 @@ func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.Handle
}
}
// DownsamplePCM - convert PCM to PCMA/PCMU with decreasing sample rate (N times)
func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc) core.HandlerFunc {
var sampleN, sampleSum float32
var ts uint32
+2 -2
View File
@@ -11,7 +11,7 @@ import (
func TestTimeout(t *testing.T) {
Timeout = time.Millisecond
ln, err := net.Listen("tcp", "localhost:0")
ln, err := net.Listen("tcp4", "localhost:0")
require.Nil(t, err)
client := NewClient("rtsp://" + ln.Addr().String() + "/stream")
@@ -27,7 +27,7 @@ func TestTimeout(t *testing.T) {
func TestMissedControl(t *testing.T) {
Timeout = time.Millisecond
ln, err := net.Listen("tcp", "localhost:0")
ln, err := net.Listen("tcp4", "localhost:0")
require.Nil(t, err)
go func() {
+2 -2
View File
@@ -50,13 +50,13 @@ func NewAPI(address string) (*webrtc.API, error) {
if address != "" {
address, network, _ := strings.Cut(address, "/")
if network == "" || network == "udp" {
if ln, err := net.ListenPacket("udp", address); err == nil {
if ln, err := net.ListenPacket("udp4", address); err == nil {
udpMux := webrtc.NewICEUDPMux(nil, ln)
s.SetICEUDPMux(udpMux)
}
}
if network == "" || network == "tcp" {
if ln, err := net.Listen("tcp", address); err == nil {
if ln, err := net.Listen("tcp4", address); err == nil {
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
}
+9 -1
View File
@@ -1,11 +1,12 @@
package webrtc
import (
"testing"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestClient(t *testing.T) {
@@ -100,3 +101,10 @@ a=recvonly
err = sender.ReplaceTrack(track)
require.Nil(t, err)
}
func TestUnmarshalICEServers(t *testing.T) {
s := `[{"credential":"xxx","urls":"xxx","username":"xxx"},{"credential":null,"urls":"xxx","username":null}]`
servers, err := UnmarshalICEServers([]byte(s))
require.Nil(t, err)
require.Len(t, servers, 2)
}
+3 -2
View File
@@ -1,11 +1,12 @@
package webrtc
import (
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
"time"
)
type Conn struct {
@@ -130,7 +131,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
}
func (c *Conn) Close() error {
c.closed.Done()
c.closed.Done(nil)
return c.pc.Close()
}
+4 -3
View File
@@ -3,6 +3,7 @@ package webrtc
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
@@ -63,13 +64,13 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
}
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:
if codec.ClockRate == 0 {
if codec.Name == core.CodecPCM {
if codec.Name == core.CodecPCM || codec.Name == core.CodecPCML {
codec.Name = core.CodecPCMA
}
codec.ClockRate = 8000
sender.Handler = pcm.Resample(track.Codec, 8000, sender.Handler)
sender.Handler = pcm.ResampleToG711(track.Codec, 8000, sender.Handler)
}
// Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500
+49 -6
View File
@@ -1,18 +1,20 @@
package webrtc
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/stun"
"github.com/pion/webrtc/v3"
"hash/crc32"
"net"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/stun"
"github.com/pion/webrtc/v3"
)
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) {
@@ -52,13 +54,15 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media
return
}
// WithResampling - will add for consumer: PCMA/0, PCMU/0, PCM/0, PCML/0
// so it can add resampling for PCMA/PCMU and repack for PCM/PCML
func WithResampling(medias []*core.Media) []*core.Media {
for _, media := range medias {
if media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly {
continue
}
var pcma, pcmu, pcm *core.Codec
var pcma, pcmu, pcm, pcml *core.Codec
for _, codec := range media.Codecs {
switch codec.Name {
@@ -76,6 +80,8 @@ func WithResampling(medias []*core.Media) []*core.Media {
}
case core.CodecPCM:
pcm = codec
case core.CodecPCML:
pcml = codec
}
}
@@ -94,6 +100,11 @@ func WithResampling(medias []*core.Media) []*core.Media {
pcm.Name = core.CodecPCM
media.Codecs = append(media.Codecs, pcm)
}
if pcma != nil && pcml == nil {
pcml = pcma.Clone()
pcml.Name = core.CodecPCML
media.Codecs = append(media.Codecs, pcml)
}
}
return medias
@@ -283,3 +294,35 @@ func CandidateManualHostTCPPassive(address string, port int) string {
foundation, priority, address, port,
)
}
func UnmarshalICEServers(b []byte) ([]webrtc.ICEServer, error) {
type ICEServer struct {
URLs any `json:"urls"`
Username string `json:"username,omitempty"`
Credential string `json:"credential,omitempty"`
}
var src []ICEServer
if err := json.Unmarshal(b, &src); err != nil {
return nil, err
}
var dst []webrtc.ICEServer
for i := range src {
srv := webrtc.ICEServer{
Username: src[i].Username,
Credential: src[i].Credential,
}
switch v := src[i].URLs.(type) {
case []string:
srv.URLs = v
case string:
srv.URLs = []string{v}
}
dst = append(dst, srv)
}
return dst, nil
}
+8 -1
View File
@@ -27,7 +27,6 @@ export class VideoRTC extends HTMLElement {
'hvc1.1.6.L153.B0', // H.265 main 5.1 (Chromecast Ultra)
'mp4a.40.2', // AAC LC
'mp4a.40.5', // AAC HE
'null', // for detecting liars (Safari iOS 12)
'flac', // FLAC (PCM compatible)
'opus', // OPUS Chrome, Firefox
];
@@ -241,6 +240,14 @@ export class VideoRTC extends HTMLElement {
this.appendChild(this.video);
// all Safari lies about supported audio codecs
const m = window.navigator.userAgent.match(/Version\/(\d+).+Safari/);
if (m) {
// AAC from v13, FLAC from v14, OPUS - unsupported
const skip = m[1] < '13' ? 'mp4a.40.2' : m[1] < '14' ? 'flac' : 'opus';
this.CODECS.splice(this.CODECS.indexOf(skip));
}
if (this.background) return;
if ('hidden' in document && this.visibilityCheck) {
+4
View File
@@ -1,5 +1,9 @@
import {VideoRTC} from './video-rtc.js';
/**
* This is example, how you can extend VideoRTC player for your app.
* Also you can check this example: https://github.com/AlexxIT/WebRTC
*/
class VideoStream extends VideoRTC {
set divMode(value) {
this.querySelector('.mode').innerText = value;