Compare commits

...

108 Commits

Author SHA1 Message Date
Alexey Khit
49f6233bde Update RTSP server filters 2022-11-08 01:50:28 +03:00
Alexey Khit
78c5c70c73 Add duration API for MP4 file 2022-11-08 01:29:58 +03:00
Alexey Khit
32651c74ab Fix frame.mp4 support 2022-11-08 01:13:38 +03:00
Alexey Khit
5c64d1f847 Update MSE procession on JS side 2022-11-08 00:37:32 +03:00
Alexey Khit
717af29630 Refactoring 2022-11-08 00:37:13 +03:00
Alexey Khit
ea18475d31 Split MSE data on packets 2022-11-07 23:35:36 +03:00
Alexey Khit
701a9c69ec Update write websocket func 2022-11-07 23:35:08 +03:00
Alexey Khit
c06253c8b2 Fix producer request new track after start 2022-11-07 17:48:45 +03:00
Alexey Khit
3a07e9fa03 Fix lock on mp4 restarts 2022-11-07 13:32:27 +03:00
Alexey Khit
e1bc30fab3 Add support AAC for RTSP 2022-11-07 11:02:26 +03:00
Alexey Khit
d16ae0972f Code refactoring 2022-11-07 11:01:19 +03:00
Alexey Khit
8b93c97e69 Add support AAC for RTMP to MP4 2022-11-06 22:44:48 +03:00
Alexey Khit
d8158bc1e3 Update stream log message 2022-11-04 22:27:11 +03:00
Alexey Khit
f4f588d2c6 Add mutex to stream 2022-11-04 22:20:52 +03:00
Alexey Khit
e287b52808 Add check for RTSPtoWeb API 2022-11-04 22:12:00 +03:00
Alexey Khit
ff96257252 Add backchannel=0 option to readme 2022-11-04 21:49:36 +03:00
Alexey Khit
909f21b7e4 Update docs about TURN server 2022-11-04 21:44:12 +03:00
Alexey Khit
7d6a5b44f8 Add frame.jpeg api for MJPEG stream 2022-11-04 21:22:33 +03:00
Alexey Khit
278f7696b6 Make sink private for Track 2022-11-04 20:54:35 +03:00
Alexey Khit
3cbf2465ae Fix loopback producer 2022-11-04 17:52:26 +03:00
Alexey Khit
e9ea7a0b1f Add reconnection feature 2022-11-04 17:23:42 +03:00
Alexey Khit
0231fc3a90 Code refactoring 2022-11-04 17:16:42 +03:00
Alexey Khit
9ef2633840 Add 5 sec timeout to ffmpeg rtsp 2022-11-04 17:06:24 +03:00
Alexey Khit
5a8df3e90a Change RTSP dial timeout to 5 sec 2022-11-04 17:05:57 +03:00
Alexey Khit
a31cbec3eb Fix check RTSP transport prefix 2022-11-04 17:05:30 +03:00
Alexey Khit
54f547977e Add mutext to streams handlers 2022-11-04 17:04:47 +03:00
Alexey Khit
65d91e02bd Move NewLogger to function 2022-11-04 17:03:56 +03:00
Alexey Khit
7fc3f0f641 Ignore srtp init in stack list func 2022-11-04 10:07:50 +03:00
Alexey Khit
7725d5ed31 Rewrite get handlers code 2022-11-04 06:24:39 +03:00
Alexey Khit
6c1b9daa8b Update logs for RTSP packets (disabled) 2022-11-03 11:25:47 +03:00
Alexey Khit
6d432574bf Make main logger global 2022-11-03 10:26:26 +03:00
Alexey Khit
616f69c88b Cache public IP for 5 min 2022-11-02 12:48:36 +03:00
Alexey Khit
f72440712b Add timeout to GetPublicIP func 2022-11-02 12:47:26 +03:00
Alexey Khit
ceed146fb8 Add webrtc sync API 2022-11-02 12:46:39 +03:00
Alexey Khit
f17dadbbbf Rewrite keepalive and add timeouts for RTSP 2022-11-02 10:50:11 +03:00
Alexey Khit
3d4514eab9 Fix loopback for stream 2022-11-02 08:51:54 +03:00
Alexey Khit
2629dccb81 Rename HTTP-FLV 2022-10-31 20:05:28 +03:00
Alexey Khit
04f1aa2900 Fix trash in webrtc.html 2022-10-31 08:34:32 +03:00
Alexey Khit
0dacdea1c3 Add support RTMPT (flv over HTTP) 2022-10-30 17:17:42 +03:00
Alexey Khit
24082b1616 Fix backchannel reconnection issue 2022-10-29 11:33:01 +03:00
Alexey Khit
7964b1743b Fix RTSP processing for Amcrest IP4M-1051 2022-10-29 11:29:53 +03:00
Alexey Khit
49773a1ece Add mjpeg link to stream to main page 2022-10-21 12:01:00 +03:00
Alexey Khit
c97a48a73f Fix mjpeg for 2K cameras 2022-10-21 12:00:00 +03:00
Alexey Khit
e03231ebb4 fix ffmpeg transcoding for Reolink RLC-510A 2022-10-21 10:58:56 +03:00
Alexey Khit
649525a842 Merge remote-tracking branch 'origin/master' 2022-10-21 10:54:54 +03:00
Alex X
d411c1a25c Merge pull request #76 from NickM-27/name_api_stream
Add ability for API to set name of stream
2022-10-14 06:59:31 +03:00
Nicolas Mowen
2f0bcf4ae0 Add ability for API to set name of stream 2022-10-13 14:58:26 -06:00
Alexey Khit
831c504cab Fix memory usage for RTSP processing 2022-10-05 21:15:59 +03:00
Alexey Khit
12925a6bc5 Fix TP-Link Tapo TC70 support 2022-10-05 19:43:36 +03:00
Alexey Khit
e50e929150 Fix empty SPS for mp4 format 2022-10-05 15:35:30 +03:00
Alexey Khit
d0c87e0379 Support SEI NAL from ffmpeg transcoding 2022-10-05 15:35:04 +03:00
Alexey Khit
247b61790e Update EncodeAVC for empty NALs 2022-10-05 15:34:34 +03:00
Alexey Khit
2ec618334a Adds NALs types logger 2022-10-05 15:33:51 +03:00
Alexey Khit
6f9976c806 Rework RTSP and RTMP processing 2022-10-05 13:25:29 +03:00
Alexey Khit
17b3a4cf3a Code refactoring 2022-10-05 13:23:31 +03:00
Alexey Khit
ba30f46c02 Fix FmtpLine for RTMP 2022-10-05 10:50:00 +03:00
Alexey Khit
4134f2a89c Fix timestamp for RTMP 2022-10-05 10:48:37 +03:00
Alexey Khit
a81160bea1 Fix support Escam Q6 camera 2022-10-03 21:12:27 +03:00
Alexey Khit
80392acb78 Fix audio copy #46 2022-09-24 08:24:52 +03:00
Alexey Khit
5afac513b4 Adds codecs section to readme 2022-09-22 00:22:44 +03:00
Alexey Khit
2243110e08 Fix H265 support for different sources 2022-09-21 23:43:52 +03:00
Alexey Khit
04a6e64650 Adds support WebRTC + H265 to Safari 2022-09-21 22:28:59 +03:00
Alexey Khit
62c13f016b Remove broken Safari codec from WebRTC API 2022-09-21 22:27:46 +03:00
Alexey Khit
9596c6139f Adds support H265 for MP4 2022-09-18 20:24:21 +03:00
Alexey Khit
34f5b99126 Update codecs.html 2022-09-18 17:37:44 +03:00
Alexey Khit
b562392d45 Remove unnecessary imports 2022-09-18 17:14:59 +03:00
Alexey Khit
eb8a4919a2 Adds fix on RemoveConsumer panic 2022-09-18 17:14:18 +03:00
Alexey Khit
237fbf23a1 FIx backward support for RTSPtoWebRTC API 2022-09-18 02:01:43 +03:00
Alexey Khit
12a73b00cb Fix readme 2022-09-17 20:32:17 +03:00
Alexey Khit
ce0fac959f Adds module MJPEG 2022-09-17 19:13:25 +03:00
Alexey Khit
1b14be7033 Update readme about Hass module 2022-09-17 06:41:56 +03:00
Alexey Khit
bbbade4097 Adds rtsp link to index.html 2022-09-16 17:59:54 +03:00
Alexey Khit
8f43ad2a35 Adds pretty print to info 2022-09-16 17:39:29 +03:00
Alexey Khit
105331d50f Fix track async access 2022-09-16 17:22:48 +03:00
Alexey Khit
a45d0b507b Code refactoring 2022-09-16 17:04:00 +03:00
Alexey Khit
407ccc45bc Adds URL templates to integration with Hass 2022-09-16 17:03:03 +03:00
Alexey Khit
428628fcce Code refactoring 2022-09-16 17:00:56 +03:00
Alexey Khit
fa23bb6899 Handle FFmpeg RTMP as HTTP source 2022-09-16 17:00:24 +03:00
Alexey Khit
71e1c840a7 Fix base_path for integration with Hass 2022-09-16 14:19:23 +03:00
Alexey Khit
63b9639e86 Adds trace logs for API 2022-09-16 12:11:40 +03:00
Alexey Khit
ae3e1372c8 Adds support RTSPtoWeb API (entity_id for zero config from Hass) 2022-09-16 12:10:58 +03:00
Alexey Khit
800ebb39be Adds canditates from domain resolver 2022-09-15 09:07:53 +03:00
Alexey Khit
3a10cb25bb Fix green video from RTSP H264 2022-09-15 06:55:05 +03:00
Alexey Khit
7784b0e64c Adds about ivideon to readme 2022-09-14 17:59:12 +03:00
Alexey Khit
945b486fe0 Update readme about new source Echo 2022-09-14 15:53:46 +03:00
Alexey Khit
d72d7b089c Reduce docker container size and add python inside 2022-09-14 14:12:43 +03:00
Alexey Khit
d339fbe712 Fix backchannel option for Dahua VTO2111D 2022-09-13 21:57:07 +03:00
Alexey Khit
3aeb278c47 Option to disable backchannel for RTSP 2022-09-13 21:54:49 +03:00
Alexey Khit
c92c1fc3e9 Adds echo source 2022-09-13 15:42:39 +03:00
Alexey Khit
def57119f4 Move shell QuoteSplit to separate pkg 2022-09-13 15:42:23 +03:00
Alexey Khit
b20275d2b5 Adds support ivideon source 2022-09-13 14:41:28 +03:00
Alexey Khit
a11ca1da6e Adds error output to producer start function 2022-09-13 14:40:58 +03:00
Alexey Khit
0fb7132947 Move SplitAVC to public function 2022-09-13 14:39:55 +03:00
Alexey Khit
0f9e3c97c5 Update mp4 entry duration 2022-09-13 14:39:19 +03:00
Alexey Khit
e049a17216 Adds error handler for mp4 init 2022-09-13 14:38:54 +03:00
Alexey Khit
217c8c2bf6 Update readme about MP4 module 2022-09-10 06:26:15 +03:00
Alexey Khit
9f0153e2a8 Adds skip SEI frame 2022-09-09 19:32:56 +03:00
Alexey Khit
b2eaf03914 Adds match any clockrate or channels 2022-09-09 19:32:36 +03:00
Alexey Khit
8b54444c89 Adds mp4 module 2022-09-09 19:31:52 +03:00
Alexey Khit
76b352d67f Add exec launch time to debug 2022-09-07 12:21:36 +03:00
Alexey Khit
e8edb65a31 Adds ignoring unnecessary ffmpeg rtsp input tracks 2022-09-07 11:31:15 +03:00
Alexey Khit
88a6208912 Update ffmpeg output param name 2022-09-07 11:29:59 +03:00
Alexey Khit
14b6df68ce Adds support nginx with wrong port 2022-09-06 18:09:44 +03:00
Alexey Khit
77080663ee Add the feature for update to any version 2022-09-06 14:10:08 +03:00
Alexey Khit
d25d27a0ee Fix SDP parsing for noname camera 2022-09-06 12:43:10 +03:00
Alexey Khit
5460e194e8 Update loggers 2022-09-06 07:08:35 +03:00
Alexey Khit
e4f565f343 Fix H264 in RTSP processing 2022-09-06 07:00:22 +03:00
Alexey Khit
6b274f2a37 Adds Hass stream info to log 2022-09-06 06:36:13 +03:00
78 changed files with 3408 additions and 1072 deletions

137
README.md
View File

