Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8526ae4eb | |||
| 966fbe7d61 | |||
| a77c2ef71f | |||
| 61a194e396 | |||
| ae25784d72 | |||
| 3343c78699 | |||
| 7928f54a95 | |||
| e4b68518e5 | |||
| 14ed1cdee8 | |||
| 72f159be88 | |||
| 144954b979 | |||
| 9e15391471 | |||
| d62b1e445a | |||
| ade4c035b7 | |||
| 13ca991c37 | |||
| e48459f49d | |||
| facf18e0df | |||
| 5c93dc62bd | |||
| d272d4b6c3 | |||
| 1b41edfc7e | |||
| d55270bd64 | |||
| 85225917f5 | |||
| eaef62a775 | |||
| f6c8d63658 | |||
| ea82d7ec2b | |||
| e8a7ba056c | |||
| 9fd40467f2 | |||
| c81e29fe54 | |||
| b9b7bb5489 | |||
| 8036278e29 | |||
| 39c25215ba | |||
| 490a48cd50 |
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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})
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 ""
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user