@@ -5,11 +5,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
![](assets/go2rtc.png) ![](assets/go2rtc.png)
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM) - zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for all supported protocols (lowest possible streaming latency) - zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams) - streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-api) - streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit) - first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- low CPU load for supported codecs
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg) - on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- multi-source 2-way [codecs negotiation](#codecs-negotiation) - multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream - mixing tracks from different sources to single stream
@@ -98,13 +97,9 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
- go2rtc > Install > Start - go2rtc > Install > Start
2. Setup [Integration](#module-hass) 2. Setup [Integration](#module-hass)
**Optionally:**
- create `go2rtc.yaml` in your Home Assistant [config](https://www.home-assistant.io/docs/configuration) folder
### go2rtc: Docker ### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg) and [Ngrok](#module-ngrok) applications. Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
```yaml ```yaml
services: services:
@@ -132,8 +127,9 @@ Available modules:
- [api](#module-api) - HTTP API (important for WebRTC support) - [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support) - [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server - [webrtc](#module-webrtc) - WebRTC Server
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network) - [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot
- [ffmpeg](#source-ffmpeg) - FFmpeg integration - [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration - [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config - [log](#module-log) - logs config
@@ -148,11 +144,11 @@ Available source types:
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types) - [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam - [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration - [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [echo](#source-echo) - get stream link from bash or python
- [homekit](#source-homekit) - streaming from HomeKit Camera - [homekit](#source-homekit) - streaming from HomeKit Camera
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration - [hass](#source-hass) - Home Assistant integration
**PS.** You can use sources like `MJPEG`, `HLS` and others via FFmpeg integration.
#### Source: RTSP #### Source: RTSP
- Support **RTSP and RTSPS** links with multiple video and audio tracks - Support **RTSP and RTSPS** links with multiple video and audio tracks
@@ -176,6 +172,8 @@ streams:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1 - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
``` ```
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
#### Source: RTMP #### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio. You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
@@ -254,6 +252,19 @@ streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output} stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
``` ```
#### Source: Echo
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
Check examples in [wiki](https://github.com/AlexxIT/go2rtc/wiki/Source-Echo-examples).
```yaml
streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: HomeKit #### Source: HomeKit
**Important:** **Important:**
@@ -270,6 +281,15 @@ If you see a device but it does not have a pair button - it is paired to some ec
**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). **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: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
```yaml
streams:
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
```
#### Source: Hass #### Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files: Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
@@ -309,13 +329,9 @@ api:
### Module: RTSP ### Module: RTSP
You can get any stream as RTSP-stream with codecs filter: You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
``` - you can omit the codec filters, so one first video and one first audio will be selected
rtsp://192.168.1.123/{stream_name}?video={codec}&audio={codec1}&audio={codec2}
```
- you can omit the codecs, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected - you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
- you can set multiple video or audio, so all of them will be selected - you can set multiple video or audio, so all of them will be selected
@@ -371,13 +387,13 @@ ngrok:
command: ... command: ...
``` ```
**Own TCP-tunnel** **Hard tech way 1. Own TCP-tunnel**
If you have personal VPS, you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config. If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
**Using TURN-server** **Hard tech way 2. Using TURN-server**
TODO... If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can install TURN server (e.g. [coturn](https://github.com/coturn/coturn), config [example](https://github.com/AlexxIT/WebRTC/wiki/Coturn-Example)).
```yaml ```yaml
webrtc: webrtc:
@@ -444,25 +460,54 @@ tunnels:
### Module: Hass ### Module: Hass
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example: #### From go2rtc to Hass
- `http://127.0.0.1:1984/` to web interface Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
In other cases you need to use IP-address of server with **go2rtc** application. 1. Add your stream to [go2rtc config](#configuration)
2. Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1`
1. Add integration with link to go2rtc HTTP API: #### From Hass to go2rtc
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. Add generic camera with RTSP link: View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://...` or `rtmp://...`
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc** When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
3. Use Picture Entity or Picture Glance lovelace card
4. Open full screen card - this is should be WebRTC stream 1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. Use Picture Entity or Picture Glance lovelace card
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
```yaml
streams:
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
```
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided. PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
### Module: MP4
Provides several features:
1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
3. Progressive MP4 stream - bad format for streaming because of high latency, doesn't work in Safari
### Module: MJPEG
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your camera outputs H264/H265 - you SHOULD use transcoding. With this example, your stream will have both H264 and MJPEG codecs:
```yaml
streams:
camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=mjpeg
```
Example link to MJPEG: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
### Module: Log ### Module: Log
You can set different log levels for different modules. You can set different log levels for different modules.
@@ -504,6 +549,30 @@ If you need Web interface protection without Home Assistant Add-on - you need to
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port. PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
## Codecs madness
`AVC/H.264` codec 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 | MP4
-------|--------|-----|----
*latency* | best | medium | bad
Desktop Chrome | H264 | H264, H265* | H264, H265*
Desktop Safari | H264, H265* | H264 | no
Desktop Edge | H264 | H264, H265* | H264, H265*
Desktop Firefox | H264 | H264 | H264
Desktop Opera | no | H264 | H264
iPhone Safari | H264, H265* | no | no
iPad Safari | H264, H265* | H264 | no
Android Chrome | H264 | H264 | H264
masOS Hass App | no | no | no
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- MSE/MP4 audio codecs: not supported yet (should be: `AAC`)
- Chrome H265: [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/)
- Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
- iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265
## FAQ ## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?** **Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**

View File

@@ -1,23 +1,40 @@
ARG BUILD_FROM ARG BUILD_FROM
FROM $BUILD_FROM
RUN apk add --no-cache git go ffmpeg FROM $BUILD_FROM as build
ARG BUILD_ARCH # 1. Build go2rtc
RUN apk add --no-cache git go
RUN git clone https://github.com/AlexxIT/go2rtc \ RUN git clone https://github.com/AlexxIT/go2rtc \
&& cd go2rtc \ && cd go2rtc \
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin && CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Download ngrok
ARG BUILD_ARCH
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile # https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \ RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \ elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
&& cd go2rtc \ && cd go2rtc \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \ && curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok -d /usr/local/bin && unzip ngrok
RUN rm -r /go2rtc
# https://devopscube.com/reduce-docker-image-size/
FROM $BUILD_FROM
# 3. Copy go2rtc and ngrok to release
COPY --from=build /go2rtc/go2rtc /usr/local/bin
COPY --from=build /go2rtc/ngrok /usr/local/bin
# 4. Install ffmpeg
# apk base OK: 22 MiB in 40 packages
# ffmpeg OK: 113 MiB in 110 packages
# python3 OK: 161 MiB in 114 packages
RUN apk add --no-cache ffmpeg python3
# 5. Copy run to release
COPY run.sh / COPY run.sh /
RUN chmod a+x /run.sh RUN chmod a+x /run.sh

View File

@@ -2,13 +2,13 @@
set +e set +e
# set cwd for go2rtc (for config file, Hass itegration, etc) # set cwd for go2rtc (for config file, Hass integration, etc)
cd /config cd /config
# add the feature to override go2rtc binary from Hass config folder # add the feature to override go2rtc binary from Hass config folder
export PATH="/config:$PATH" export PATH="/config:$PATH"
while true; do while true; do
go2rtc go2rtc
sleep 5 sleep 5
done done

View File

@@ -34,11 +34,10 @@ func Init() {
log = app.GetLogger("api") log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir) initStatic(cfg.Mod.StaticDir)
initWS()
HandleFunc("/api/frame.mp4", frameHandler) HandleFunc("api/streams", streamsHandler)
HandleFunc("/api/frame.raw", frameHandler) HandleFunc("api/ws", apiWS)
HandleFunc("/api/streams", streamsHandler)
HandleFunc("/api/ws", apiWS)
// ensure we can listen without errors // ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen) listener, err := net.Listen("tcp", cfg.Mod.Listen)
@@ -51,14 +50,29 @@ func Init() {
go func() { go func() {
s := http.Server{} s := http.Server{}
if log.Trace().Enabled() {
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Stringer("url", r.URL).Msgf("[api] %s", r.Method)
http.DefaultServeMux.ServeHTTP(w, r)
})
}
if err = s.Serve(listener); err != nil { if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] serve") log.Fatal().Err(err).Msg("[api] serve")
} }
}() }()
} }
// HandleFunc handle pattern with relative path:
// - "api/streams" => "{basepath}/api/streams"
// - "/streams" => "/streams"
func HandleFunc(pattern string, handler http.HandlerFunc) { func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(basePath+pattern, handler) if len(pattern) == 0 || pattern[0] != '/' {
pattern = basePath + "/" + pattern
}
log.Trace().Str("path", pattern).Msg("[api] register path")
http.HandleFunc(pattern, handler)
} }
func HandleWS(msgType string, handler WSHandler) { func HandleWS(msgType string, handler WSHandler) {
@@ -71,10 +85,15 @@ var wsHandlers = make(map[string]WSHandler)
func streamsHandler(w http.ResponseWriter, r *http.Request) { func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
name := r.URL.Query().Get("name")
if name == "" {
name = src
}
switch r.Method { switch r.Method {
case "PUT": case "PUT":
streams.Get(src) streams.New(name, src)
return return
case "DELETE": case "DELETE":
streams.Delete(src) streams.Delete(src)
@@ -87,13 +106,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
} else { } else {
v = streams.All() v = streams.All()
} }
data, err := json.Marshal(v)
if err != nil { e := json.NewEncoder(w)
log.Error().Err(err).Msg("[api.streams] marshal") e.SetIndent("", " ")
} _ = e.Encode(v)
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.streams] write")
}
} }
func apiWS(w http.ResponseWriter, r *http.Request) { func apiWS(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,40 +0,0 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/keyframe"
"net/http"
"strings"
)
func frameHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}
var ch = make(chan []byte)
cons := new(keyframe.Consumer)
cons.IsMP4 = strings.HasSuffix(r.URL.Path, ".mp4")
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case []byte:
ch <- msg.([]byte)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.frame] add consumer")
return
}
data := <-ch
stream.RemoveConsumer(cons)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.frame] write")
}
}

View File

@@ -14,13 +14,13 @@ func initStatic(staticDir string) {
root = http.FS(www.Static) root = http.FS(www.Static)
} }
base := len(basePath)
fileServer := http.FileServer(root) fileServer := http.FileServer(root)
HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
if basePath != "" { if base > 0 {
r.URL.Path = r.URL.Path[len(basePath):] r.URL.Path = r.URL.Path[base:]
} }
fileServer.ServeHTTP(w, r) fileServer.ServeHTTP(w, r)
}) })
} }

View File

@@ -4,16 +4,42 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"net/http" "net/http"
"net/url"
"strings"
"sync" "sync"
) )
type WSHandler func(ctx *Context, msg *streamer.Message) func initWS() {
wsUp = &websocket.Upgrader{
var apiWsUp = websocket.Upgrader{ ReadBufferSize: 1024,
ReadBufferSize: 1024, WriteBufferSize: 512000,
WriteBufferSize: 512000, }
wsUp.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
o, err := url.Parse(origin[0])
if err != nil {
return false
}
if o.Host == r.Host {
return true
}
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host)
// some users change Nginx external port using Docker port
// so origin will be with a port and host without
if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host
}
return false
}
} }
var wsUp *websocket.Upgrader
type WSHandler func(ctx *Context, msg *streamer.Message)
type Context struct { type Context struct {
Conn *websocket.Conn Conn *websocket.Conn
Request *http.Request Request *http.Request
@@ -24,7 +50,7 @@ type Context struct {
} }
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) { func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
ctx.Conn, err = apiWsUp.Upgrade(w, r, nil) ctx.Conn, err = wsUp.Upgrade(w, r, nil)
ctx.Request = r ctx.Request = r
return return
} }
@@ -38,22 +64,14 @@ func (ctx *Context) Close() {
func (ctx *Context) Write(msg interface{}) { func (ctx *Context) Write(msg interface{}) {
ctx.mu.Lock() ctx.mu.Lock()
defer ctx.mu.Unlock()
var err error if data, ok := msg.([]byte); ok {
_ = ctx.Conn.WriteMessage(websocket.BinaryMessage, data)
switch msg := msg.(type) { } else {
case *streamer.Message: _ = ctx.Conn.WriteJSON(msg)
err = ctx.Conn.WriteJSON(msg)
case []byte:
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
default:
return
} }
if err != nil { ctx.mu.Unlock()
//panic(err) // TODO: fix panic
}
} }
func (ctx *Context) Error(err error) { func (ctx *Context) Error(err error) {

View File

@@ -3,6 +3,7 @@ package app
import ( import (
"flag" "flag"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io" "io"
"os" "os"
@@ -30,10 +31,18 @@ func Init() {
} }
} }
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
path, _ := os.Getwd()
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
}
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout var writer io.Writer = os.Stdout
// styles
format := cfg.Mod["format"]
if format != "json" { if format != "json" {
writer = zerolog.ConsoleWriter{ writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000", Out: writer, TimeFormat: "15:04:05.000",
@@ -43,18 +52,12 @@ func Init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(cfg.Mod["level"]) lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel { if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel lvl = zerolog.InfoLevel
} }
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl) return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
modules = cfg.Mod
path, _ := os.Getwd()
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
} }
func LoadConfig(v interface{}) { func LoadConfig(v interface{}) {
@@ -68,15 +71,13 @@ func LoadConfig(v interface{}) {
func GetLogger(module string) zerolog.Logger { func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok { if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s) lvl, err := zerolog.ParseLevel(s)
if err != nil { if err == nil {
log.Warn().Err(err).Msg("[log]") return log.Level(lvl)
return log
} }
log.Warn().Err(err).Caller().Send()
return log.Level(lvl)
} }
return log return log.Logger
} }
// internal // internal
@@ -84,8 +85,5 @@ func GetLogger(module string) zerolog.Logger {
// data - config content // data - config content
var data []byte var data []byte
// log - main logger
var log zerolog.Logger
// modules log levels // modules log levels
var modules map[string]string var modules map[string]string

View File

@@ -10,8 +10,8 @@ import (
) )
func Init() { func Init() {
api.HandleFunc("/api/stack", stackHandler) api.HandleFunc("api/stack", stackHandler)
api.HandleFunc("/api/exit", exitHandler) api.HandleFunc("api/exit", exitHandler)
streams.HandleFunc("null", nullHandler) streams.HandleFunc("null", nullHandler)
} }

View File

@@ -21,6 +21,7 @@ var stackSkip = [][]byte{
[]byte("created by net/http.(*Server).Serve"), // TODO: why two? []byte("created by net/http.(*Server).Serve"), // TODO: why two?
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"), []byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
// webrtc/api.go // webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"), []byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),

29
cmd/echo/echo.go Normal file
View File

@@ -0,0 +1,29 @@
package echo
import (
"bytes"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
)
func Init() {
log := app.GetLogger("echo")
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return nil, err
}
b = bytes.TrimSpace(b)
log.Debug().Str("url", url).Msgf("[echo] %s", b)
return streams.GetProducer(string(b))
})
}

View File

@@ -8,11 +8,13 @@ import (
"github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"time" "time"
) )
@@ -22,22 +24,22 @@ func Init() {
return return
} }
rtsp.OnProducer = func(prod streamer.Producer) bool { rtsp.HandleFunc(func(conn *pkg.Conn) bool {
if conn := prod.(*pkg.Conn); conn != nil { waitersMu.Lock()
if waiter := waiters[conn.URL.Path]; waiter != nil { waiter := waiters[conn.URL.Path]
waiter <- prod waitersMu.Unlock()
return true
} if waiter == nil {
return false
} }
return false
} waiter <- conn
return true
})
streams.HandleFunc("exec", Handle) streams.HandleFunc("exec", Handle)
log = app.GetLogger("exec") log = app.GetLogger("exec")
// TODO: add sync.Mutex
waiters = map[string]chan streamer.Producer{}
} }
func Handle(url string) (streamer.Producer, error) { func Handle(url string) (streamer.Producer, error) {
@@ -49,7 +51,7 @@ func Handle(url string) (streamer.Producer, error) {
) )
// remove `exec:` // remove `exec:`
args := QuoteSplit(url[5:]) args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() { if log.Trace().Enabled() {
@@ -59,11 +61,20 @@ func Handle(url string) (streamer.Producer, error) {
ch := make(chan streamer.Producer) ch := make(chan streamer.Producer)
waitersMu.Lock()
waiters[path] = ch waiters[path] = ch
defer delete(waiters, path) waitersMu.Unlock()
defer func() {
waitersMu.Lock()
delete(waiters, path)
waitersMu.Unlock()
}()
log.Debug().Str("url", url).Msg("[exec] run") log.Debug().Str("url", url).Msg("[exec] run")
ts := time.Now()
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]") log.Error().Err(err).Str("url", url).Msg("[exec]")
return nil, err return nil, err
@@ -75,6 +86,7 @@ func Handle(url string) (streamer.Producer, error) {
log.Error().Str("url", url).Msg("[exec] timeout") log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout") return nil, errors.New("timeout")
case prod := <-ch: case prod := <-ch:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
return prod, nil return prod, nil
} }
} }
@@ -82,40 +94,5 @@ func Handle(url string) (streamer.Producer, error) {
// internal // internal
var log zerolog.Logger var log zerolog.Logger
var waiters map[string]chan streamer.Producer var waiters = map[string]chan streamer.Producer{}
var waitersMu sync.Mutex
func QuoteSplit(s string) []string {
var a []string
for len(s) > 0 {
is := strings.IndexByte(s, ' ')
if is >= 0 {
// skip prefix and double spaces
if is == 0 {
// goto next symbol
s = s[1:]
continue
}
// check if quote in word
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
// search quote end
if is = strings.Index(s, `" `); is > 0 {
is += 1
} else {
is = -1
}
}
}
if is >= 0 {
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
s = s[is+1:]
} else {
//add last word
a = append(a, s)
break
}
}
return a
}

View File

@@ -39,3 +39,5 @@
- https://html5test.com/ - https://html5test.com/
- https://trac.ffmpeg.org/wiki/Capture/Webcam - https://trac.ffmpeg.org/wiki/Capture/Webcam
- https://trac.ffmpeg.org/wiki/DirectShow - https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md

View File

@@ -15,7 +15,7 @@ import (
func Init() { func Init() {
log = app.GetLogger("exec") log = app.GetLogger("exec")
api.HandleFunc("/api/devices", handle) api.HandleFunc("api/devices", handle)
} }
func GetInput(src string) (string, error) { func GetInput(src string) (string, error) {

View File

@@ -23,10 +23,10 @@ func Init() {
// inputs // inputs
"file": "-re -stream_loop -1 -i {input}", "file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}", "http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}", "rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -timeout 5000000 -i {input}",
// output // output
"out": "-rtsp_transport tcp -f rtsp {output}", "output": "-rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval // `-g 30` - group of picture, GOP, keyframe interval
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1` // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
@@ -37,6 +37,7 @@ func Init() {
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency", "h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency", "h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency", "h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
"mjpeg": "-codec:v mjpeg -force_duplicated_matrix 1 -huffman 0 -pix_fmt yuvj420p",
"opus": "-codec:a libopus -ar 48000 -ac 2", "opus": "-codec:a libopus -ar 48000 -ac 2",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1", "pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1", "pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
@@ -55,18 +56,36 @@ func Init() {
s = s[7:] // remove `ffmpeg:` s = s[7:] // remove `ffmpeg:`
var query url.Values var query url.Values
var queryVideo, queryAudio bool
if i := strings.IndexByte(s, '#'); i > 0 { if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:]) query = parseQuery(s[i+1:])
queryVideo = query["video"] != nil
queryAudio = query["audio"] != nil
s = s[:i] s = s[:i]
} else {
// by default query both video and audio
queryVideo = true
queryAudio = true
} }
var input string var input string
if i := strings.IndexByte(s, ':'); i > 0 { if i := strings.IndexByte(s, ':'); i > 0 {
switch s[:i] { switch s[:i] {
case "http", "https": case "http", "https", "rtmp":
input = strings.Replace(tpl["http"], "{input}", s, 1) input = strings.Replace(tpl["http"], "{input}", s, 1)
case "rtsp", "rtsps": case "rtsp", "rtsps":
input = strings.Replace(tpl["rtsp"], "{input}", s, 1) // https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case queryVideo && queryAudio:
input = "-allowed_media_types video+audio "
case queryVideo:
input = "-allowed_media_types video "
case queryAudio:
input = "-allowed_media_types audio "
}
input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
} }
} }
@@ -102,23 +121,23 @@ func Init() {
for _, audio := range query["audio"] { for _, audio := range query["audio"] {
if audio == "copy" { if audio == "copy" {
s += " -codec:v copy" s += " -codec:a copy"
} else { } else {
s += " " + tpl[audio] s += " " + tpl[audio]
} }
} }
if query["video"] == nil { switch {
s += " -vn" case queryVideo && !queryAudio:
}
if query["audio"] == nil {
s += " -an" s += " -an"
case queryAudio && !queryVideo:
s += " -vn"
} }
} else { } else {
s += " -c copy" s += " -c copy"
} }
s += " " + tpl["out"] s += " " + tpl["output"]
return exec.Handle(s) return exec.Handle(s)
}) })

153
cmd/hass/api.go Normal file
View File

@@ -0,0 +1,153 @@
package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"net/http"
"net/url"
"strings"
)
func initAPI() {
ok := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
}
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/streams", ok)
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
return
}
// we can get three types of links:
// 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 {
// check if it is rtsp link to go2rtc
stream = rtspStream(v.Channels.First.Url)
if stream != nil {
streams.New(v.Name, stream)
} else {
stream = streams.New(v.Name, "{input}")
}
}
stream.SetSource(v.Channels.First.Url)
ok(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)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
}
})
// api from RTSPtoWebRTC
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return
}
src := r.FormValue("url")
src, err = url.QueryUnescape(src)
if err != nil {
return
}
stream := streams.Get(src)
if stream == nil {
if stream = rtspStream(src); stream != nil {
streams.New(src, stream)
} else {
stream = streams.New(src, src)
}
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
return
}
v := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
})
}
func rtspStream(url string) *streams.Stream {
if strings.HasPrefix(url, "rtsp://") {
if i := strings.IndexByte(url[7:], '/'); i > 0 {
return streams.Get(url[8+i:])
}
}
return nil
}
type addJSON struct {
Name string `json:"name"`
Channels struct {
First struct {
//Name string `json:"name"`
Url string `json:"url"`
} `json:"0"`
} `json:"channels"`
}

View File

@@ -1,20 +1,14 @@
package hass package hass
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"net/http"
"net/url"
"os" "os"
"path" "path"
"strings"
) )
func Init() { func Init() {
@@ -26,13 +20,9 @@ func Init() {
app.LoadConfig(&conf) app.LoadConfig(&conf)
log = app.GetLogger("api") log = app.GetLogger("hass")
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/ initAPI()
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/stream", handler)
// support load cameras from Hass config file // support load cameras from Hass config file
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries") filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
@@ -78,72 +68,13 @@ func Init() {
continue continue
} }
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
//streams.Get("hass:" + entrie.Title) //streams.Get("hass:" + entrie.Title)
} }
} }
var log zerolog.Logger var log zerolog.Logger
func handler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
src := r.FormValue("url")
src, err := url.QueryUnescape(src)
if err != nil {
log.Error().Err(err).Msg("[api.hass] query unescape")
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
// check if stream links to our rtsp server
if strings.HasPrefix(src, "rtsp://") {
i := strings.IndexByte(src[7:], '/')
if i > 0 && streams.Has(src[8+i:]) {
src = src[8+i:]
}
}
stream := streams.Get(src)
if stream == nil {
log.Error().Str("url", src).Msg("[api.hass] unsupported source")
return
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
resp := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
data, err := json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("[api.hass] marshal JSON")
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.hass] write")
return
}
}
type entries struct { type entries struct {
Data struct { Data struct {
Entries []struct { Entries []struct {

View File

@@ -54,15 +54,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
items = append(items, device) items = append(items, device)
} }
data, err := json.Marshal(items) _= json.NewEncoder(w).Encode(items)
if err != nil {
log.Error().Err(err).Msg("[api.homekit]")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.homekit]")
}
case "POST": case "POST":
// TODO: post params... // TODO: post params...

View File

@@ -14,7 +14,7 @@ func Init() {
streams.HandleFunc("homekit", streamHandler) streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("/api/homekit", apiHandler) api.HandleFunc("api/homekit", apiHandler)
} }
var log zerolog.Logger var log zerolog.Logger

19
cmd/ivideon/ivideon.go Normal file
View File

@@ -0,0 +1,19 @@
package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
func Init() {
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
id := strings.Replace(url[8:], "/", ":", 1)
prod := ivideon.NewClient(id)
if err := prod.Dial(); err != nil {
return nil, err
}
return prod, nil
})
}

89
cmd/mjpeg/mjpeg.go Normal file
View File

@@ -0,0 +1,89 @@
package mjpeg
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan []byte)
cons := &mjpeg.Consumer{}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
exit <- msg
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handlerStream(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan struct{})
cons := &mjpeg.Consumer{}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
data := []byte(header + strconv.Itoa(len(msg)))
data = append(data, 0x0D, 0x0A, 0x0D, 0x0A)
data = append(data, msg...)
data = append(data, 0x0D, 0x0A)
if _, err := w.Write(data); err != nil {
exit <- struct{}{}
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
return
}
w.Header().Set("Content-Type", `multipart/x-mixed-replace; boundary=frame`)
<-exit
stream.RemoveConsumer(cons)
//log.Trace().Msg("[api.mjpeg] close")
}

151
cmd/mp4/mp4.go Normal file
View File

@@ -0,0 +1,151 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/rs/zerolog"
"net/http"
"strconv"
"strings"
"time"
)
func Init() {
log = app.GetLogger("mp4")
api.HandleWS(MsgTypeMSE, handlerWS)
api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("api/stream.mp4", handlerMP4)
}
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) {
return
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan []byte)
cons := &mp4.Keyframe{}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok && exit != nil {
exit <- data
exit = nil
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("Content-Type", cons.MimeType)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerMP4(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) || isSafari(w, r) {
return
}
log.Trace().Msgf("[api.mp4] %+v", r)
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan error)
cons := &mp4.Consumer{}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
exit <- err
exit = nil
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
defer stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", cons.MimeType())
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
return
}
cons.Start()
var duration *time.Timer
if s := r.URL.Query().Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
if exit != nil {
exit <- nil
exit = nil
}
})
}
}
err = <-exit
log.Trace().Err(err).Caller().Send()
if duration != nil {
duration.Stop()
}
}
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
if strings.Contains(r.UserAgent(), " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return true
}
}
return false
}
func isSafari(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Range") == "bytes=0-1" {
handlerKeyframe(w, r)
return true
}
return false
}

57
cmd/mp4/mse.go Normal file
View File

@@ -0,0 +1,57 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
const MsgTypeMSE = "mse" // fMP4
const packetSize = 8192
func handlerWS(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
cons := &mp4.Consumer{}
cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
for len(data) > packetSize {
ctx.Write(data[:packetSize])
data = data[packetSize:]
}
ctx.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Caller().Send()
ctx.Error(err)
return
}
ctx.OnClose(func() {
stream.RemoveConsumer(cons)
})
ctx.Write(&streamer.Message{Type: MsgTypeMSE, Value: cons.MimeType()})
data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Caller().Send()
ctx.Error(err)
return
}
ctx.Write(data)
cons.Start()
}

View File

@@ -1,42 +0,0 @@
package mse
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mse"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleWS("mse", handler)
}
func handler(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}
cons := new(mse.Consumer)
cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case *streamer.Message, []byte:
ctx.Write(msg)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.mse] Add consumer")
ctx.Error(err)
return
}
ctx.OnClose(func() {
stream.RemoveConsumer(cons)
})
cons.Init()
}

View File

@@ -8,6 +8,8 @@ import (
func Init() { func Init() {
streams.HandleFunc("rtmp", handle) streams.HandleFunc("rtmp", handle)
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
} }
func handle(url string) (streamer.Producer, error) { func handle(url string) (streamer.Producer, error) {

View File

@@ -8,6 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"net" "net"
"strings"
) )
func Init() { func Init() {
@@ -31,22 +32,54 @@ func Init() {
// RTSP server support // RTSP server support
address := conf.Mod.Listen address := conf.Mod.Listen
if address != "" { if address == "" {
_, Port, _ = net.SplitHostPort(address) return
go worker(address)
} }
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
_, Port, _ = net.SplitHostPort(address)
log.Info().Str("addr", address).Msg("[rtsp] listen")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go tcpHandler(conn)
}
}()
}
type Handler func(conn *rtsp.Conn) bool
func HandleFunc(handler Handler) {
handlers = append(handlers, handler)
} }
var Port string var Port string
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
// internal // internal
var log zerolog.Logger var log zerolog.Logger
var handlers []Handler
func rtspHandler(url string) (streamer.Producer, error) { func rtspHandler(url string) (streamer.Producer, error) {
backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 {
if url[i+1:] == "backchannel=0" {
backchannel = false
}
url = url[:i]
}
conn, err := rtsp.NewClient(url) conn, err := rtsp.NewClient(url)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -67,13 +100,17 @@ func rtspHandler(url string) (streamer.Producer, error) {
return nil, err return nil, err
} }
conn.Backchannel = true conn.Backchannel = backchannel
if err = conn.Describe(); err != nil { if err = conn.Describe(); err != nil {
if !backchannel {
return nil, err
}
// second try without backchannel, we need to reconnect // second try without backchannel, we need to reconnect
conn.Backchannel = false
if err = conn.Dial(); err != nil { if err = conn.Dial(); err != nil {
return nil, err return nil, err
} }
conn.Backchannel = false
if err = conn.Describe(); err != nil { if err = conn.Describe(); err != nil {
return nil, err return nil, err
} }
@@ -82,101 +119,89 @@ func rtspHandler(url string) (streamer.Producer, error) {
return conn, nil return conn, nil
} }
func worker(address string) { func tcpHandler(c net.Conn) {
srv, err := tcp.NewServer(address) var name string
if err != nil { var closer func()
log.Error().Err(err).Msg("[rtsp] listen")
return
}
log.Info().Str("addr", address).Msg("[rtsp] listen") trace := log.Trace().Enabled()
srv.Listen(func(msg interface{}) { conn := rtsp.NewServer(c)
switch msg.(type) { conn.Listen(func(msg interface{}) {
case net.Conn: if trace {
var name string switch msg := msg.(type) {
var onDisconnect func() case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
trace := log.Trace().Enabled() switch msg {
case rtsp.MethodDescribe:
name = conn.URL.Path[1:]
conn := rtsp.NewServer(msg.(net.Conn)) stream := streams.Get(name)
conn.Listen(func(msg interface{}) { if stream == nil {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
switch msg {
case rtsp.MethodDescribe:
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
stream := streams.Get(name) // TODO: rewrite
if stream == nil {
return
}
initMedias(conn)
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
onDisconnect = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if OnProducer != nil {
if OnProducer(conn) {
return
}
}
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
str := streams.Get(conn.URL.Path[1:])
if str == nil {
return
}
str.AddProducer(conn)
onDisconnect = func() {
str.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})
if err = conn.Accept(); err != nil {
log.Warn().Err(err).Msg("[rtsp] accept")
return return
} }
if err = conn.Handle(); err != nil { log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
//log.Warn().Err(err).Msg("[rtsp] handle server")
initMedias(conn)
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
} }
if onDisconnect != nil { closer = func() {
onDisconnect() stream.RemoveConsumer(conn)
} }
log.Debug().Str("stream", name).Msg("[rtsp] disconnect") case rtsp.MethodAnnounce:
name = conn.URL.Path[1:]
stream := streams.Get(name)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream.AddProducer(conn)
closer = func() {
stream.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
} }
}) })
srv.Serve() if err := conn.Accept(); err != nil {
log.Warn().Err(err).Caller().Send()
_ = conn.Close()
return
}
for _, handler := range handlers {
if handler(conn) {
return
}
}
if closer != nil {
if err := conn.Handle(); err != nil {
log.Debug().Err(err).Caller().Send()
}
closer()
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
}
_ = conn.Close()
} }
func initMedias(conn *rtsp.Conn) { func initMedias(conn *rtsp.Conn) {
@@ -184,16 +209,27 @@ func initMedias(conn *rtsp.Conn) {
for key, value := range conn.URL.Query() { for key, value := range conn.URL.Query() {
switch key { switch key {
case streamer.KindVideo, streamer.KindAudio: case streamer.KindVideo, streamer.KindAudio:
for _, value := range value { for _, name := range value {
name = strings.ToUpper(name)
// check aliases
switch name {
case "COPY":
name = "" // pass empty codecs list
case "MJPEG":
name = streamer.CodecJPEG
case "AAC":
name = streamer.CodecAAC
}
media := &streamer.Media{ media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly, Kind: key, Direction: streamer.DirectionRecvonly,
} }
switch value { // empty codecs match all codecs
case "", "copy": // pass empty codecs list if name != "" {
default: // empty clock rate and channels match any values
codec := streamer.NewCodec(value) media.Codecs = []*streamer.Codec{{Name: name}}
media.Codecs = append(media.Codecs, codec)
} }
conn.Medias = append(conn.Medias, media) conn.Medias = append(conn.Medias, media)

View File

@@ -4,30 +4,36 @@ import (
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings" "strings"
"sync"
) )
type Handler func(url string) (streamer.Producer, error) type Handler func(url string) (streamer.Producer, error)
var handlers map[string]Handler var handlers = map[string]Handler{}
var handlersMu sync.Mutex
func HandleFunc(scheme string, handler Handler) { func HandleFunc(scheme string, handler Handler) {
if handlers == nil { handlersMu.Lock()
handlers = make(map[string]Handler)
}
handlers[scheme] = handler handlers[scheme] = handler
handlersMu.Unlock()
}
func getHandler(url string) Handler {
i := strings.IndexByte(url, ':')
if i <= 0 { // TODO: i < 4 ?
return nil
}
handlersMu.Lock()
defer handlersMu.Unlock()
return handlers[url[:i]]
} }
func HasProducer(url string) bool { func HasProducer(url string) bool {
i := strings.IndexByte(url, ':') return getHandler(url) != nil
if i <= 0 { // TODO: i < 4 ?
return false
}
return handlers[url[:i]] != nil
} }
func GetProducer(url string) (streamer.Producer, error) { func GetProducer(url string) (streamer.Producer, error) {
i := strings.IndexByte(url, ':') handler := getHandler(url)
handler := handlers[url[:i]]
if handler == nil { if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url) return nil, fmt.Errorf("unsupported scheme: %s", url)
} }

View File

@@ -2,7 +2,9 @@ package streams
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync" "sync"
"time"
) )
type state byte type state byte
@@ -17,25 +19,35 @@ const (
type Producer struct { type Producer struct {
streamer.Element streamer.Element
url string url string
template string
element streamer.Producer element streamer.Producer
tracks []*streamer.Track tracks []*streamer.Track
state state state state
mx sync.Mutex mu sync.Mutex
restart *time.Timer
}
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) GetMedias() []*streamer.Media { func (p *Producer) GetMedias() []*streamer.Media {
p.mx.Lock() p.mu.Lock()
defer p.mx.Unlock() defer p.mu.Unlock()
if p.state == stateNone { if p.state == stateNone {
log.Debug().Str("url", p.url).Msg("[streams] probe producer") log.Debug().Msgf("[streams] probe producer url=%s", p.url)
var err error var err error
p.element, err = GetProducer(p.url) p.element, err = GetProducer(p.url)
if err != nil || p.element == nil { if err != nil || p.element == nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer") log.Error().Err(err).Caller().Send()
return nil return nil
} }
@@ -46,55 +58,124 @@ func (p *Producer) GetMedias() []*streamer.Media {
} }
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
p.mx.Lock() p.mu.Lock()
defer p.mx.Unlock() defer p.mu.Unlock()
if p.state == stateMedias { if p.state == stateNone {
p.state = stateTracks return nil
} }
track := p.element.GetTrack(media, codec) for _, track := range p.tracks {
if track.Codec == codec {
for _, t := range p.tracks {
if track == t {
return track return track
} }
} }
// can't get new tracks after start
if p.state == stateStart {
return nil
}
track := p.element.GetTrack(media, codec)
if track == nil {
return nil
}
p.tracks = append(p.tracks, track) p.tracks = append(p.tracks, track)
if p.state == stateMedias {
p.state = stateTracks
}
return track return track
} }
// internals // internals
func (p *Producer) start() { func (p *Producer) start() {
p.mx.Lock() p.mu.Lock()
defer p.mx.Unlock() defer p.mu.Unlock()
if p.state != stateTracks { if p.state != stateTracks {
return return
} }
log.Debug().Str("url", p.url).Msg("[streams] start producer") log.Debug().Msgf("[streams] start producer url=%s", p.url)
p.state = stateStart p.state = stateStart
go p.element.Start() go func() {
// safe read element while mu locked
if err := p.element.Start(); err != nil {
log.Warn().Err(err).Caller().Send()
}
p.reconnect()
}()
}
func (p *Producer) reconnect() {
p.mu.Lock()
defer p.mu.Unlock()
if p.state != stateStart {
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
return
}
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
var err error
p.element, err = GetProducer(p.url)
if err != nil || p.element == nil {
log.Debug().Err(err).Caller().Send()
// TODO: dynamic timeout
p.restart = time.AfterFunc(30*time.Second, p.reconnect)
return
}
medias := p.element.GetMedias()
// convert all old producer tracks to new tracks
for i, oldTrack := range p.tracks {
// match new element medias with old track codec
for _, media := range medias {
codec := media.MatchCodec(oldTrack.Codec)
if codec == nil {
continue
}
// move sink from old track to new track
newTrack := p.element.GetTrack(media, codec)
newTrack.GetSink(oldTrack)
p.tracks[i] = newTrack
break
}
}
go func() {
if err = p.element.Start(); err != nil {
log.Debug().Err(err).Caller().Send()
}
p.reconnect()
}()
} }
func (p *Producer) stop() { func (p *Producer) stop() {
p.mx.Lock() p.mu.Lock()
log.Debug().Str("url", p.url).Msg("[streams] stop producer") log.Debug().Msgf("[streams] stop producer url=%s", p.url)
if p.element != nil { if p.element != nil {
_ = p.element.Stop() _ = p.element.Stop()
p.element = nil p.element = nil
} else {
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
} }
p.tracks = nil if p.restart != nil {
p.state = stateNone p.restart.Stop()
p.restart = nil
}
p.mx.Unlock() p.state = stateNone
p.tracks = nil
p.mu.Unlock()
} }

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"sync"
) )
type Consumer struct { type Consumer struct {
@@ -14,46 +15,57 @@ type Consumer struct {
type Stream struct { type Stream struct {
producers []*Producer producers []*Producer
consumers []*Consumer consumers []*Consumer
mu sync.Mutex
} }
func NewStream(source interface{}) *Stream { func NewStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) { switch source := source.(type) {
case string: case string:
s := new(Stream)
prod := &Producer{url: source} prod := &Producer{url: source}
s.producers = append(s.producers, prod) s.producers = append(s.producers, prod)
return s
case []interface{}: case []interface{}:
s := new(Stream)
for _, source := range source { for _, source := range source {
prod := &Producer{url: source.(string)} prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod) s.producers = append(s.producers, prod)
} }
return s
case *Stream:
return source
case map[string]interface{}: case map[string]interface{}:
return NewStream(source["url"]) return NewStream(source["url"])
case nil: case nil:
return new(Stream)
default: default:
panic("wrong source type") panic("wrong source type")
} }
}
return s func (s *Stream) SetSource(source string) {
for _, prod := range s.producers {
prod.SetSource(source)
}
} }
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) { func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
ic := len(s.consumers) ic := len(s.consumers)
consumer := &Consumer{element: cons} consumer := &Consumer{element: cons}
var producers []*Producer // matched producers for consumer
// Step 1. Get consumer medias // Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() { for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia). log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer:%d:%d candidate", ic, icc) Msgf("[streams] consumer=%d candidate=%d", ic, icc)
producers: producers:
for ip, prod := range s.producers { for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet) // Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() { for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia). log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer:%d:%d candidate", ip, ipc) Msgf("[streams] producer=%d candidate=%d", ip, ipc)
// Step 3. Match consumer/producer codecs list // Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia) prodCodec := prodMedia.MatchMedia(consMedia)
@@ -72,20 +84,23 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
consTrack := consumer.element.AddTrack(consMedia, prodTrack) consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack) consumer.tracks = append(consumer.tracks, consTrack)
producers = append(producers, prod)
break producers break producers
} }
} }
} }
} }
// can't match tracks for consumer if len(producers) == 0 {
if len(consumer.tracks) == 0 {
return errors.New("couldn't find the matching tracks") return errors.New("couldn't find the matching tracks")
} }
s.mu.Lock()
s.consumers = append(s.consumers, consumer) s.consumers = append(s.consumers, consumer)
s.mu.Unlock()
for _, prod := range s.producers { // there may be duplicates, but that's not a problem
for _, prod := range producers {
prod.start() prod.start()
} }
@@ -93,7 +108,13 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
} }
func (s *Stream) RemoveConsumer(cons streamer.Consumer) { func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
s.mu.Lock()
for i, consumer := range s.consumers { for i, consumer := range s.consumers {
if consumer == nil {
log.Warn().Msgf("empty consumer: %+v\n", s)
continue
}
if consumer.element == cons { if consumer.element == cons {
// remove consumer pads from all producers // remove consumer pads from all producers
for _, track := range consumer.tracks { for _, track := range consumer.tracks {
@@ -106,9 +127,14 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
} }
for _, producer := range s.producers { for _, producer := range s.producers {
if producer == nil {
log.Warn().Msgf("empty producer: %+v\n", s)
continue
}
var sink bool var sink bool
for _, track := range producer.tracks { for _, track := range producer.tracks {
if len(track.Sink) > 0 { if track.HasSink() {
sink = true sink = true
} }
} }
@@ -116,38 +142,44 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
producer.stop() producer.stop()
} }
} }
s.mu.Unlock()
} }
func (s *Stream) AddProducer(prod streamer.Producer) { func (s *Stream) AddProducer(prod streamer.Producer) {
producer := &Producer{element: prod, state: stateTracks} producer := &Producer{element: prod, state: stateTracks}
s.mu.Lock()
s.producers = append(s.producers, producer) s.producers = append(s.producers, producer)
s.mu.Unlock()
} }
func (s *Stream) RemoveProducer(prod streamer.Producer) { func (s *Stream) RemoveProducer(prod streamer.Producer) {
s.mu.Lock()
for i, producer := range s.producers { for i, producer := range s.producers {
if producer.element == prod { if producer.element == prod {
s.removeProducer(i) s.removeProducer(i)
break break
} }
} }
s.mu.Unlock()
} }
func (s *Stream) Active() bool { //func (s *Stream) Active() bool {
if len(s.consumers) > 0 { // if len(s.consumers) > 0 {
return true // return true
} // }
//
for _, prod := range s.producers { // for _, prod := range s.producers {
if prod.element != nil { // if prod.element != nil {
return true // return true
} // }
} // }
//
return false // return false
} //}
func (s *Stream) MarshalJSON() ([]byte, error) { func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{} var v []interface{}
s.mu.Lock()
for _, prod := range s.producers { for _, prod := range s.producers {
if prod.element != nil { if prod.element != nil {
v = append(v, prod.element) v = append(v, prod.element)
@@ -157,6 +189,7 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
// cons.element always not nil // cons.element always not nil
v = append(v, cons.element) v = append(v, cons.element)
} }
s.mu.Unlock()
if len(v) == 0 { if len(v) == 0 {
v = nil v = nil
} }

View File

@@ -24,10 +24,19 @@ func Init() {
} }
} }
func Get(src string) *Stream { func Get(name string) *Stream {
return streams[name]
}
func New(name string, source interface{}) *Stream {
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok { if stream, ok := streams[src]; ok {
return stream return stream
} }
if !HasProducer(src) { if !HasProducer(src) {
@@ -35,17 +44,8 @@ func Get(src string) *Stream {
} }
log.Info().Str("url", src).Msg("[streams] create new stream") log.Info().Str("url", src).Msg("[streams] create new stream")
stream := NewStream(src)
streams[src] = stream
return stream
}
func Has(src string) bool { return New(src, src)
return streams[src] != nil
}
func New(name string, source interface{}) {
streams[name] = NewStream(source)
} }
func Delete(name string) { func Delete(name string) {

View File

@@ -5,7 +5,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"strings"
) )
var candidates []string var candidates []string
@@ -32,15 +31,11 @@ func addCanditates(answer string) (string, error) {
} }
for _, address := range candidates { for _, address := range candidates {
if strings.HasPrefix(address, "stun:") { var err error
ip, err := webrtc.GetPublicIP() address, err = webrtc.LookupIP(address)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("[webrtc] public IP") log.Warn().Err(err).Msg("[webrtc] candidate")
continue continue
}
address = ip.String() + address[4:]
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
} }
cand, err := webrtc.NewCandidate(address) cand, err := webrtc.NewCandidate(address)

View File

@@ -8,7 +8,9 @@ import (
"github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3" pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"io/ioutil"
"net" "net"
"net/http"
) )
func Init() { func Init() {
@@ -55,6 +57,8 @@ func Init() {
api.HandleWS(webrtc.MsgTypeOffer, offerHandler) api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler) api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
api.HandleFunc("api/webrtc", syncHandler)
} }
var Port string var Port string
@@ -69,7 +73,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
return return
} }
log.Debug().Str("src", src).Msg("[webrtc] new consumer") log.Debug().Str("url", src).Msg("[webrtc] new consumer")
var err error var err error
@@ -137,6 +141,32 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
ctx.Consumer = conn ctx.Consumer = conn
} }
func syncHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func ExchangeSDP( func ExchangeSDP(
stream *streams.Stream, offer string, userAgent string, stream *streams.Stream, offer string, userAgent string,
) (answer string, err error) { ) (answer string, err error) {

4
go.mod
View File

@@ -54,8 +54,4 @@ replace (
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1 github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix // RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657
// MSE update
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
// AES_256_CM_HMAC_SHA1_80 support
github.com/pion/srtp/v2 v2.0.10 => github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10
) )

10
go.sum
View File

@@ -1,9 +1,5 @@
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78= github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0= github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0= github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs= github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
@@ -11,6 +7,8 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepch/vdk v0.0.19 h1:r6xYyBTtXEIEh+csO0XHT00sI7xLF+hQFkJE9/go5II=
github.com/deepch/vdk v0.0.19/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
@@ -103,6 +101,8 @@ github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
@@ -121,7 +121,7 @@ github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc= github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4= github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk= github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M= github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

16
main.go
View File

@@ -4,11 +4,14 @@ import (
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug" "github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/echo"
"github.com/AlexxIT/go2rtc/cmd/exec" "github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg" "github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass" "github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/homekit" "github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/mse" "github.com/AlexxIT/go2rtc/cmd/ivideon"
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
"github.com/AlexxIT/go2rtc/cmd/mp4"
"github.com/AlexxIT/go2rtc/cmd/ngrok" "github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp" "github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/rtsp"
@@ -24,20 +27,25 @@ func main() {
app.Init() // init config and logs app.Init() // init config and logs
streams.Init() // load streams list streams.Init() // load streams list
api.Init() // init HTTP API server
echo.Init()
rtsp.Init() // add support RTSP client and RTSP server rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server) exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme) ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init() webrtc.Init()
mse.Init() mp4.Init()
mjpeg.Init()
srtp.Init() srtp.Init()
homekit.Init() homekit.Init()
ivideon.Init()
ngrok.Init() ngrok.Init()
debug.Init() debug.Init()

5
pkg/README.md Normal file
View File

@@ -0,0 +1,5 @@
## Useful links
- https://www.wowza.com/blog/streaming-protocols
- https://vimeo.com/blog/post/rtmp-stream/
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a

57
pkg/aac/rtp.go Normal file
View File

@@ -0,0 +1,57 @@
package aac
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
const RTPPacketVersionAAC = 0
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = packet.Payload[2+headersSize:]
return push(&clone)
}
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAAC {
return push(packet)
}
// support ONLY one unit in payload
size := uint16(len(packet.Payload))
// 2 bytes header size + 2 bytes first payload size
payload := make([]byte, 2+2+size)
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], size<<3)
copy(payload[4:], packet.Payload)
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
return push(&clone)
}
}
}

View File

@@ -1,3 +1,22 @@
# H264
Access Unit (AU) can contain one or multiple NAL Unit:
1. [SEI,] SPS, PPS, IFrame, [IFrame...]
2. BFrame, [BFrame...]
3. IFrame, [IFrame...]
## RTP H264
Camera | NALu
-------|-----
EZVIZ C3S | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 1t, 1t, 1t
Sonoff GK-200MP2-B | 28:28:28 -> 5t, 1t, 1t, 1t
Dahua IPC-K42 | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg copy | 5t, 1t, 1t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg h264 | 24 -> 6:5:5:5:5t, 24 -> 1:1:1:1t, 28:28:28 -> 5f, 28:28:28 -> 5f, 28:28:28 -> 5t
FFmpeg resize | 6f, 28:28:28 -> 5f, 28... -> 5t, 24 -> 1:1f, 24 -> 1:1t
## WebRTC ## WebRTC
Video codec | Media string | Device Video codec | Media string | Device
@@ -25,3 +44,4 @@ H.264/high | avc1.6400xx | FFmpeg superfast
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels) - [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter) - [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media) - [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)
- [Two stream formats, Annex-B, AVCC (H.264) and HVCC (H.265)](https://www.programmersought.com/article/3901815022/)

View File

@@ -6,55 +6,71 @@ import (
"github.com/pion/rtp" "github.com/pion/rtp"
) )
const PayloadTypeAVC = 255 func EncodeAVC(nals ...[]byte) (avc []byte) {
var i, n int
func IsAVC(codec *streamer.Codec) bool { for _, nal := range nals {
return codec.PayloadType == PayloadTypeAVC if i = len(nal); i > 0 {
} n += 4 + i
}
}
avc = make([]byte, n)
n = 0
for _, nal := range nals {
if i = len(nal); i > 0 {
binary.BigEndian.PutUint32(avc[n:], uint32(i))
n += 4 + copy(avc[n+4:], nal)
}
}
func EncodeAVC(raw []byte) (avc []byte) {
avc = make([]byte, len(raw)+4)
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
copy(avc[4:], raw)
return return
} }
func RepairAVC(track *streamer.Track) streamer.WrapperFunc { func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
sps, pps := GetParameterSet(track.Codec.FmtpLine) sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps) ps := EncodeAVC(sps, pps)
pps = EncodeAVC(pps)
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) (err error) { return func(packet *rtp.Packet) (err error) {
naluType := NALUType(packet.Payload) if NALUType(packet.Payload) == NALUTypeIFrame {
switch naluType { packet.Payload = Join(ps, packet.Payload)
case NALUTypeSPS:
sps = packet.Payload
return
case NALUTypePPS:
pps = packet.Payload
return
} }
return push(packet)
var clone rtp.Packet
if naluType == NALUTypeIFrame {
clone = *packet
clone.Payload = sps
if err = push(&clone); err != nil {
return
}
clone = *packet
clone.Payload = pps
if err = push(&clone); err != nil {
return
}
}
clone = *packet
clone.Payload = packet.Payload
return push(&clone)
} }
} }
} }
func SplitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data)) + 4
// check if multiple items in one packet
if size < len(data) {
nals = append(nals, data[:size])
data = data[size:]
} else {
nals = append(nals, data)
break
}
}
return nals
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
}

View File

@@ -2,21 +2,58 @@ package h264
import ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings" "strings"
) )
const ( const (
NALUTypePFrame = 1 NALUTypePFrame = 1 // Coded slice of a non-IDR picture
NALUTypeIFrame = 5 NALUTypeIFrame = 5 // Coded slice of an IDR picture
NALUTypeSPS = 7 NALUTypeSEI = 6 // Supplemental enhancement information (SEI)
NALUTypePPS = 8 NALUTypeSPS = 7 // Sequence parameter set
NALUTypePPS = 8 // Picture parameter set
NALUTypeAUD = 9 // Access unit delimiter
) )
func NALUType(b []byte) byte { func NALUType(b []byte) byte {
return b[4] & 0x1F return b[4] & 0x1F
} }
// IsKeyframe - check if any NALU in one AU is Keyframe
func IsKeyframe(b []byte) bool {
for {
switch NALUType(b) {
case NALUTypePFrame:
return false
case NALUTypeIFrame:
return true
}
size := int(binary.BigEndian.Uint32(b)) + 4
if size < len(b) {
b = b[size:]
continue
} else {
return false
}
}
}
func Join(ps, iframe []byte) []byte {
b := make([]byte, len(ps)+len(iframe))
i := copy(b, ps)
copy(b[i:], iframe)
return b
}
func GetProfileLevelID(fmtp string) string {
if fmtp == "" {
return ""
}
return streamer.Between(fmtp, "profile-level-id=", ";")
}
func GetParameterSet(fmtp string) (sps, pps []byte) { func GetParameterSet(fmtp string) (sps, pps []byte) {
if fmtp == "" { if fmtp == "" {
return return

View File

@@ -13,90 +13,69 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
depack := &codecs.H264Packet{IsAVC: true} depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(track.Codec.FmtpLine) sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps) ps := EncodeAVC(sps, pps)
pps = EncodeAVC(pps)
var buffer []byte buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error { return func(packet *rtp.Packet) error {
//nalUnitType := packet.Payload[0] & 0x1F //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC,
//)
// NALu packets can be split in different ways: payload, err := depack.Unmarshal(packet.Payload)
// - single type 7 and type 8 packets if len(payload) == 0 || err != nil {
// - join type 7 and type 8 packet (type 24)
// - split type 5 on multiple 28 packets
// - split type 5 on multiple separate 28 packets
units, err := depack.Unmarshal(packet.Payload)
if len(units) == 0 || err != nil {
return nil return nil
} }
for { // Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
i := int(binary.BigEndian.Uint32(units)) + 4 if packet.Marker {
unitAVC := units[:i] switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
unitType := NALUType(unitAVC) buf = append(buf, payload...)
switch unitType {
case NALUTypeSPS:
//println("new SPS")
sps = unitAVC
return nil
case NALUTypePPS:
//println("new PPS")
pps = unitAVC
return nil return nil
} }
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if !packet.Marker {
buffer = append(buffer, unitAVC...)
return nil
}
if buffer != nil {
buffer = append(buffer, unitAVC...)
unitAVC = buffer
buffer = nil
}
var clone rtp.Packet
if unitType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = unitAVC
if err = push(&clone); err != nil {
return err
}
if len(units) == i {
return nil
}
units = units[i:]
} }
if len(buf) == 0 {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return nil
}
payload = payload[i:]
if NALUType(payload) == NALUTypeIFrame {
buf = append(buf, ps...)
}
}
}
// collect all NALs for Access Unit
if !packet.Marker {
buf = append(buf, payload...)
return nil
}
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
}
//log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
clone := *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = payload
return push(&clone)
} }
} }
} }
@@ -108,28 +87,28 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error { return func(packet *rtp.Packet) error {
if packet.Version == RTPPacketVersionAVC { if packet.Version != RTPPacketVersionAVC {
payloads := payloader.Payload(mtu, packet.Payload) return push(packet)
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == len(payloads)-1,
//PayloadType: packet.PayloadType,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
//SSRC: packet.SSRC,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
}
return nil
} }
return push(packet) payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == last,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
}
return nil
} }
} }
} }

3
pkg/h265/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://datatracker.ietf.org/doc/html/rfc7798

35
pkg/h265/helper.go Normal file
View File

@@ -0,0 +1,35 @@
package h265
import (
"encoding/base64"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
const (
NALUnitTypeIFrame = 19
)
func NALUnitType(b []byte) byte {
return b[4] >> 1
}
func IsKeyframe(b []byte) bool {
return NALUnitType(b) == NALUnitTypeIFrame
}
func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
if fmtp == "" {
return
}
s := streamer.Between(fmtp, "sprop-vps=", ";")
vps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-sps=", ";")
sps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-pps=", ";")
pps, _ = base64.StdEncoding.DecodeString(s)
return
}

153
pkg/h265/rtp.go Normal file
View File

@@ -0,0 +1,153 @@
package h265
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h265parser"
"github.com/pion/rtp"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
var buffer []byte
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
nut := (packet.Payload[0] >> 1) & 0x3f
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
// track.Codec.Name, nut, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
//)
switch nut {
case h265parser.NAL_UNIT_UNSPECIFIED_49:
data := packet.Payload
switch data[2] >> 6 {
case 2: // begin
buffer = []byte{
(data[0] & 0x81) | (data[2] & 0x3f << 1), data[1],
}
buffer = append(buffer, data[3:]...)
return nil
case 0: // continue
buffer = append(buffer, data[3:]...)
return nil
case 1: // end
packet.Payload = append(buffer, data[3:]...)
}
case h265parser.NAL_UNIT_VPS:
vps = packet.Payload
return nil
case h265parser.NAL_UNIT_SPS:
sps = packet.Payload
return nil
case h265parser.NAL_UNIT_PPS:
pps = packet.Payload
return nil
default:
//panic("not implemented")
}
var clone rtp.Packet
nut = (packet.Payload[0] >> 1) & 0x3f
if nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA {
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(vps)
if err := push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(sps)
if err := push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(pps)
if err := push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(packet.Payload)
return push(&clone)
}
}
}
// SafariPay - generate Safari friendly payload for H265
func SafariPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer()
size := int(mtu - 12) // rtp.Header size
var buffer []byte
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return push(packet)
}
data := packet.Payload
data[0] = 0
data[1] = 0
data[2] = 0
data[3] = 1
var start byte
nut := (data[4] >> 1) & 0b111111
//fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20])
switch {
case nut >= h265parser.NAL_UNIT_VPS && nut <= h265parser.NAL_UNIT_PPS:
buffer = append(buffer, data...)
return nil
case nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA:
buffer = append([]byte{3}, buffer...)
data = append(buffer, data...)
start = 1
default:
data = append([]byte{2}, data...)
start = 0
}
for len(data) > size {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: false,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: data[:size],
}
if err := push(&clone); err != nil {
return err
}
data = append([]byte{start}, data[size:]...)
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: data,
}
return push(&clone)
}
}
}

3
pkg/httpflv/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779

100
pkg/httpflv/httpflv.go Normal file
View File

@@ -0,0 +1,100 @@
package httpflv
import (
"bufio"
"errors"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
"io"
"net/http"
)
func Dial(uri string) (*Conn, error) {
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
c := Conn{
conn: res.Body,
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
buf: make([]byte, 256),
}
if _, err = io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
return nil, err
}
flags, n, err := flvio.ParseFileHeader(c.buf)
if err != nil {
return nil, err
}
if flags&flvio.FILE_HAS_VIDEO == 0 {
return nil, errors.New("not supported")
}
if _, err = c.reader.Discard(n); err != nil {
return nil, err
}
return &c, nil
}
type Conn struct {
conn io.ReadCloser
reader *bufio.Reader
buf []byte
}
func (c *Conn) Streams() ([]av.CodecData, error) {
for {
tag, _, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return nil, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
continue
}
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
if err != nil {
return nil, err
}
return []av.CodecData{stream}, nil
}
}
func (c *Conn) ReadPacket() (av.Packet, error) {
for {
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return av.Packet{}, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
continue
}
return av.Packet{
Idx: 0,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
}
}
func (c *Conn) Close() (err error) {
return c.conn.Close()
}

286
pkg/ivideon/client.go Normal file
View File

@@ -0,0 +1,286 @@
package ivideon
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/gorilla/websocket"
"github.com/pion/rtp"
"io"
"net/http"
"strings"
"time"
)
type Client struct {
streamer.Element
ID string
conn *websocket.Conn
medias []*streamer.Media
tracks map[byte]*streamer.Track
closed bool
msg *message
t0 time.Time
buffer chan []byte
}
func NewClient(id string) *Client {
return &Client{ID: id}
}
func (c *Client) Dial() (err error) {
resp, err := http.Get(
"https://openapi-alpha.ivideon.com/cameras/" + c.ID +
"/live_stream?op=GET&access_token=public&q=2&" +
"video_codecs=h264&format=ws-fmp4",
)
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var v liveResponse
if err = json.Unmarshal(data, &v); err != nil {
return err
}
if !v.Success {
return fmt.Errorf("wrong response: %s", data)
}
c.conn, _, err = websocket.DefaultDialer.Dial(v.Result.URL, nil)
if err != nil {
return err
}
if err = c.getTracks(); err != nil {
_ = c.conn.Close()
return err
}
return nil
}
func (c *Client) Handle() error {
c.buffer = make(chan []byte, 5)
// add delay to the stream for smooth playing (not a best solution)
c.t0 = time.Now().Add(time.Second)
// processing stream in separate thread for lower delay between packets
go c.worker()
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
track := c.tracks[c.msg.Track]
if track != nil {
c.buffer <- data
}
// we have one unprocessed msg after getTracks
for {
_, data, err = c.conn.ReadMessage()
if err != nil {
return err
}
var msg message
if err = json.Unmarshal(data, &msg); err != nil {
return err
}
switch msg.Type {
case "stream-init":
continue
case "fragment":
_, data, err = c.conn.ReadMessage()
if err != nil {
return err
}
track = c.tracks[msg.Track]
if track != nil {
c.buffer <- data
}
default:
return fmt.Errorf("wrong message type: %s", data)
}
}
}
func (c *Client) Close() error {
if c.conn == nil {
return nil
}
close(c.buffer)
c.closed = true
return c.conn.Close()
}
func (c *Client) getTracks() error {
c.tracks = map[byte]*streamer.Track{}
for {
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
var msg message
if err = json.Unmarshal(data, &msg); err != nil {
return err
}
switch msg.Type {
case "stream-init":
s := msg.CodecString
i := strings.IndexByte(s, '.')
if i > 0 {
s = s[:i]
}
switch s {
case "avc1": // avc1.4d0029
// skip multiple identical init
if c.tracks[msg.TrackID] != nil {
continue
}
codec := &streamer.Codec{
Name: streamer.CodecH264,
ClockRate: 90000,
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
PayloadType: streamer.PayloadTypeMP4,
}
i = bytes.Index(msg.Data, []byte("avcC")) - 4
if i < 0 {
return fmt.Errorf("wrong AVC: %s", msg.Data)
}
avccLen := binary.BigEndian.Uint32(msg.Data[i:])
data = msg.Data[i+8 : i+int(avccLen)]
record := h264parser.AVCDecoderConfRecord{}
if _, err = record.Unmarshal(data); err != nil {
return err
}
codec.FmtpLine += ";sprop-parameter-sets=" +
base64.StdEncoding.EncodeToString(record.SPS[0]) + "," +
base64.StdEncoding.EncodeToString(record.PPS[0])
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
c.medias = append(c.medias, media)
track := &streamer.Track{
Direction: streamer.DirectionSendonly,
Codec: codec,
}
c.tracks[msg.TrackID] = track
case "mp4a": // mp4a.40.2
}
case "fragment":
c.msg = &msg
return nil
default:
return fmt.Errorf("wrong message type: %s", data)
}
}
}
func (c *Client) worker() {
var track *streamer.Track
for _, track = range c.tracks {
break
}
for data := range c.buffer {
moof := &fmp4io.MovieFrag{}
if _, err := moof.Unmarshal(data, 0); err != nil {
continue
}
moofLen := binary.BigEndian.Uint32(data)
_ = moofLen
mdat := moof.Unknowns[0]
if mdat.Tag() != fmp4io.MDAT {
continue
}
i, _ := mdat.Pos() // offset, size
data = data[i+8:]
traf := moof.Tracks[0]
ts := uint32(traf.DecodeTime.Time)
//println("!!!", (time.Duration(ts) * time.Millisecond).String(), time.Since(c.t0).String())
for _, entry := range traf.Run.Entries {
// synchronize framerate for WebRTC and MSE
d := time.Duration(ts)*time.Millisecond - time.Since(c.t0)
if d < 0 {
d = time.Duration(entry.Duration) * time.Millisecond / 2
}
time.Sleep(d)
// can be SPS, PPS and IFrame in one packet
packet := &rtp.Packet{
// ivideon clockrate=1000, RTP clockrate=90000
Header: rtp.Header{Timestamp: ts * 90},
Payload: data[:entry.Size],
}
_ = track.WriteRTP(packet)
data = data[entry.Size:]
ts += entry.Duration
}
if len(data) != 0 {
continue
}
}
}
type liveResponse struct {
Result struct {
URL string `json:"url"`
} `json:"result"`
Success bool `json:"success"`
}
type message struct {
Type string `json:"type"`
CodecString string `json:"codec_string"`
Data []byte `json:"data"`
TrackID byte `json:"track_id"`
Track byte `json:"track"`
StartTime float32 `json:"start_time"`
Duration float32 `json:"duration"`
IsKey bool `json:"is_key"`
DataOffset uint32 `json:"data_offset"`
}

31
pkg/ivideon/streamer.go Normal file
View File

@@ -0,0 +1,31 @@
package ivideon
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
}
func (c *Client) Start() error {
err := c.Handle()
if c.closed {
return nil
}
return err
}
func (c *Client) Stop() error {
return c.Close()
}

View File

@@ -1,72 +0,0 @@
package keyframe
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
var annexB = []byte{0, 0, 0, 1}
type Consumer struct {
streamer.Element
IsMP4 bool
}
func (k *Consumer) GetMedias() []*streamer.Media {
// support keyframe extraction only for one coded...
codec := streamer.NewCodec(streamer.CodecH264)
return []*streamer.Media{
{
Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{codec},
},
}
}
func (k *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
// sps and pps without AVC headers
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
push := func(packet *rtp.Packet) error {
// TODO: remove it, unnecessary
if packet.Version != h264.RTPPacketVersionAVC {
panic("wrong packet type")
}
switch h264.NALUType(packet.Payload) {
case h264.NALUTypeSPS:
sps = packet.Payload[4:] // remove AVC header
case h264.NALUTypePPS:
pps = packet.Payload[4:] // remove AVC header
case h264.NALUTypeIFrame:
if sps == nil || pps == nil {
return nil
}
var data []byte
if k.IsMP4 {
data = mp4.MarshalMP4(sps, pps, packet.Payload)
} else {
data = append(data, annexB...)
data = append(data, sps...)
data = append(data, annexB...)
data = append(data, pps...)
data = append(data, annexB...)
data = append(data, packet.Payload[4:]...)
}
k.Fire(data)
}
return nil
}
if !h264.IsAVC(track.Codec) {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}

4
pkg/mjpeg/README.md Normal file
View File

@@ -0,0 +1,4 @@
## Useful links
- https://www.rfc-editor.org/rfc/rfc2435
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c

95
pkg/mjpeg/consumer.go Normal file
View File

@@ -0,0 +1,95 @@
package mjpeg
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
codecs []*streamer.Codec
start bool
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}},
}}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
var header, payload []byte
push := func(packet *rtp.Packet) error {
//fmt.Printf(
// "[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\n",
// track.Codec.Name, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
//)
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
b := packet.Payload
// 3.1. JPEG header
t := b[4]
// 3.1.7. Restart Marker header
if 64 <= t && t <= 127 {
b = b[12:] // skip it
} else {
b = b[8:]
}
if header == nil {
var lqt, cqt []byte
// 3.1.8. Quantization Table header
q := packet.Payload[5]
if q >= 128 {
lqt = b[4:68]
cqt = b[68:132]
b = b[132:]
} else {
lqt, cqt = MakeTables(q)
}
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
// The maximum width is 2040 pixels.
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
// fix 2560x1920 and 2560x1440
if w == 512 && (h == 1920 || h == 1440) {
w = 2560
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
header = MakeHeaders(t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
payload = append(payload, b...)
if packet.Marker {
b = append(header, payload...)
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
b = append(b, 0xFF, 0xD9)
}
c.Fire(b)
header = nil
payload = nil
}
return nil
}
return track.Bind(push)
}

182
pkg/mjpeg/rfc2435.go Normal file
View File

@@ -0,0 +1,182 @@
package mjpeg
// RFC 2435. Appendix A
var jpeg_luma_quantizer = []byte{
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99,
}
var jpeg_chroma_quantizer = []byte{
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
}
func MakeTables(q byte) (lqt, cqt []byte) {
var factor int
switch {
case q < 1:
factor = 1
case q > 99:
factor = 99
default:
factor = int(q)
}
if q < 50 {
factor = 5000 / factor
} else if q > 99 {
factor = 200 - factor*2
}
lqt = make([]byte, 64)
cqt = make([]byte, 64)
for i := 0; i < 64; i++ {
lq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100
cq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100
/* Limit the quantizers to 1 <= q <= 255 */
switch {
case lq < 1:
lqt[i] = 1
case lq > 255:
lqt[i] = 255
default:
lqt[i] = byte(lq)
}
switch {
case cq < 1:
cqt[i] = 1
case cq > 255:
cqt[i] = 255
default:
cqt[i] = byte(cq)
}
}
return
}
// RFC 2435. Appendix B
var lum_dc_codelens = []byte{
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
}
var lum_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var lum_ac_codelens = []byte{
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
}
var lum_ac_symbols = []byte{
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
var chm_dc_codelens = []byte{
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
}
var chm_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var chm_ac_codelens = []byte{
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
}
var chm_ac_symbols = []byte{
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
func MakeHeaders(t byte, w, h uint16, lqt, cqt []byte) []byte {
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
p := []byte{0xFF, 0xD8}
p = MakeQuantHeader(p, lqt, 0)
p = MakeQuantHeader(p, cqt, 1)
if t == 0 {
t = 0x21
} else {
t = 0x22
}
p = append(p,
0xFF, 0xC0, 0, 17, 8,
byte(h>>8), byte(h&0xFF),
byte(w>>8), byte(w&0xFF),
3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1,
)
p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0)
}
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
p = append(p, 0xFF, 0xDB, 0, 67, tableNo)
return append(p, qt...)
}
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
p = append(p,
0xFF, 0xC4, 0,
byte(3+len(codelens)+len(symbols)),
(tableClass<<4)|tableNo,
)
p = append(p, codelens...)
return append(p, symbols...)
}

23
pkg/mp4/README.md Normal file
View File

@@ -0,0 +1,23 @@
## HEVC
Browser | avc1 | hvc1 | hev1
------------|------|------|---
Mac Chrome | + | - | +
Mac Safari | + | + | -
iOS 15? | + | + | -
Mac Firefox | + | - | -
iOS 12 | + | - | -
Android 13 | + | - | -
```
ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4
Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
```
## Useful links
- https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1
- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
- https://jellyfin.org/docs/general/clients/codec-support.html
- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding

100
pkg/mp4/const.go Normal file
View File

@@ -0,0 +1,100 @@
package mp4
import (
"encoding/binary"
"github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"time"
)
var matrix = [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000}
var time0 = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
func FTYP() []byte {
b := make([]byte, 0x18)
binary.BigEndian.PutUint32(b, 0x18)
copy(b[0x04:], "ftyp")
copy(b[0x08:], "iso5")
copy(b[0x10:], "iso5")
copy(b[0x14:], "avc1")
return b
}
func MOOV() *mp4io.Movie {
return &mp4io.Movie{
Header: &mp4io.MovieHeader{
PreferredRate: 1,
PreferredVolume: 1,
Matrix: matrix,
NextTrackId: -1,
Duration: 0,
TimeScale: 1000,
CreateTime: time0,
ModifyTime: time0,
PreviewTime: time0,
PreviewDuration: time0,
PosterTime: time0,
SelectionTime: time0,
SelectionDuration: time0,
CurrentTime: time0,
},
MovieExtend: &mp4io.MovieExtend{},
}
}
func TRAK(id int) *mp4io.Track {
return &mp4io.Track{
// trak > tkhd
Header: &mp4io.TrackHeader{
TrackId: int32(id),
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
Duration: 0, // OK
Matrix: matrix,
CreateTime: time0,
ModifyTime: time0,
},
// trak > mdia
Media: &mp4io.Media{
// trak > mdia > mdhd
Header: &mp4io.MediaHeader{
TimeScale: 1000,
Duration: 0,
Language: 0x55C4,
CreateTime: time0,
ModifyTime: time0,
},
// trak > mdia > minf
Info: &mp4io.MediaInfo{
// trak > mdia > minf > dinf
Data: &mp4io.DataInfo{
Refer: &mp4io.DataRefer{
Url: &mp4io.DataReferUrl{
Flags: 0x000001, // self reference
},
},
},
// trak > mdia > minf > stbl
Sample: &mp4io.SampleTable{
SampleDesc: &mp4io.SampleDesc{},
TimeToSample: &mp4io.TimeToSample{},
SampleToChunk: &mp4io.SampleToChunk{},
SampleSize: &mp4io.SampleSize{},
ChunkOffset: &mp4io.ChunkOffset{},
},
},
},
}
}
func ESDS(conf []byte) *mp4f.FDummy {
esds := &mp4fio.ElemStreamDesc{DecConfig: conf}
b := make([]byte, esds.Len())
esds.Marshal(b)
return &mp4f.FDummy{
Data: b,
Tag_: mp4io.Tag(uint32(mp4io.ESDS)),
}
}

150
pkg/mp4/consumer.go Normal file
View File

@@ -0,0 +1,150 @@
package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
muxer *Muxer
codecs []*streamer.Codec
start bool
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
trackID := byte(len(c.codecs))
c.codecs = append(c.codecs, track.Codec)
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
var wrapper streamer.WrapperFunc
if codec.IsMP4() {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
if !codec.IsMP4() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
if !c.start {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
if !codec.IsMP4() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs)
}
func (c *Consumer) Init() ([]byte, error) {
c.muxer = &Muxer{}
return c.muxer.GetInit(c.codecs)
}
func (c *Consumer) Start() {
c.start = true
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MP4 server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
}
return json.Marshal(v)
}

View File

@@ -1,47 +0,0 @@
package mp4
import (
"errors"
"io"
)
type MemoryWriter struct {
buf []byte
pos int
}
func (m *MemoryWriter) Write(p []byte) (n int, err error) {
minCap := m.pos + len(p)
if minCap > cap(m.buf) { // Make sure buf has enough capacity:
buf2 := make([]byte, len(m.buf), minCap+len(p)) // add some extra
copy(buf2, m.buf)
m.buf = buf2
}
if minCap > len(m.buf) {
m.buf = m.buf[:minCap]
}
copy(m.buf[m.pos:], p)
m.pos += len(p)
return len(p), nil
}
func (m *MemoryWriter) Seek(offset int64, whence int) (int64, error) {
newPos, offs := 0, int(offset)
switch whence {
case io.SeekStart:
newPos = offs
case io.SeekCurrent:
newPos = m.pos + offs
case io.SeekEnd:
newPos = len(m.buf) + offs
}
if newPos < 0 {
return 0, errors.New("negative result pos")
}
m.pos = newPos
return int64(newPos), nil
}
func (m *MemoryWriter) Bytes() []byte {
return m.buf
}

85
pkg/mp4/keyframe.go Normal file
View File

@@ -0,0 +1,85 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
type Keyframe struct {
streamer.Element
MimeType string
}
func (c *Keyframe) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
}
}
func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
muxer := &Muxer{}
codecs := []*streamer.Codec{track.Codec}
init, err := muxer.GetInit(codecs)
if err != nil {
return nil
}
c.MimeType = muxer.MimeType(codecs)
switch track.Codec.Name {
case streamer.CodecH264:
push := func(packet *rtp.Packet) error {
if !h264.IsKeyframe(packet.Payload) {
return nil
}
buf := muxer.Marshal(0, packet)
c.Fire(append(init, buf...))
return nil
}
var wrapper streamer.WrapperFunc
if track.Codec.IsMP4() {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
push := func(packet *rtp.Packet) error {
if !h265.IsKeyframe(packet.Payload) {
return nil
}
buf := muxer.Marshal(0, packet)
c.Fire(append(init, buf...))
return nil
}
if !track.Codec.IsMP4() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}

View File

@@ -1,37 +1,257 @@
package mp4 package mp4
import ( import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av" "github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/mp4" "github.com/deepch/vdk/codec/h265parser"
"time" "github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"github.com/pion/rtp"
) )
func MarshalMP4(sps, pps, frame []byte) []byte { type Muxer struct {
writer := &MemoryWriter{} fragIndex uint32
muxer := mp4.NewMuxer(writer) dts []uint64
pts []uint32
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) //data []byte
if err != nil { //total int
panic(err) }
}
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
if err = muxer.WriteHeader([]av.CodecData{stream}); err != nil { s := `video/mp4; codecs="`
panic(err)
} for i, codec := range codecs {
if i > 0 {
pkt := av.Packet{ s += ","
CompositionTime: time.Millisecond, }
IsKeyFrame: true,
Duration: time.Second, switch codec.Name {
Data: frame, case streamer.CodecH264:
} s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
if err = muxer.WritePacket(pkt); err != nil { case streamer.CodecH265:
panic(err) // +Safari +Chrome +Edge -iOS15 -Android13
} s += "hvc1.1.6.L93.B0" // hev1.1.6.L93.B0
if err = muxer.WriteTrailer(); err != nil { case streamer.CodecAAC:
panic(err) s += "mp4a.40.2"
} }
}
return writer.buf
return s + `"`
}
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov := MOOV()
for i, codec := range codecs {
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
pps = []byte{0x68, 0xce, 0x38, 0x80}
}
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.AVC1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
return nil, fmt.Errorf("empty SPS: %#v", codec)
}
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.HV1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil, err
}
codecData, err := aacparser.ParseMPEG4AudioConfigBytes(b)
if err != nil {
return nil, err
}
trak := TRAK(i + 1)
trak.Header.AlternateGroup = 1
trak.Header.Duration = 0
trak.Header.Volume = 1
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'s', 'o', 'u', 'n'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
DataRefIdx: 1,
NumberOfChannels: int16(codecData.ChannelLayout.Count()),
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
SampleRate: float64(codecData.SampleRate),
Unknowns: []mp4io.Atom{ESDS(b)},
}
moov.Tracks = append(moov.Tracks, trak)
}
trex := &mp4io.TrackExtend{
TrackId: uint32(i + 1),
DefaultSampleDescIdx: 1,
DefaultSampleDuration: 0,
}
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
m.pts = append(m.pts, 0)
m.dts = append(m.dts, 0)
}
data := make([]byte, moov.Len())
moov.Marshal(data)
return append(FTYP(), data...), nil
}
//func (m *Muxer) Rewind() {
// m.dts = 0
// m.pts = 0
//}
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
run := &mp4fio.TrackFragRun{
Flags: 0x000b05,
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
DataOffset: 0,
Entries: []mp4io.TrackFragRunEntry{},
}
moof := &mp4fio.MovieFrag{
Header: &mp4fio.MovieFragHeader{
Seqnum: m.fragIndex + 1,
},
Tracks: []*mp4fio.TrackFrag{
{
Header: &mp4fio.TrackFragHeader{
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
},
DecodeTime: &mp4fio.TrackFragDecodeTime{
Version: 1,
Flags: 0,
Time: m.dts[trackID],
},
Run: run,
},
},
}
entry := mp4io.TrackFragRunEntry{
//Duration: 90000,
Size: uint32(len(packet.Payload)),
}
newTime := packet.Timestamp
if m.pts[trackID] > 0 {
//m.dts += uint64(newTime - m.pts)
entry.Duration = newTime - m.pts[trackID]
m.dts[trackID] += uint64(entry.Duration)
}
m.pts[trackID] = newTime
// important before moof.Len()
run.Entries = append(run.Entries, entry)
moofLen := moof.Len()
mdatLen := 8 + len(packet.Payload)
// important after moof.Len()
run.DataOffset = uint32(moofLen + 8)
buf := make([]byte, moofLen+mdatLen)
moof.Marshal(buf)
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
copy(buf[moofLen+4:], "mdat")
copy(buf[moofLen+8:], packet.Payload)
m.fragIndex++
//m.total += moofLen + mdatLen
return buf
} }

View File

@@ -1,27 +1,27 @@
package mse package mp4f
import ( import (
"encoding/json" "encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av" "github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/mp4f" "github.com/deepch/vdk/format/mp4f"
"github.com/pion/rtp" "github.com/pion/rtp"
"time" "time"
) )
const MsgTypeMSE = "mse"
type Consumer struct { type Consumer struct {
streamer.Element streamer.Element
UserAgent string UserAgent string
RemoteAddr string RemoteAddr string
muxer *mp4f.Muxer muxer *mp4f.Muxer
streams []av.CodecData streams []av.CodecData
start bool mimeType string
start bool
send int send int
} }
@@ -34,7 +34,8 @@ func (c *Consumer) GetMedias() []*streamer.Media {
Codecs: []*streamer.Codec{ Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000}, {Name: streamer.CodecH264, ClockRate: 90000},
}, },
}, { },
{
Kind: streamer.KindAudio, Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly, Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{ Codecs: []*streamer.Codec{
@@ -46,18 +47,20 @@ func (c *Consumer) GetMedias() []*streamer.Media {
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
codec := track.Codec codec := track.Codec
trackID := int8(len(c.streams))
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case streamer.CodecH264:
idx := int8(len(c.streams))
sps, pps := h264.GetParameterSet(codec.FmtpLine) sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil { if err != nil {
return nil return nil
} }
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
c.streams = append(c.streams, stream) c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: idx, CompositionTime: time.Millisecond} pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate) ts2time := time.Second / time.Duration(codec.ClockRate)
@@ -66,15 +69,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return nil return nil
} }
switch h264.NALUType(packet.Payload) { if !c.start {
case h264.NALUTypeIFrame:
c.start = true
pkt.IsKeyFrame = true
case h264.NALUTypePFrame:
if !c.start {
return nil
}
default:
return nil return nil
} }
@@ -85,7 +80,8 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
} }
pkt.Time = newTime pkt.Time = newTime
for _, buf := range c.muxer.WritePacketV5(pkt) { ready, buf, _ := c.muxer.WritePacket(pkt, false)
if ready {
c.send += len(buf) c.send += len(buf)
c.Fire(buf) c.Fire(buf)
} }
@@ -98,23 +94,60 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
push = wrapper(push) push = wrapper(push)
} }
return track.Bind(push)
case streamer.CodecAAC:
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
c.mimeType += ",mp4a.40.2"
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
ready, buf, _ := c.muxer.WritePacket(pkt, false)
if ready {
c.send += len(buf)
c.Fire(buf)
}
return nil
}
return track.Bind(push) return track.Bind(push)
} }
panic("unsupported codec") panic("unsupported codec")
} }
func (c *Consumer) Init() { func (c *Consumer) MimeType() string {
return `video/mp4; codecs="` + c.mimeType + `"`
}
func (c *Consumer) Init() ([]byte, error) {
c.muxer = mp4f.NewMuxer(nil) c.muxer = mp4f.NewMuxer(nil)
if err := c.muxer.WriteHeader(c.streams); err != nil { if err := c.muxer.WriteHeader(c.streams); err != nil {
return return nil, err
} }
_, data := c.muxer.GetInit(c.streams)
return data, nil
}
codecs, buf := c.muxer.GetInit(c.streams) func (c *Consumer) Start() {
c.Fire(&streamer.Message{Type: MsgTypeMSE, Value: codecs}) c.start = true
c.send += len(buf)
c.Fire(buf)
} }
// //

View File

@@ -2,19 +2,26 @@ package rtmp
import ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/httpflv"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av" "github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtmp" "github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp" "github.com/pion/rtp"
"strings"
"time" "time"
) )
// Conn for RTMP and RTMPT (flv over HTTP)
type Conn interface {
Streams() (streams []av.CodecData, err error)
ReadPacket() (pkt av.Packet, err error)
Close() (err error)
}
type Client struct { type Client struct {
streamer.Element streamer.Element
@@ -23,7 +30,7 @@ type Client struct {
medias []*streamer.Media medias []*streamer.Media
tracks []*streamer.Track tracks []*streamer.Track
conn *rtmp.Conn conn Conn
closed bool closed bool
receive int receive int
@@ -34,7 +41,12 @@ func NewClient(uri string) *Client {
} }
func (c *Client) Dial() (err error) { func (c *Client) Dial() (err error) {
c.conn, err = rtmp.Dial(c.URI) if strings.HasPrefix(c.URI, "http") {
c.conn, err = httpflv.Dial(c.URI)
} else {
c.conn, err = rtmp.Dial(c.URI)
}
if err != nil { if err != nil {
return return
} }
@@ -48,16 +60,20 @@ func (c *Client) Dial() (err error) {
for _, stream := range streams { for _, stream := range streams {
switch stream.Type() { switch stream.Type() {
case av.H264: case av.H264:
cd := stream.(h264parser.CodecData) info := stream.(h264parser.CodecData).RecordInfo
fmtp := "sprop-parameter-sets=" +
base64.StdEncoding.EncodeToString(cd.RecordInfo.SPS[0]) + "," + fmtp := fmt.Sprintf(
base64.StdEncoding.EncodeToString(cd.RecordInfo.PPS[0]) "profile-level-id=%02X%02X%02X;sprop-parameter-sets=%s,%s",
info.AVCProfileIndication, info.ProfileCompatibility, info.AVCLevelIndication,
base64.StdEncoding.EncodeToString(info.SPS[0]),
base64.StdEncoding.EncodeToString(info.PPS[0]),
)
codec := &streamer.Codec{ codec := &streamer.Codec{
Name: streamer.CodecH264, Name: streamer.CodecH264,
ClockRate: 90000, ClockRate: 90000,
FmtpLine: fmtp, FmtpLine: fmtp,
PayloadType: h264.PayloadTypeAVC, PayloadType: streamer.PayloadTypeMP4,
} }
media := &streamer.Media{ media := &streamer.Media{
@@ -76,17 +92,13 @@ func (c *Client) Dial() (err error) {
// TODO: fix support // TODO: fix support
cd := stream.(aacparser.CodecData) cd := stream.(aacparser.CodecData)
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
fmtp := fmt.Sprintf(
"config=%s",
hex.EncodeToString(cd.ConfigBytes),
)
codec := &streamer.Codec{ codec := &streamer.Codec{
Name: streamer.CodecAAC, Name: streamer.CodecAAC,
ClockRate: uint32(cd.Config.SampleRate), ClockRate: uint32(cd.Config.SampleRate),
Channels: uint16(cd.Config.ChannelConfig), Channels: uint16(cd.Config.ChannelConfig),
FmtpLine: fmtp, // a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
PayloadType: streamer.PayloadTypeMP4,
} }
media := &streamer.Media{ media := &streamer.Media{
@@ -130,22 +142,14 @@ func (c *Client) Handle() (err error) {
track := c.tracks[int(pkt.Idx)] track := c.tracks[int(pkt.Idx)]
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate)) // convert seconds to RTP timestamp
timestamp := uint32(pkt.Time * time.Duration(track.Codec.ClockRate) / time.Second)
var payloads [][]byte packet := &rtp.Packet{
if track.Codec.Name == streamer.CodecH264 { Header: rtp.Header{Timestamp: timestamp},
payloads = splitAVC(pkt.Data) Payload: pkt.Data,
} else {
payloads = [][]byte{pkt.Data}
}
for _, payload := range payloads {
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: timestamp},
Payload: payload,
}
_ = track.WriteRTP(packet)
} }
_ = track.WriteRTP(packet)
} }
} }
@@ -156,21 +160,3 @@ func (c *Client) Close() error {
c.closed = true c.closed = true
return c.conn.Close() return c.conn.Close()
} }
func splitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data))
// check if multiple items in one packet
if size+4 < len(data) {
nals = append(nals, data[:size+4])
data = data[size+4:]
} else {
nals = append(nals, data)
break
}
}
return nals
}

View File

@@ -32,7 +32,7 @@ func (c *Client) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{ v := map[string]interface{}{
streamer.JSONReceive: c.receive, streamer.JSONReceive: c.receive,
streamer.JSONType: "RTMP client producer", streamer.JSONType: "RTMP client producer",
streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(), //streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
"url": c.URI, "url": c.URI,
} }
for i, media := range c.medias { for i, media := range c.medias {

View File

@@ -7,6 +7,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
@@ -43,8 +44,6 @@ const (
ModeServerConsumer ModeServerConsumer
) )
const KeepAlive = time.Second * 25
type Conn struct { type Conn struct {
streamer.Element streamer.Element
@@ -60,11 +59,12 @@ type Conn struct {
// internal // internal
auth *tcp.Auth auth *tcp.Auth
closed bool
conn net.Conn conn net.Conn
mode Mode
reader *bufio.Reader reader *bufio.Reader
sequence int sequence int
uri string
mode Mode
tracks []*streamer.Track tracks []*streamer.Track
channels map[byte]*streamer.Track channels map[byte]*streamer.Track
@@ -76,24 +76,10 @@ type Conn struct {
} }
func NewClient(uri string) (*Conn, error) { func NewClient(uri string) (*Conn, error) {
var err error
c := new(Conn) c := new(Conn)
c.URL, err = url.Parse(uri)
if err != nil {
return nil, err
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
c.URL.Host += ":554"
}
// remove UserInfo from URL
c.auth = tcp.NewAuth(c.URL.User)
c.mode = ModeClientProducer c.mode = ModeClientProducer
c.URL.User = nil c.uri = uri
return c, c.parseURI()
return c, nil
} }
func NewServer(conn net.Conn) *Conn { func NewServer(conn net.Conn) *Conn {
@@ -104,17 +90,32 @@ func NewServer(conn net.Conn) *Conn {
return c return c
} }
func (c *Conn) parseURI() (err error) {
c.URL, err = url.Parse(c.uri)
if err != nil {
return err
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
c.URL.Host += ":554"
}
// remove UserInfo from URL
c.auth = tcp.NewAuth(c.URL.User)
c.URL.User = nil
return nil
}
func (c *Conn) Dial() (err error) { func (c *Conn) Dial() (err error) {
//if c.state != StateClientInit { //if c.state != StateClientInit {
// panic("wrong state") // panic("wrong state")
//} //}
if c.conn != nil && c.auth != nil { if c.conn != nil {
c.auth.Reset() _ = c.parseURI()
} }
c.conn, err = net.DialTimeout( c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
"tcp", c.URL.Host, 10*time.Second,
)
if err != nil { if err != nil {
return return
} }
@@ -359,6 +360,24 @@ func (c *Conn) SetupMedia(
var res *tcp.Response var res *tcp.Response
res, err = c.Do(req) res, err = c.Do(req)
if err != nil { if err != nil {
// some Dahua/Amcrest cameras fail here because two simultaneous
// backchannel connections
if c.Backchannel {
c.Backchannel = false
if err := c.Dial(); err != nil {
return nil, err
}
if err := c.Describe(); err != nil {
return nil, err
}
for _, newMedia := range c.Medias {
if newMedia.Control == media.Control {
return c.SetupMedia(newMedia, newMedia.Codecs[0])
}
}
}
return nil, err return nil, err
} }
@@ -375,12 +394,16 @@ func (c *Conn) SetupMedia(
// we send our `interleaved`, but camera can answer with another // we send our `interleaved`, but camera can answer with another
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7 // Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.123;source=192.168.10.12;interleaved=0 // Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1 // Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
s := res.Header.Get("Transport") s := res.Header.Get("Transport")
// TODO: rewrite // TODO: rewrite
if !strings.HasPrefix(s, "RTP/AVP/TCP;") { if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
return nil, fmt.Errorf("wrong transport: %s", s) // Escam Q6 has a bug:
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
if !strings.Contains(s, ";interleaved=") {
return nil, fmt.Errorf("wrong transport: %s", s)
}
} }
i := strings.Index(s, "interleaved=") i := strings.Index(s, "interleaved=")
@@ -434,24 +457,19 @@ func (c *Conn) Teardown() (err error) {
} }
func (c *Conn) Close() error { func (c *Conn) Close() error {
if c.conn == nil { if c.closed {
return nil return nil
} }
if err := c.Teardown(); err != nil { if err := c.Teardown(); err != nil {
return err return err
} }
conn := c.conn c.closed = true
c.conn = nil return c.conn.Close()
return conn.Close()
} }
const transport = "RTP/AVP/TCP;unicast;interleaved=" const transport = "RTP/AVP/TCP;unicast;interleaved="
func (c *Conn) Accept() error { func (c *Conn) Accept() error {
//if c.state != StateServerInit {
// panic("wrong state")
//}
for { for {
req, err := tcp.ReadRequest(c.reader) req, err := tcp.ReadRequest(c.reader)
if err != nil { if err != nil {
@@ -554,7 +572,7 @@ func (c *Conn) Accept() error {
Request: req, Request: req,
} }
if tr[:len(transport)] == transport { if strings.HasPrefix(tr, transport) {
c.Session = "1" // TODO: fixme c.Session = "1" // TODO: fixme
res.Header.Set("Transport", tr[:len(transport)+3]) res.Header.Set("Transport", tr[:len(transport)+3])
} else { } else {
@@ -577,16 +595,44 @@ func (c *Conn) Accept() error {
func (c *Conn) Handle() (err error) { func (c *Conn) Handle() (err error) {
defer func() { defer func() {
if c.conn == nil { if c.closed {
err = nil err = nil
} else {
// may have gotten here because of the deadline
// so close the connection to stop keepalive
_ = c.conn.Close()
} }
//c.Fire(streamer.StateNull)
}() }()
//c.Fire(streamer.StatePlaying) var timeout time.Duration
ts := time.Now().Add(KeepAlive)
switch c.mode {
case ModeClientProducer:
// polling frames from remote RTSP Server (ex Camera)
timeout = time.Second * 5
go c.keepalive()
case ModeServerProducer:
// polling frames from remote RTSP Client (ex FFmpeg)
timeout = time.Second * 15
case ModeServerConsumer:
// pushing frames to remote RTSP Client (ex VLC)
timeout = time.Second * 60
default:
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
}
for { for {
if c.closed {
return
}
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return
}
// we can read: // we can read:
// 1. RTP interleaved: `$` + 1B channel number + 2B size // 1. RTP interleaved: `$` + 1B channel number + 2B size
// 2. RTSP response: RTSP/1.0 200 OK // 2. RTSP response: RTSP/1.0 200 OK
@@ -664,16 +710,19 @@ func (c *Conn) Handle() (err error) {
c.Fire(msg) c.Fire(msg)
} }
}
}
// keep-alive func (c *Conn) keepalive() {
now := time.Now() // TODO: rewrite to RTCP
if now.After(ts) { req := &tcp.Request{Method: MethodOptions, URL: c.URL}
req := &tcp.Request{Method: MethodOptions, URL: c.URL} for {
// don't need to wait respose on this request time.Sleep(time.Second * 25)
if err = c.Request(req); err != nil { if c.closed {
return err return
} }
ts = now.Add(KeepAlive) if err := c.Request(req); err != nil {
return
} }
} }
} }
@@ -691,20 +740,16 @@ func (c *Conn) bindTrack(
track *streamer.Track, channel uint8, payloadType uint8, track *streamer.Track, channel uint8, payloadType uint8,
) *streamer.Track { ) *streamer.Track {
push := func(packet *rtp.Packet) error { push := func(packet *rtp.Packet) error {
if c.conn == nil { if c.closed {
return nil return nil
} }
packet.Header.PayloadType = payloadType packet.Header.PayloadType = payloadType
//packet.Header.PayloadType = 100
//packet.Header.PayloadType = 8
//packet.Header.PayloadType = 106
size := packet.MarshalSize() size := packet.MarshalSize()
data := make([]byte, 4+size) data := make([]byte, 4+size)
data[0] = '$' data[0] = '$'
data[1] = channel data[1] = channel
//data[1] = 10
binary.BigEndian.PutUint16(data[2:], uint16(size)) binary.BigEndian.PutUint16(data[2:], uint16(size))
if _, err := packet.MarshalTo(data[4:]); err != nil { if _, err := packet.MarshalTo(data[4:]); err != nil {
@@ -720,9 +765,15 @@ func (c *Conn) bindTrack(
return nil return nil
} }
if h264.IsAVC(track.Codec) { if track.Codec.IsMP4() {
wrapper := h264.RTPPay(1500) switch track.Codec.Name {
push = wrapper(push) case streamer.CodecH264:
wrapper := h264.RTPPay(1500)
push = wrapper(push)
case streamer.CodecAAC:
wrapper := aac.RTPPay(1500)
push = wrapper(push)
}
} }
return track.Bind(push) return track.Bind(push)

View File

@@ -2,6 +2,7 @@ package rtsp
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv" "strconv"
) )
@@ -27,13 +28,16 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
} }
func (c *Conn) Start() error { func (c *Conn) Start() error {
if c.mode == ModeServerProducer { switch c.mode {
return nil case ModeClientProducer:
if err := c.Play(); err != nil {
return err
}
case ModeServerProducer:
default:
return fmt.Errorf("start wrong mode: %d", c.mode)
} }
if err := c.Play(); err != nil {
return err
}
return c.Handle() return c.Handle()
} }

41
pkg/shell/shell.go Normal file
View File

@@ -0,0 +1,41 @@
package shell
import (
"strings"
)
func QuoteSplit(s string) []string {
var a []string
for len(s) > 0 {
is := strings.IndexByte(s, ' ')
if is >= 0 {
// skip prefix and double spaces
if is == 0 {
// goto next symbol
s = s[1:]
continue
}
// check if quote in word
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
// search quote end
if is = strings.Index(s, `" `); is > 0 {
is += 1
} else {
is = -1
}
}
}
if is >= 0 {
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
s = s[is+1:]
} else {
//add last word
a = append(a, s)
break
}
}
return a
}

View File

@@ -90,8 +90,8 @@ func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
switch len(masterKey) { switch len(masterKey) {
case 16: case 16:
return srtp.ProtectionProfileAes128CmHmacSha1_80 return srtp.ProtectionProfileAes128CmHmacSha1_80
case 32: //case 32:
return srtp.ProtectionProfileAes256CmHmacSha1_80 // return srtp.ProtectionProfileAes256CmHmacSha1_80
} }
return 0 return 0
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"strconv" "strconv"
"strings" "strings"
"unicode"
) )
const ( const (
@@ -24,6 +25,7 @@ const (
CodecVP8 = "VP8" CodecVP8 = "VP8"
CodecVP9 = "VP9" CodecVP9 = "VP9"
CodecAV1 = "AV1" CodecAV1 = "AV1"
CodecJPEG = "JPEG" // payloadType: 26
CodecPCMU = "PCMU" // payloadType: 0 CodecPCMU = "PCMU" // payloadType: 0
CodecPCMA = "PCMA" // payloadType: 8 CodecPCMA = "PCMA" // payloadType: 8
@@ -33,9 +35,11 @@ const (
CodecMPA = "MPA" // payload: 14 CodecMPA = "MPA" // payload: 14
) )
const PayloadTypeMP4 byte = 255
func GetKind(name string) string { func GetKind(name string) string {
switch name { switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1: case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA: case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
return KindAudio return KindAudio
@@ -73,13 +77,13 @@ func (m *Media) AV() bool {
return m.Kind == KindVideo || m.Kind == KindAudio return m.Kind == KindVideo || m.Kind == KindAudio
} }
func (m *Media) MatchCodec(codec *Codec) bool { func (m *Media) MatchCodec(codec *Codec) *Codec {
for _, c := range m.Codecs { for _, c := range m.Codecs {
if c.Match(codec) { if c.Match(codec) {
return true return c
} }
} }
return false return nil
} }
func (m *Media) MatchMedia(media *Media) *Codec { func (m *Media) MatchMedia(media *Media) *Codec {
@@ -125,20 +129,6 @@ type Codec struct {
PayloadType uint8 PayloadType uint8
} }
func NewCodec(name string) *Codec {
name = strings.ToUpper(name)
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
return &Codec{Name: name, ClockRate: 90000}
case CodecPCMU, CodecPCMA:
return &Codec{Name: name, ClockRate: 8000}
case CodecOpus:
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
}
panic(fmt.Sprintf("unsupported codec: %s", name))
}
func (c *Codec) String() string { func (c *Codec) String() string {
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate) s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
if c.Channels > 0 { if c.Channels > 0 {
@@ -147,6 +137,10 @@ func (c *Codec) String() string {
return s return s
} }
func (c *Codec) IsMP4() bool {
return c.PayloadType == PayloadTypeMP4
}
func (c *Codec) Clone() *Codec { func (c *Codec) Clone() *Codec {
clone := *c clone := *c
return &clone return &clone
@@ -154,8 +148,8 @@ func (c *Codec) Clone() *Codec {
func (c *Codec) Match(codec *Codec) bool { func (c *Codec) Match(codec *Codec) bool {
return c.Name == codec.Name && return c.Name == codec.Name &&
c.ClockRate == codec.ClockRate && (c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
c.Channels == codec.Channels (c.Channels == codec.Channels || codec.Channels == 0)
} }
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) { func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
@@ -242,7 +236,8 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
ss := strings.Split(attr.Value[i+1:], "/") ss := strings.Split(attr.Value[i+1:], "/")
c.Name = strings.ToUpper(ss[0]) c.Name = strings.ToUpper(ss[0])
c.ClockRate = uint32(atoi(ss[1])) // fix tailing space: `a=rtpmap:96 H264/90000 `
c.ClockRate = uint32(atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
if len(ss) == 3 && ss[2] == "2" { if len(ss) == 3 && ss[2] == "2" {
c.Channels = 2 c.Channels = 2
@@ -255,6 +250,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
} }
if c.Name == "" { if c.Name == "" {
// https://en.wikipedia.org/wiki/RTP_payload_formats
switch payloadType { switch payloadType {
case "0": case "0":
c.Name = CodecPCMU c.Name = CodecPCMU
@@ -265,6 +261,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
case "14": case "14":
c.Name = CodecMPA c.Name = CodecMPA
c.ClockRate = 44100 c.ClockRate = 44100
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
default: default:
c.Name = payloadType c.Name = payloadType
} }

View File

@@ -12,41 +12,54 @@ type WrapperFunc func(push WriterFunc) WriterFunc
type Track struct { type Track struct {
Codec *Codec Codec *Codec
Direction string Direction string
Sink map[*Track]WriterFunc sink map[*Track]WriterFunc
mx sync.Mutex sinkMu sync.RWMutex
} }
func (t *Track) String() string { func (t *Track) String() string {
s := t.Codec.String() s := t.Codec.String()
s += fmt.Sprintf(", sinks=%d", len(t.Sink)) s += fmt.Sprintf(", sinks=%d", len(t.sink))
return s return s
} }
func (t *Track) WriteRTP(p *rtp.Packet) error { func (t *Track) WriteRTP(p *rtp.Packet) error {
t.mx.Lock() t.sinkMu.RLock()
for _, f := range t.Sink { for _, f := range t.sink {
_ = f(p) _ = f(p)
} }
t.mx.Unlock() t.sinkMu.RUnlock()
return nil return nil
} }
func (t *Track) Bind(w WriterFunc) *Track { func (t *Track) Bind(w WriterFunc) *Track {
if t.Sink == nil { t.sinkMu.Lock()
t.Sink = map[*Track]WriterFunc{}
if t.sink == nil {
t.sink = map[*Track]WriterFunc{}
} }
clone := &Track{ clone := &Track{
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink, Codec: t.Codec, Direction: t.Direction, sink: t.sink,
} }
t.mx.Lock() t.sink[clone] = w
t.Sink[clone] = w
t.mx.Unlock() t.sinkMu.Unlock()
return clone return clone
} }
func (t *Track) Unbind() { func (t *Track) Unbind() {
t.mx.Lock() t.sinkMu.Lock()
delete(t.Sink, t) delete(t.sink, t)
t.mx.Unlock() t.sinkMu.Unlock()
}
func (t *Track) GetSink(from *Track) {
t.sink = from.sink
}
func (t *Track) HasSink() bool {
t.sinkMu.RLock()
defer t.sinkMu.RUnlock()
return len(t.sink) > 0
} }

View File

@@ -80,12 +80,6 @@ func (a *Auth) Write(req *Request) {
} }
} }
func (a *Auth) Reset() {
if a.Method == AuthDigest {
a.Method = AuthUnknown
}
}
func Between(s, sub1, sub2 string) string { func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1) i := strings.Index(s, sub1)
if i < 0 { if i < 0 {

View File

@@ -86,10 +86,6 @@ func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
PayloadType: 98, //123, PayloadType: 98, //123,
}, },
// macOS Safari 15.1 // macOS Safari 15.1
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f", videoRTCPFeedback},
PayloadType: 99,
},
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback}, RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
PayloadType: 100, PayloadType: 100,

View File

@@ -59,7 +59,6 @@ func (c *Conn) Init() {
} }
fmt.Printf("TODO: webrtc ontrack %+v\n", remote) fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
fmt.Printf("TODO: webrtc ontrack %#v\n", remote)
}) })
// OK connection: // OK connection:

View File

@@ -3,6 +3,7 @@ package webrtc
import ( import (
"encoding/json" "encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
@@ -51,16 +52,26 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
return trackLocal.WriteRTP(packet) return trackLocal.WriteRTP(packet)
} }
if codec.Name == streamer.CodecH264 { switch codec.Name {
case streamer.CodecH264:
wrapper := h264.RTPPay(1200) wrapper := h264.RTPPay(1200)
push = wrapper(push) push = wrapper(push)
if h264.IsAVC(codec) { if codec.IsMP4() {
wrapper = h264.RepairAVC(track) wrapper = h264.RepairAVC(track)
} else { } else {
wrapper = h264.RTPDepay(track) wrapper = h264.RTPDepay(track)
} }
push = wrapper(push) push = wrapper(push)
case streamer.CodecH265:
// SafariPay because it is the only browser in the world
// that supports WebRTC + H265
wrapper := h265.SafariPay(1200)
push = wrapper(push)
wrapper = h265.RTPDepay(track)
push = wrapper(push)
} }
track = track.Bind(push) track = track.Bind(push)

View File

@@ -1,12 +1,15 @@
package webrtc package webrtc
import ( import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/stun" "github.com/pion/stun"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"net" "net"
"strconv" "strconv"
"strings"
"time"
) )
func NewCandidate(address string) (string, error) { func NewCandidate(address string) (string, error) {
@@ -34,13 +37,47 @@ func NewCandidate(address string) (string, error) {
return "candidate:" + cand.Marshal(), nil return "candidate:" + cand.Marshal(), nil
} }
func LookupIP(address string) (string, error) {
if strings.HasPrefix(address, "stun:") {
ip, err := GetCachedPublicIP()
if err != nil {
return "", err
}
return ip.String() + address[4:], nil
}
if IsIP(address) {
return address, nil
}
i := strings.IndexByte(address, ':')
ips, err := net.LookupIP(address[:i])
if err != nil {
return "", err
}
if len(ips) == 0 {
return "", fmt.Errorf("can't resolve: %s", address)
}
return ips[0].String() + address[i:], nil
}
// GetPublicIP example from https://github.com/pion/stun // GetPublicIP example from https://github.com/pion/stun
func GetPublicIP() (net.IP, error) { func GetPublicIP() (net.IP, error) {
c, err := stun.Dial("udp", "stun.l.google.com:19302") conn, err := net.Dial("udp", "stun.l.google.com:19302")
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err := stun.NewClient(conn)
if err != nil {
return nil, err
}
if err = conn.SetDeadline(time.Now().Add(time.Second * 3)); err != nil {
return nil, err
}
var res stun.Event var res stun.Event
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest) message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
@@ -63,6 +100,33 @@ func GetPublicIP() (net.IP, error) {
return xorAddr.IP, nil return xorAddr.IP, nil
} }
var cachedIP net.IP
var cachedTS time.Time
func GetCachedPublicIP() (net.IP, error) {
now := time.Now()
if now.After(cachedTS) {
newIP, err := GetPublicIP()
if err == nil {
cachedIP = newIP
cachedTS = now.Add(time.Minute * 5)
} else if cachedIP == nil {
return nil, err
}
}
return cachedIP, nil
}
func IsIP(host string) bool {
for _, i := range host {
if i >= 'A' {
return false
}
}
return true
}
func MimeType(codec *streamer.Codec) string { func MimeType(codec *streamer.Codec) string {
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case streamer.CodecH264:

View File

@@ -51,4 +51,7 @@ pc.ontrack = ev => {
## Useful links ## Useful links
- https://www.webrtc-experiment.com/DetectRTC/ - https://www.webrtc-experiment.com/DetectRTC/
- https://divtable.com/table-styler/ - https://divtable.com/table-styler/
- https://www.chromium.org/audio-video/
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API

View File

@@ -45,7 +45,9 @@
'video/mp4; codecs="avc1.640032"', 'video/mp4; codecs="avc1.640032"',
'video/mp4; codecs="avc1.640C32"', 'video/mp4; codecs="avc1.640C32"',
'video/mp4; codecs="avc1.F4001F"', 'video/mp4; codecs="avc1.F4001F"',
'video/mp4; codecs="hvc1.016000"', 'video/mp4; codecs="hvc1.1.6.L93.B0"',
'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/mp4; codecs="hev1.2.4.L120.B0"',
]; ];
const video = document.createElement("video"); const video = document.createElement("video");

View File

@@ -66,7 +66,11 @@
const links = [ const links = [
'<a href="webrtc.html?src={name}">webrtc</a>', '<a href="webrtc.html?src={name}">webrtc</a>',
'<a href="mse.html?src={name}">mse</a>', '<a href="mse.html?src={name}">mse</a>',
'<a href="api/frame.mp4?src={name}">frame.mp4</a>', // '<a href="video.html?src={name}">video</a>',
'<a href="api/stream.mp4?src={name}">mp4</a>',
'<a href="api/frame.mp4?src={name}">frame</a>',
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
'<a href="api/streams?src={name}">info</a>', '<a href="api/streams?src={name}">info</a>',
]; ];

View File

@@ -25,95 +25,75 @@
<!-- muted is important for autoplay --> <!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video> <video id="video" autoplay controls playsinline muted></video>
<script> <script>
const video = document.querySelector('#video');
// support api_path // support api_path
const baseUrl = location.origin + location.pathname.substr( const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/") 0, location.pathname.lastIndexOf("/")
); );
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`); const video = document.querySelector('#video');
ws.binaryType = "arraybuffer";
let mediaSource; function init() {
let mediaSource, sourceBuffer, queueBuffer = [];
ws.onopen = () => { const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
console.log("Start WS"); ws.binaryType = "arraybuffer";
// https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering ws.onopen = () => {
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API mediaSource = new MediaSource();
mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource);
video.src = URL.createObjectURL(mediaSource); mediaSource.onsourceopen = () => {
mediaSource.onsourceopen = () => { mediaSource.onsourceopen = null;
console.debug("mediaSource.onsourceopen"); URL.revokeObjectURL(video.src);
ws.send(JSON.stringify({"type": "mse"}));
mediaSource.onsourceopen = null; };
URL.revokeObjectURL(video.src);
ws.send(JSON.stringify({"type": "mse"}));
}; };
};
let sourceBuffer, queueBuffer = []; ws.onmessage = ev => {
if (typeof ev.data === 'string') {
const data = JSON.parse(ev.data);
console.debug("ws.onmessage", data);
ws.onmessage = ev => { if (data.type === "mse") {
if (typeof ev.data === 'string') { sourceBuffer = mediaSource.addSourceBuffer(data.value);
const data = JSON.parse(ev.data); sourceBuffer.mode = "segments"; // segments or sequence
console.debug("ws.onmessage", data); sourceBuffer.onupdateend = () => {
if (!sourceBuffer.updating && queueBuffer.length > 0) {
if (data.type === "mse") { try {
sourceBuffer = mediaSource.addSourceBuffer( sourceBuffer.appendBuffer(queueBuffer.shift());
`video/mp4; codecs="${data.value}"` } catch (e) {
); // console.warn(e);
// important: segments supports TrackFragDecodeTime }
// sequence supports only TrackFragRunEntry Duration }
sourceBuffer.mode = "segments";
sourceBuffer.onupdateend = () => {
if (!sourceBuffer.updating && queueBuffer.length > 0) {
sourceBuffer.appendBuffer(queueBuffer.shift());
} }
} }
} } else if (sourceBuffer.updating || queueBuffer.length > 0) {
} else { queueBuffer.push(ev.data);
if (sourceBuffer.updating) {
queueBuffer.push(ev.data)
} else { } else {
sourceBuffer.appendBuffer(ev.data); try {
sourceBuffer.appendBuffer(ev.data);
} catch (e) {
// console.warn(e);
}
}
if (video.seekable.length > 0) {
const delay = video.seekable.end(video.seekable.length - 1) - video.currentTime;
if (delay < 1) {
video.playbackRate = 1;
} else if (delay > 10) {
video.playbackRate = 10;
} else if (delay > 2) {
video.playbackRate = Math.floor(delay);
}
} }
} }
video.onpause = () => {
ws.close();
setTimeout(init, 0);
}
} }
let offsetTime = 1, noWaiting = 0; init();
setInterval(() => {
if (video.paused || video.seekable.length === 0) return;
if (noWaiting < 0) {
offsetTime = Math.min(offsetTime * 1.1, 5);
console.debug("offset time up:", offsetTime);
} else if (noWaiting >= 30) {
noWaiting = 0;
offsetTime = Math.max(offsetTime * 0.9, 0.5);
console.debug("offset time down:", offsetTime);
}
noWaiting += 1;
const endTime = video.seekable.end(video.seekable.length - 1);
let playbackRate = (endTime - video.currentTime) / offsetTime;
if (playbackRate < 0.1) {
// video.currentTime = endTime - offsetTime;
playbackRate = 0.1;
} else if (playbackRate > 10) {
// video.currentTime = endTime - offsetTime;
playbackRate = 10;
}
// https://github.com/GoogleChrome/developer.chrome.com/issues/135
video.playbackRate = playbackRate;
}, 1000);
video.onwaiting = () => {
const endTime = video.seekable.end(video.seekable.length - 1);
video.currentTime = endTime - offsetTime;
noWaiting = -1;
}
</script> </script>
</body> </body>
</html> </html>

53
www/video.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - WebRTC</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
#video {
/* video "container" size */
width: 100%;
height: 100%;
background: black;
}
</style>
</head>
<body>
<video id="video" autoplay controls playsinline muted></video>
<!--<video id="video" preload="auto" controls playsinline muted></video>-->
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const video = document.getElementById('video');
video.oncanplay = ev => console.log(ev.type, ev);
video.onplaying = ev => console.log(ev.type, ev);
video.onwaiting = ev => console.log(ev.type, ev);
video.onseeking = ev => console.log(ev.type, ev);
video.onloadeddata = ev => console.log(ev.type, ev);
video.oncanplaythrough = ev => console.log(ev.type, ev);
// video.ondurationchange = ev => console.log(ev.type, ev);
// video.ontimeupdate = ev => console.log(ev.type, ev);
video.onplay = ev => console.log(ev.type, ev);
video.onpause = ev => console.log(ev.type, ev);
video.onsuspended = ev => console.log(ev.type, ev);
video.onemptied = ev => console.log(ev.type, ev);
video.onstalled = ev => console.log(ev.type, ev);
console.log("start");
video.src = baseUrl + "/api/stream.mp4" + location.search;
</script>
</body>
</html>

View File

@@ -25,12 +25,12 @@
<body> <body>
<video id="video" autoplay controls playsinline muted></video> <video id="video" autoplay controls playsinline muted></video>
<script> <script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
function init(stream) { function init(stream) {
// support api_path // support api_path
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`); const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
ws.onopen = () => { ws.onopen = () => {
console.debug('ws.onopen'); console.debug('ws.onopen');
@@ -51,11 +51,6 @@
pc.addIceCandidate({candidate: msg.value, sdpMid: ''}); pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
} else if (msg.type === 'webrtc/answer') { } else if (msg.type === 'webrtc/answer') {
pc.setRemoteDescription({type: 'answer', sdp: msg.value}); pc.setRemoteDescription({type: 'answer', sdp: msg.value});
pc.getTransceivers().forEach(t => {
if (t.receiver.track.kind === 'audio') {
t.currentDirection
}
})
} }
} }