Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb3c64598c | |||
| 3002d5f4f1 | |||
| cca4f0500e | |||
| b087be9c56 | |||
| 2d5a0e4822 | |||
| acf5ec5256 | |||
| e1e8abc334 | |||
| d84efd1238 | |||
| 7c79c1ff26 | |||
| 43840576ea | |||
| bd79b24db3 | |||
| e728643aad | |||
| 12a7b96289 | |||
| 2146ea470b | |||
| d4d91e4920 | |||
| a6393da956 | |||
| d686d4f691 | |||
| 58849fd1e5 | |||
| 31c86272bb | |||
| 0382fbf8a9 | |||
| 0b714a59e5 | |||
| 13c426e2a9 | |||
| d6d21286c1 | |||
| ce2898ac3a | |||
| e0320b8ead | |||
| a960b9b9ee | |||
| 0b4ebb4e21 | |||
| a1dd941814 | |||
| 146fb62b8e | |||
| 53e8fed0b0 | |||
| 3d34854387 | |||
| 77842643c8 | |||
| f9fe22569c | |||
| e17645ac02 | |||
| 1fc2cf3175 | |||
| 775b1818d1 | |||
| 1e83dc85f7 | |||
| 03a4393ce3 | |||
| 5f2368b0f9 | |||
| 59b35f2501 | |||
| 9ab9412c95 | |||
| d805d560b9 | |||
| 5aa20f0845 | |||
| c2cdf60ffc | |||
| c70c3a58f1 | |||
| df0ab77791 | |||
| 402df50b65 | |||
| 5c084c9989 | |||
| 1703e0dce8 | |||
| 7301f55e4a | |||
| 06fc9717df | |||
| 4e19c54467 | |||
| a0e04fb70e | |||
| 218eea6806 | |||
| ad3c5440fe | |||
| f5892e4cfc | |||
| 4328d2a573 | |||
| 3fb917f00f | |||
| eca79f1c0b | |||
| bd9b69d0d5 | |||
| 676ec25a7f | |||
| 12d10ae14e | |||
| eb1f423da3 | |||
| 5846cbd989 | |||
| ab1b3932ac | |||
| 1fe21bb300 | |||
| 41cdcb69c6 | |||
| 6b00134575 | |||
| 5519f3e061 | |||
| e312d0b46b | |||
| eff7b27293 |
@@ -0,0 +1,37 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: './website'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
@@ -6,15 +6,17 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
|
||||
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
||||
- zero-delay for many supported protocols (lowest possible streaming latency)
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
||||
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HLS](#module-hls) or [MJPEG](#module-mjpeg)
|
||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||
- first project in the World with support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
|
||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
|
||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||
- mixing tracks from different sources to single stream
|
||||
- auto match client supported codecs
|
||||
- 2-way audio for `ONVIF Profile T` Cameras
|
||||
- [2-way audio](#two-way-audio) for some cameras
|
||||
- streaming from private networks via [Ngrok](#module-ngrok)
|
||||
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
|
||||
|
||||
@@ -36,6 +38,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
|
||||
* [Configuration](#configuration)
|
||||
* [Module: Streams](#module-streams)
|
||||
* [Two way audio](#two-way-audio)
|
||||
* [Source: RTSP](#source-rtsp)
|
||||
* [Source: RTMP](#source-rtmp)
|
||||
* [Source: HTTP](#source-http)
|
||||
@@ -44,8 +47,12 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Source: Exec](#source-exec)
|
||||
* [Source: Echo](#source-echo)
|
||||
* [Source: HomeKit](#source-homekit)
|
||||
* [Source: DVRIP](#source-dvrip)
|
||||
* [Source: Tapo](#source-tapo)
|
||||
* [Source: Ivideon](#source-ivideon)
|
||||
* [Source: Hass](#source-hass)
|
||||
* [Incoming sources](#incoming-sources)
|
||||
* [Stream to camera](#stream-to-camera)
|
||||
* [Module: API](#module-api)
|
||||
* [Module: RTSP](#module-rtsp)
|
||||
* [Module: WebRTC](#module-webrtc)
|
||||
@@ -142,41 +149,55 @@ Available modules:
|
||||
|
||||
Available source types:
|
||||
|
||||
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
|
||||
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support
|
||||
- [rtmp](#source-rtmp) - `RTMP` streams
|
||||
- [http](#source-http) - `HTTP-FLV`, `JPEG` (snapshots), `MJPEG` streams
|
||||
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
|
||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||
- [echo](#source-echo) - get stream link from bash or python
|
||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
|
||||
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
|
||||
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
|
||||
Read more about [incoming sources](#incoming-sources)
|
||||
|
||||
#### Two way audio
|
||||
|
||||
Supported for sources:
|
||||
|
||||
- RTSP cameras with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||
- TP-Link Tapo cameras
|
||||
|
||||
Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
|
||||
|
||||
go2rtc also support [play audio](#stream-to-camera) files and live streams on this cameras.
|
||||
|
||||
#### Source: RTSP
|
||||
|
||||
- Support **RTSP and RTSPS** links with multiple video and audio tracks
|
||||
- Support **2-way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
|
||||
|
||||
**Attention:** other 2-way audio standards are not supported! ONVIF without Profile T is not supported!
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||
```
|
||||
|
||||
If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream.
|
||||
|
||||
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for 2-way audio with `&proto=Onvif` in link and only one codec without it.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua_camera:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
|
||||
amcrest_doorbell:
|
||||
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||
unify_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
|
||||
glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1
|
||||
```
|
||||
|
||||
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
|
||||
**Recommendations**
|
||||
|
||||
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
||||
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
|
||||
- **Unify** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
|
||||
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
|
||||
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
|
||||
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
|
||||
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
|
||||
|
||||
#### Source: RTMP
|
||||
|
||||
@@ -194,6 +215,7 @@ Support Content-Type:
|
||||
- **HTTP-FLV** (`video/x-flv`) - same as RTMP, but over HTTP
|
||||
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
|
||||
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
|
||||
- **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream)
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -239,7 +261,7 @@ streams:
|
||||
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||
```
|
||||
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
|
||||
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
|
||||
|
||||
@@ -248,13 +270,17 @@ ffmpeg:
|
||||
bin: ffmpeg # path to ffmpeg binary
|
||||
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
|
||||
mycodec: "-any args that support ffmpeg..."
|
||||
myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}"
|
||||
```
|
||||
|
||||
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
|
||||
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
|
||||
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
|
||||
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
|
||||
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
|
||||
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
|
||||
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
|
||||
- You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`)
|
||||
- You can add your own input templates
|
||||
|
||||
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
|
||||
|
||||
@@ -331,6 +357,36 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
|
||||
|
||||
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
|
||||
|
||||
#### Source: DVRIP
|
||||
|
||||
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
|
||||
|
||||
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
|
||||
- setup separate streams for different channels
|
||||
- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream
|
||||
- only the TCP protocol is supported
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
```
|
||||
|
||||
#### Source: Tapo
|
||||
|
||||
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
|
||||
|
||||
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
|
||||
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
|
||||
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# cloud password without username
|
||||
camera1: tapo://cloud-password@192.168.1.123
|
||||
# admin username and UPPERCASE MD5 cloud-password hash
|
||||
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
|
||||
```
|
||||
|
||||
#### Source: Ivideon
|
||||
|
||||
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||
@@ -358,6 +414,55 @@ streams:
|
||||
|
||||
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
|
||||
|
||||
### Incoming sources
|
||||
|
||||
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
|
||||
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp) and [HTTP](#source-http) formats
|
||||
- Go2rtc won't stop such a source if it has no clients
|
||||
- You can push data only to existing stream (create stream with empty source in config)
|
||||
- You can push multiple incoming sources to same stream
|
||||
- You can push data to non empty stream, so it will have additional codecs inside
|
||||
|
||||
**Examples**
|
||||
|
||||
- RTSP with any codec
|
||||
```yaml
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://localhost:8554/camera1
|
||||
```
|
||||
- HTTP-MJPEG with MJPEG codec
|
||||
```yaml
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c mjpeg -f mpjpeg http://localhost:1984/api/stream.mjpeg?dst=camera1
|
||||
```
|
||||
- HTTP-FLV with H264, AAC codecs
|
||||
```yaml
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f flv http://localhost:1984/api/stream.flv?dst=camera1
|
||||
```
|
||||
- MPEG-TS with H264 codec
|
||||
```yaml
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f mpegts http://localhost:1984/api/stream.ts?dst=camera1
|
||||
```
|
||||
|
||||
#### Stream to camera
|
||||
|
||||
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support.
|
||||
|
||||
API example:
|
||||
|
||||
```
|
||||
POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com/song.mp3#audio=pcma#input=file
|
||||
```
|
||||
|
||||
- you can stream: local files, web files, live streams or any format, supported by FFmpeg
|
||||
- you should use [ffmpeg source](#source-ffmpeg) for transcoding audio to codec, that your camera supports
|
||||
- you can check camera codecs on the go2rtc WebUI info page when the stream is active
|
||||
- some cameras support only low quality `PCMA/8000` codec (ex. [Tapo](#source-tapo))
|
||||
- it is recommended to choose higher quality formats if your camera supports them (ex. `PCMA/48000` for some Dahua cameras)
|
||||
- if you play files over http-link, you need to add `#input=file` params for transcoding, so file will be transcoded and played in real time
|
||||
- if you play live streams, you should skip `#input` param, because it is already in real time
|
||||
- you can stop active playback by calling the API with the empty `src` parameter
|
||||
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
|
||||
|
||||
### Module: API
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
|
||||
@@ -408,7 +513,6 @@ api:
|
||||
**PS:**
|
||||
|
||||
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
|
||||
- you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https))
|
||||
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
|
||||
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
|
||||
|
||||
@@ -437,7 +541,12 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
### Module: WebRTC
|
||||
|
||||
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
|
||||
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
|
||||
It **can't pass** media data through your Nginx or Cloudflare or [Nabu Casa](https://www.nabucasa.com/) HTTP TCP connection!
|
||||
It can automatically detects your external IP via public [STUN](https://en.wikipedia.org/wiki/STUN) server.
|
||||
It can establish a external direct connection via [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology even if you not open your server to the World.
|
||||
|
||||
But about 10-20% of users may need to configure additional settings for external access if **mobile phone** or **go2rtc server** behing [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/).
|
||||
|
||||
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
|
||||
- you can use this port for external access
|
||||
@@ -592,7 +701,7 @@ Provides several features:
|
||||
|
||||
1. MSE stream (fMP4 over WebSocket)
|
||||
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
|
||||
3. MP4 "file stream" - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 it this case.
|
||||
3. HTTP progressive streaming (MP4 file stream) - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 it this case.
|
||||
|
||||
API examples:
|
||||
|
||||
@@ -684,7 +793,7 @@ PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted
|
||||
|
||||
go2rtc can automatically detect which codecs your device supports for [WebRTC](#module-webrtc) and [MSE](#module-mp4) technologies.
|
||||
|
||||
But it cannot be done for [RTSP](#module-rtsp), [stream.mp4](#module-mp4), [HLS](#module-hls) technologies. You can manually add a codec filter when you create a link to a stream. The filters work the same for all three technologies. Filters do not create a new codec. They only select the suitable codec from existing sources. You can add new codecs to the stream using the [FFmpeg transcoding](#source-ffmpeg).
|
||||
But it cannot be done for [RTSP](#module-rtsp), [HTTP progressive streaming](#module-mp4), [HLS](#module-hls) technologies. You can manually add a codec filter when you create a link to a stream. The filters work the same for all three technologies. Filters do not create a new codec. They only select the suitable codec from existing sources. You can add new codecs to the stream using the [FFmpeg transcoding](#source-ffmpeg).
|
||||
|
||||
Without filters:
|
||||
|
||||
@@ -705,7 +814,7 @@ Some examples:
|
||||
|
||||
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
||||
|
||||
| Device | WebRTC | MSE | stream.mp4 |
|
||||
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|
||||
|---------------------|-------------------------------|------------------------|-----------------------------------------|
|
||||
| *latency* | best | medium | bad |
|
||||
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
|
||||
@@ -730,8 +839,8 @@ Some examples:
|
||||
|
||||
**Apple devices**
|
||||
|
||||
- all Apple devices don't support MP4 stream (they only support progressive loading of static files)
|
||||
- iPhones don't support MSE technology because it competes with the HLS technology, invented by Apple
|
||||
- all Apple devices don't support HTTP progressive streaming
|
||||
- iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
|
||||
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
|
||||
|
||||
## Codecs negotiation
|
||||
@@ -768,6 +877,8 @@ streams:
|
||||
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
|
||||
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
||||
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
|
||||
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
||||
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
|
||||
|
||||
## Cameras experience
|
||||
|
||||
|
||||
@@ -144,3 +144,23 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
code, _ := strconv.Atoi(s)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
type Stream struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func ResponseStreams(w http.ResponseWriter, streams []Stream) {
|
||||
if len(streams) == 0 {
|
||||
http.Error(w, "no streams", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Streams []Stream `json:"streams"`
|
||||
}
|
||||
response.Streams = streams
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -63,13 +63,13 @@ func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Unmarshal the first YAML file into a map
|
||||
var config1 map[string]interface{}
|
||||
var config1 map[string]any
|
||||
if err = yaml.Unmarshal(data1, &config1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal the second YAML document into a map
|
||||
var config2 map[string]interface{}
|
||||
var config2 map[string]any
|
||||
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,15 +81,15 @@ func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
|
||||
return yaml.Marshal(&config1)
|
||||
}
|
||||
|
||||
func merge(dst, src map[string]interface{}) map[string]interface{} {
|
||||
func merge(dst, src map[string]any) map[string]any {
|
||||
for k, v := range src {
|
||||
if vv, ok := dst[k]; ok {
|
||||
switch vv := vv.(type) {
|
||||
case map[string]interface{}:
|
||||
v := v.(map[string]interface{})
|
||||
case map[string]any:
|
||||
v := v.(map[string]any)
|
||||
dst[k] = merge(vv, v)
|
||||
case []interface{}:
|
||||
v := v.([]interface{})
|
||||
case []any:
|
||||
v := v.([]any)
|
||||
dst[k] = v
|
||||
default:
|
||||
dst[k] = v
|
||||
|
||||
+39
-10
@@ -11,8 +11,24 @@ import (
|
||||
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Value any `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
if s, ok := m.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Message) GetString(key string) string {
|
||||
if v, ok := m.Value.(map[string]any); ok {
|
||||
if s, ok := v[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
@@ -25,8 +41,8 @@ var wsHandlers = make(map[string]WSHandler)
|
||||
|
||||
func initWS(origin string) {
|
||||
wsUp = &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 2028,
|
||||
ReadBufferSize: 4096, // for SDP
|
||||
WriteBufferSize: 512 * 1024, // 512K
|
||||
}
|
||||
|
||||
switch origin {
|
||||
@@ -68,7 +84,7 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
tr := &Transport{Request: r}
|
||||
tr.OnWrite(func(msg interface{}) {
|
||||
tr.OnWrite(func(msg any) {
|
||||
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||
|
||||
if data, ok := msg.([]byte); ok {
|
||||
@@ -88,6 +104,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
|
||||
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
if err = handler(tr, msg); err != nil {
|
||||
@@ -103,19 +121,20 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
var wsUp *websocket.Upgrader
|
||||
|
||||
type Transport struct {
|
||||
Request *http.Request
|
||||
Consumer interface{} // TODO: rewrite
|
||||
Request *http.Request
|
||||
|
||||
ctx map[any]any
|
||||
|
||||
closed bool
|
||||
mx sync.Mutex
|
||||
wrmx sync.Mutex
|
||||
|
||||
onChange func()
|
||||
onWrite func(msg interface{})
|
||||
onWrite func(msg any)
|
||||
onClose []func()
|
||||
}
|
||||
|
||||
func (t *Transport) OnWrite(f func(msg interface{})) {
|
||||
func (t *Transport) OnWrite(f func(msg any)) {
|
||||
t.mx.Lock()
|
||||
if t.onChange != nil {
|
||||
t.onChange()
|
||||
@@ -124,7 +143,7 @@ func (t *Transport) OnWrite(f func(msg interface{})) {
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) Write(msg interface{}) {
|
||||
func (t *Transport) Write(msg any) {
|
||||
t.wrmx.Lock()
|
||||
t.onWrite(msg)
|
||||
t.wrmx.Unlock()
|
||||
@@ -154,3 +173,13 @@ func (t *Transport) OnClose(f func()) {
|
||||
}
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
// WithContext - run function with Context variable
|
||||
func (t *Transport) WithContext(f func(ctx map[any]any)) {
|
||||
t.mx.Lock()
|
||||
if t.ctx == nil {
|
||||
t.ctx = map[any]any{}
|
||||
}
|
||||
f(t.ctx)
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
+3
-3
@@ -14,11 +14,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var Version = "1.2.0"
|
||||
var Version = "1.3.0"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
var Info = map[string]interface{}{
|
||||
var Info = map[string]any{
|
||||
"version": Version,
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func NewLogger(format string, level string) zerolog.Logger {
|
||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||
}
|
||||
|
||||
func LoadConfig(v interface{}) {
|
||||
func LoadConfig(v any) {
|
||||
for _, data := range configs {
|
||||
if err := yaml.Unmarshal(data, v); err != nil {
|
||||
log.Warn().Err(err).Msg("[app] read config")
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
const name = "go2rtc.json"
|
||||
|
||||
var store map[string]interface{}
|
||||
var store map[string]any
|
||||
|
||||
func load() {
|
||||
data, _ := os.ReadFile(name)
|
||||
@@ -20,7 +20,7 @@ func load() {
|
||||
}
|
||||
|
||||
if store == nil {
|
||||
store = make(map[string]interface{})
|
||||
store = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func save() error {
|
||||
return os.WriteFile(name, data, 0644)
|
||||
}
|
||||
|
||||
func GetRaw(key string) interface{} {
|
||||
func GetRaw(key string) any {
|
||||
if store == nil {
|
||||
load()
|
||||
}
|
||||
@@ -41,16 +41,16 @@ func GetRaw(key string) interface{} {
|
||||
return store[key]
|
||||
}
|
||||
|
||||
func GetDict(key string) map[string]interface{} {
|
||||
func GetDict(key string) map[string]any {
|
||||
raw := GetRaw(key)
|
||||
if raw != nil {
|
||||
return raw.(map[string]interface{})
|
||||
return raw.(map[string]any)
|
||||
}
|
||||
|
||||
return make(map[string]interface{})
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
func Set(key string, v interface{}) error {
|
||||
func Set(key string, v any) error {
|
||||
if store == nil {
|
||||
load()
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ package debug
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -12,6 +12,6 @@ func Init() {
|
||||
streams.HandleFunc("null", nullHandler)
|
||||
}
|
||||
|
||||
func nullHandler(string) (streamer.Producer, error) {
|
||||
func nullHandler(string) (core.Producer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,15 +2,15 @@ package dvrip
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("dvrip", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (streamer.Producer, error) {
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := dvrip.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
|
||||
+2
-2
@@ -4,15 +4,15 @@ import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"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) {
|
||||
streams.HandleFunc("echo", func(url string) (core.Producer, error) {
|
||||
args := shell.QuoteSplit(url[5:])
|
||||
|
||||
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||
|
||||
+4
-4
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -48,7 +48,7 @@ func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
func Handle(url string) (streamer.Producer, error) {
|
||||
func Handle(url string) (core.Producer, error) {
|
||||
sum := md5.Sum([]byte(url))
|
||||
path := "/" + hex.EncodeToString(sum[:])
|
||||
|
||||
@@ -67,7 +67,7 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
ch := make(chan streamer.Producer)
|
||||
ch := make(chan core.Producer)
|
||||
|
||||
waitersMu.Lock()
|
||||
waiters[path] = ch
|
||||
@@ -116,5 +116,5 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
// internal
|
||||
|
||||
var log zerolog.Logger
|
||||
var waiters = map[string]chan streamer.Producer{}
|
||||
var waiters = map[string]chan core.Producer{}
|
||||
var waitersMu sync.Mutex
|
||||
|
||||
@@ -2,7 +2,7 @@ package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
const deviceInputPrefix = "-f avfoundation"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(streamer.KindVideo, videoIdx)
|
||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
audio := findMedia(core.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `"` + video.MID + `:` + audio.MID + `"`
|
||||
return `"` + video.ID + `:` + audio.ID + `"`
|
||||
case video != nil:
|
||||
return `"` + video.MID + `"`
|
||||
return `"` + video.ID + `"`
|
||||
case audio != nil:
|
||||
return `"` + audio.MID + `"`
|
||||
return `"` + audio.ID + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -40,10 +40,10 @@ process:
|
||||
for _, line := range lines {
|
||||
switch {
|
||||
case strings.HasSuffix(line, "video devices:"):
|
||||
kind = streamer.KindVideo
|
||||
kind = core.KindVideo
|
||||
continue
|
||||
case strings.HasSuffix(line, "audio devices:"):
|
||||
kind = streamer.KindAudio
|
||||
kind = core.KindAudio
|
||||
continue
|
||||
case strings.HasPrefix(line, "dummy"):
|
||||
break process
|
||||
@@ -56,6 +56,6 @@ process:
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *streamer.Media {
|
||||
return &streamer.Media{Kind: kind, MID: name}
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
const deviceInputPrefix = "-f v4l2"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(streamer.KindVideo, videoIdx)
|
||||
return video.MID
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
return video.ID
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
@@ -23,8 +23,8 @@ func loadMedias() {
|
||||
}
|
||||
for _, file := range files {
|
||||
log.Trace().Msg("[ffmpeg] " + file.Name())
|
||||
if strings.HasPrefix(file.Name(), streamer.KindVideo) {
|
||||
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name())
|
||||
if strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
|
||||
if media != nil {
|
||||
medias = append(medias, media)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func loadMedias() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *streamer.Media {
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
||||
)
|
||||
@@ -44,5 +44,5 @@ func loadMedia(kind, name string) *streamer.Media {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &streamer.Media{Kind: kind, MID: name}
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
const deviceInputPrefix = "-f dshow"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(streamer.KindVideo, videoIdx)
|
||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
audio := findMedia(core.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `video="` + video.MID + `":audio=` + audio.MID + `"`
|
||||
return `video="` + video.ID + `":audio=` + audio.ID + `"`
|
||||
case video != nil:
|
||||
return `video="` + video.MID + `"`
|
||||
return `video="` + video.ID + `"`
|
||||
case audio != nil:
|
||||
return `audio="` + audio.MID + `"`
|
||||
return `audio="` + audio.ID + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -37,9 +37,9 @@ func loadMedias() {
|
||||
for _, line := range lines {
|
||||
var kind string
|
||||
if strings.HasSuffix(line, "(video)") {
|
||||
kind = streamer.KindVideo
|
||||
kind = core.KindVideo
|
||||
} else if strings.HasSuffix(line, "(audio)") {
|
||||
kind = streamer.KindAudio
|
||||
kind = core.KindAudio
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
@@ -52,6 +52,6 @@ func loadMedias() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *streamer.Media {
|
||||
return &streamer.Media{Kind: kind, MID: name}
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -52,9 +51,9 @@ func GetInput(src string) (string, error) {
|
||||
|
||||
var Bin string
|
||||
var log zerolog.Logger
|
||||
var medias []*streamer.Media
|
||||
var medias []*core.Media
|
||||
|
||||
func findMedia(kind string, index int) *streamer.Media {
|
||||
func findMedia(kind string, index int) *core.Media {
|
||||
for _, media := range medias {
|
||||
if media.Kind != kind {
|
||||
continue
|
||||
@@ -72,12 +71,21 @@ func handle(w http.ResponseWriter, r *http.Request) {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
data, err := json.Marshal(medias)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.ffmpeg]")
|
||||
return
|
||||
}
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.ffmpeg]")
|
||||
var items []api.Stream
|
||||
var iv, ia int
|
||||
|
||||
for _, media := range medias {
|
||||
var source string
|
||||
switch media.Kind {
|
||||
case core.KindVideo:
|
||||
source = "ffmpeg:device?video=" + strconv.Itoa(iv)
|
||||
iv++
|
||||
case core.KindAudio:
|
||||
source = "ffmpeg:device?audio=" + strconv.Itoa(ia)
|
||||
ia++
|
||||
}
|
||||
items = append(items, api.Stream{Name: media.ID, URL: source})
|
||||
}
|
||||
|
||||
api.ResponseStreams(w, items)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -27,7 +27,7 @@ func Init() {
|
||||
defaults["global"] += " -v error"
|
||||
}
|
||||
|
||||
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
|
||||
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
|
||||
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
||||
if args == nil {
|
||||
return nil, errors.New("can't generate ffmpeg command")
|
||||
@@ -70,6 +70,10 @@ var defaults = map[string]string{
|
||||
"aac": "-c:a aac", // keep sample rate and channels
|
||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||
"mp3": "-c:a libmp3lame -q:a 8",
|
||||
"pcm": "-c:a pcm_s16be",
|
||||
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||
|
||||
// hardware Intel and AMD on Linux
|
||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||
|
||||
+2
-2
@@ -79,7 +79,7 @@ func initAPI() {
|
||||
return
|
||||
}
|
||||
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
||||
return
|
||||
@@ -117,7 +117,7 @@ func initAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
str, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
+45
-24
@@ -1,12 +1,16 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
@@ -26,19 +30,27 @@ func Init() {
|
||||
|
||||
// support load cameras from Hass config file
|
||||
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
||||
data, err := os.ReadFile(filename)
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
storage := new(entries)
|
||||
if err = json.Unmarshal(data, storage); err != nil {
|
||||
if err = json.Unmarshal(b, storage); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urls := map[string]string{}
|
||||
|
||||
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
|
||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, r *http.Request) {
|
||||
var items []api.Stream
|
||||
for name, url := range urls {
|
||||
items = append(items, api.Stream{Name: name, URL: url})
|
||||
}
|
||||
api.ResponseStreams(w, items)
|
||||
})
|
||||
|
||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
||||
if hurl := urls[url[5:]]; hurl != "" {
|
||||
return streams.GetProducer(hurl)
|
||||
}
|
||||
@@ -48,22 +60,41 @@ func Init() {
|
||||
for _, entrie := range storage.Data.Entries {
|
||||
switch entrie.Domain {
|
||||
case "generic":
|
||||
if entrie.Options.StreamSource == "" {
|
||||
var options struct {
|
||||
StreamSource string `json:"stream_source"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Options, &options); err != nil {
|
||||
continue
|
||||
}
|
||||
urls[entrie.Title] = entrie.Options.StreamSource
|
||||
urls[entrie.Title] = options.StreamSource
|
||||
|
||||
case "homekit_controller":
|
||||
if entrie.Data.ClientID == "" {
|
||||
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
|
||||
continue
|
||||
}
|
||||
|
||||
var data struct {
|
||||
ClientID string `json:"iOSPairingId"`
|
||||
ClientPrivate string `json:"iOSDeviceLTSK"`
|
||||
ClientPublic string `json:"iOSDeviceLTPK"`
|
||||
DeviceID string `json:"AccessoryPairingID"`
|
||||
DevicePublic string `json:"AccessoryLTPK"`
|
||||
DeviceHost string `json:"AccessoryIP"`
|
||||
DevicePort uint16 `json:"AccessoryPort"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Data, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
urls[entrie.Title] = fmt.Sprintf(
|
||||
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
|
||||
entrie.Data.DeviceHost, entrie.Data.DevicePort,
|
||||
entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
|
||||
entrie.Data.DeviceID, entrie.Data.DevicePublic,
|
||||
data.DeviceHost, data.DevicePort,
|
||||
data.ClientID, data.ClientPrivate, data.ClientPublic,
|
||||
data.DeviceID, data.DevicePublic,
|
||||
)
|
||||
|
||||
case "roborock":
|
||||
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
@@ -78,20 +109,10 @@ var log zerolog.Logger
|
||||
type entries struct {
|
||||
Data struct {
|
||||
Entries []struct {
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data struct {
|
||||
ClientID string `json:"iOSPairingId"`
|
||||
ClientPrivate string `json:"iOSDeviceLTSK"`
|
||||
ClientPublic string `json:"iOSDeviceLTPK"`
|
||||
DeviceID string `json:"AccessoryPairingID"`
|
||||
DevicePublic string `json:"AccessoryLTPK"`
|
||||
DeviceHost string `json:"AccessoryIP"`
|
||||
DevicePort uint16 `json:"AccessoryPort"`
|
||||
} `json:"data"`
|
||||
Options struct {
|
||||
StreamSource string `json:"stream_source"`
|
||||
}
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
} `json:"entries"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -27,7 +27,7 @@ func Init() {
|
||||
}
|
||||
|
||||
type Consumer interface {
|
||||
streamer.Consumer
|
||||
core.Consumer
|
||||
Init() ([]byte, error)
|
||||
MimeCodecs() string
|
||||
Start()
|
||||
@@ -83,7 +83,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
session := &Session{cons: cons}
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.(any).(*core.Listener).Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
session.mu.Lock()
|
||||
session.segment = append(session.segment, data...)
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import (
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
items := make([]interface{}, 0)
|
||||
items := make([]any, 0)
|
||||
|
||||
for name, src := range store.GetDict("streams") {
|
||||
if src := src.(string); strings.HasPrefix(src, "homekit") {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -20,12 +20,12 @@ func Init() {
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamHandler(url string) (streamer.Producer, error) {
|
||||
func streamHandler(url string) (core.Producer, error) {
|
||||
conn, err := homekit.NewClient(url, srtp.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Dial();err!=nil{
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -18,7 +18,7 @@ func Init() {
|
||||
streams.HandleFunc("https", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (streamer.Producer, error) {
|
||||
func handle(url string) (core.Producer, error) {
|
||||
// first we get the Content-Type to define supported producer
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package isapi
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/isapi"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("isapi", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn, err := isapi.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package ivideon
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
|
||||
streams.HandleFunc("ivideon", func(url string) (core.Producer, error) {
|
||||
id := strings.Replace(url[8:], "/", ":", 1)
|
||||
prod := ivideon.NewClient(id)
|
||||
if err := prod.Dial(); err != nil {
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
exit <- msg
|
||||
@@ -84,7 +84,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
data := []byte(header + strconv.Itoa(len(msg)))
|
||||
@@ -149,7 +149,7 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
|
||||
RemoteAddr: tr.Request.RemoteAddr,
|
||||
UserAgent: tr.Request.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
tr.Write(data)
|
||||
}
|
||||
|
||||
+4
-4
@@ -4,8 +4,8 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -46,7 +46,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
exit := make(chan []byte)
|
||||
|
||||
cons := &mp4.Segment{OnlyKeyframe: true}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok && exit != nil {
|
||||
exit <- data
|
||||
exit = nil
|
||||
@@ -105,10 +105,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
cons := &mp4.Consumer{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
UserAgent: r.UserAgent(),
|
||||
Medias: streamer.ParseQuery(r.URL.Query()),
|
||||
Medias: core.ParseQuery(r.URL.Query()),
|
||||
}
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
if _, err := w.Write(data); err != nil && exit != nil {
|
||||
exit <- err
|
||||
|
||||
+18
-24
@@ -4,13 +4,11 @@ import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const packetSize = 1400
|
||||
|
||||
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
||||
src := tr.Request.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
@@ -23,17 +21,13 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
||||
UserAgent: tr.Request.UserAgent(),
|
||||
}
|
||||
|
||||
if codecs, ok := msg.Value.(string); ok {
|
||||
if codecs := msg.String(); codecs != "" {
|
||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
||||
cons.Medias = parseMedias(codecs, true)
|
||||
}
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
for len(data) > packetSize {
|
||||
tr.Write(data[:packetSize])
|
||||
data = data[packetSize:]
|
||||
}
|
||||
tr.Write(data)
|
||||
}
|
||||
})
|
||||
@@ -75,12 +69,12 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
||||
OnlyKeyframe: true,
|
||||
}
|
||||
|
||||
if codecs, ok := msg.Value.(string); ok {
|
||||
if codecs := msg.String(); codecs != "" {
|
||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
||||
cons.Medias = parseMedias(codecs, false)
|
||||
}
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
tr.Write(data)
|
||||
}
|
||||
@@ -100,40 +94,40 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
|
||||
var videos []*streamer.Codec
|
||||
var audios []*streamer.Codec
|
||||
func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
|
||||
var videos []*core.Codec
|
||||
var audios []*core.Codec
|
||||
|
||||
for _, name := range strings.Split(codecs, ",") {
|
||||
switch name {
|
||||
case mp4.MimeH264:
|
||||
codec := &streamer.Codec{Name: streamer.CodecH264}
|
||||
codec := &core.Codec{Name: core.CodecH264}
|
||||
videos = append(videos, codec)
|
||||
case mp4.MimeH265:
|
||||
codec := &streamer.Codec{Name: streamer.CodecH265}
|
||||
codec := &core.Codec{Name: core.CodecH265}
|
||||
videos = append(videos, codec)
|
||||
case mp4.MimeAAC:
|
||||
codec := &streamer.Codec{Name: streamer.CodecAAC}
|
||||
codec := &core.Codec{Name: core.CodecAAC}
|
||||
audios = append(audios, codec)
|
||||
case mp4.MimeOpus:
|
||||
codec := &streamer.Codec{Name: streamer.CodecOpus}
|
||||
codec := &core.Codec{Name: core.CodecOpus}
|
||||
audios = append(audios, codec)
|
||||
}
|
||||
}
|
||||
|
||||
if videos != nil {
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: videos,
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
if audios != nil && parseAudio {
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: audios,
|
||||
}
|
||||
medias = append(medias, media)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ func Init() {
|
||||
log.Error().Err(err).Msg("[ngrok] start")
|
||||
}
|
||||
|
||||
ngr.Listen(func(msg interface{}) {
|
||||
ngr.Listen(func(msg any) {
|
||||
if msg := msg.(*ngrok.Message); msg != nil {
|
||||
if strings.HasPrefix(msg.Line, "ERROR:") {
|
||||
log.Warn().Msg("[ngrok] " + msg.Line)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package roborock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("roborock", handle)
|
||||
|
||||
api.HandleFunc("api/roborock", apiHandle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := roborock.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
var Auth struct {
|
||||
UserData *roborock.UserInfo `json:"user_data"`
|
||||
BaseURL string `json:"base_url"`
|
||||
}
|
||||
|
||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
if Auth.UserData == nil {
|
||||
http.Error(w, "no auth", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
case "POST":
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.Form.Get("username")
|
||||
password := r.Form.Get("password")
|
||||
if username == "" || password == "" {
|
||||
http.Error(w, "empty username or password", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
base, err := roborock.GetBaseURL(username)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ui, err := roborock.Login(base, username, password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Auth.BaseURL = base
|
||||
Auth.UserData = ui
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
homeID, err := roborock.GetHomeID(Auth.BaseURL, Auth.UserData.Token)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := roborock.GetDevices(Auth.UserData, homeID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var items []api.Stream
|
||||
|
||||
for _, device := range devices {
|
||||
source := fmt.Sprintf(
|
||||
"roborock://%s?u=%s&s=%s&k=%s&did=%s&key=%s&pin=",
|
||||
Auth.UserData.IoT.URL.MQTT[6:],
|
||||
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
|
||||
device.DID, device.Key,
|
||||
)
|
||||
items = append(items, api.Stream{Name: device.Name, URL: source})
|
||||
}
|
||||
|
||||
api.ResponseStreams(w, items)
|
||||
}
|
||||
+2
-2
@@ -3,8 +3,8 @@ package rtmp
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog/log"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -16,7 +16,7 @@ func Init() {
|
||||
api.HandleFunc("api/stream.flv", apiHandle)
|
||||
}
|
||||
|
||||
func streamsHandle(url string) (streamer.Producer, error) {
|
||||
func streamsHandle(url string) (core.Producer, error) {
|
||||
conn := rtmp.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
|
||||
+8
-15
@@ -3,9 +3,9 @@ package rtsp
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
@@ -86,9 +86,9 @@ var Port string
|
||||
|
||||
var log zerolog.Logger
|
||||
var handlers []Handler
|
||||
var defaultMedias []*streamer.Media
|
||||
var defaultMedias []*core.Media
|
||||
|
||||
func rtspHandler(url string) (streamer.Producer, error) {
|
||||
func rtspHandler(url string) (core.Producer, error) {
|
||||
backchannel := true
|
||||
|
||||
if i := strings.IndexByte(url, '#'); i > 0 {
|
||||
@@ -98,15 +98,11 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
url = url[:i]
|
||||
}
|
||||
|
||||
conn, err := rtsp.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := rtsp.NewClient(url)
|
||||
conn.UserAgent = app.UserAgent
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
conn.Listen(func(msg interface{}) {
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *tcp.Request:
|
||||
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
|
||||
@@ -118,12 +114,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
})
|
||||
}
|
||||
|
||||
if err = conn.Dial(); err != nil {
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn.Backchannel = backchannel
|
||||
if err = conn.Describe(); err != nil {
|
||||
if err := conn.Describe(); err != nil {
|
||||
if !backchannel {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,7 +143,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
|
||||
trace := log.Trace().Enabled()
|
||||
|
||||
conn.Listen(func(msg interface{}) {
|
||||
conn.Listen(func(msg any) {
|
||||
if trace {
|
||||
switch msg := msg.(type) {
|
||||
case *tcp.Request:
|
||||
@@ -211,9 +207,6 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
closer = func() {
|
||||
stream.RemoveProducer(conn)
|
||||
}
|
||||
|
||||
case streamer.StatePlaying:
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] start")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
element streamer.Consumer
|
||||
tracks []*streamer.Track
|
||||
}
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.element)
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package streams
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Handler func(url string) (streamer.Producer, error)
|
||||
type Handler func(url string) (core.Producer, error)
|
||||
|
||||
var handlers = map[string]Handler{}
|
||||
var handlersMu sync.Mutex
|
||||
@@ -32,7 +32,7 @@ func HasProducer(url string) bool {
|
||||
return getHandler(url) != nil
|
||||
}
|
||||
|
||||
func GetProducer(url string) (streamer.Producer, error) {
|
||||
func GetProducer(url string) (core.Producer, error) {
|
||||
handler := getHandler(url)
|
||||
if handler == nil {
|
||||
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod map[string]interface{} `yaml:"streams"`
|
||||
Mod map[string]any `yaml:"streams"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
@@ -33,7 +33,7 @@ func Get(name string) *Stream {
|
||||
return streams[name]
|
||||
}
|
||||
|
||||
func New(name string, source interface{}) *Stream {
|
||||
func New(name string, source any) *Stream {
|
||||
stream := NewStream(source)
|
||||
streams[name] = stream
|
||||
return stream
|
||||
+74
-39
@@ -2,14 +2,14 @@ package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (s *Stream) Play(source string) error {
|
||||
s.mu.Lock()
|
||||
for _, producer := range s.producers {
|
||||
if producer.state == stateInternal && producer.element != nil {
|
||||
_ = producer.element.Stop()
|
||||
if producer.state == stateInternal && producer.conn != nil {
|
||||
_ = producer.conn.Stop()
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
@@ -18,41 +18,77 @@ func (s *Stream) Play(source string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var src core.Producer
|
||||
|
||||
for _, producer := range s.producers {
|
||||
// start new client
|
||||
client, err := GetProducer(producer.url)
|
||||
if err != nil {
|
||||
return err
|
||||
if producer.conn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if client support consumer interface
|
||||
cons, ok := client.(streamer.Consumer)
|
||||
cons, ok := producer.conn.(core.Consumer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// start new producer
|
||||
prod, err := GetProducer(source)
|
||||
if src == nil {
|
||||
var err error
|
||||
if src, err = GetProducer(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !matchMedia(src, cons) {
|
||||
continue
|
||||
}
|
||||
|
||||
s.AddInternalProducer(src)
|
||||
|
||||
go func() {
|
||||
_ = src.Start()
|
||||
s.RemoveProducer(src)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, producer := range s.producers {
|
||||
// start new client
|
||||
dst, err := GetProducer(producer.url)
|
||||
if err != nil {
|
||||
return err
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchMedia(prod, cons) {
|
||||
return errors.New("can't match media")
|
||||
// check if client support consumer interface
|
||||
cons, ok := dst.(core.Consumer)
|
||||
if !ok {
|
||||
_ = dst.Stop()
|
||||
continue
|
||||
}
|
||||
|
||||
s.AddInternalProducer(prod)
|
||||
// start new producer
|
||||
if src == nil {
|
||||
if src, err = GetProducer(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !matchMedia(src, cons) {
|
||||
_ = dst.Stop()
|
||||
continue
|
||||
}
|
||||
|
||||
s.AddInternalProducer(src)
|
||||
s.AddInternalConsumer(cons)
|
||||
|
||||
go func() {
|
||||
_ = prod.Start()
|
||||
_ = client.Stop()
|
||||
s.RemoveProducer(prod)
|
||||
_ = src.Start()
|
||||
_ = dst.Stop()
|
||||
s.RemoveProducer(src)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = client.Start()
|
||||
_ = prod.Stop()
|
||||
_ = dst.Start()
|
||||
_ = src.Stop()
|
||||
s.RemoveInternalConsumer(cons)
|
||||
}()
|
||||
|
||||
@@ -62,50 +98,49 @@ func (s *Stream) Play(source string) error {
|
||||
return errors.New("can't find consumer")
|
||||
}
|
||||
|
||||
func (s *Stream) AddInternalProducer(prod streamer.Producer) {
|
||||
producer := &Producer{element: prod, state: stateInternal}
|
||||
func (s *Stream) AddInternalProducer(conn core.Producer) {
|
||||
producer := &Producer{conn: conn, state: stateInternal}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) AddInternalConsumer(cons streamer.Consumer) {
|
||||
consumer := &Consumer{element: cons}
|
||||
func (s *Stream) AddInternalConsumer(conn core.Consumer) {
|
||||
s.mu.Lock()
|
||||
s.consumers = append(s.consumers, consumer)
|
||||
s.consumers = append(s.consumers, conn)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveInternalConsumer(cons streamer.Consumer) {
|
||||
func (s *Stream) RemoveInternalConsumer(conn core.Consumer) {
|
||||
s.mu.Lock()
|
||||
for i, consumer := range s.consumers {
|
||||
if consumer.element == cons {
|
||||
s.removeConsumer(i)
|
||||
if consumer == conn {
|
||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func matchMedia(prod streamer.Producer, cons streamer.Consumer) bool {
|
||||
func matchMedia(prod core.Producer, cons core.Consumer) bool {
|
||||
for _, consMedia := range cons.GetMedias() {
|
||||
for _, prodMedia := range prod.GetMedias() {
|
||||
// codec negotiation
|
||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodMedia.Direction != core.DirectionRecvonly {
|
||||
continue
|
||||
}
|
||||
|
||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodCodec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// setup producer track
|
||||
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
||||
if prodTrack == nil {
|
||||
return false
|
||||
track, err := prod.GetTrack(prodMedia, prodCodec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// setup consumer track
|
||||
consTrack := cons.AddTrack(consMedia, prodTrack)
|
||||
if consTrack == nil {
|
||||
return false
|
||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
+117
-83
@@ -2,7 +2,8 @@ package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -20,20 +21,95 @@ const (
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
url string
|
||||
template string
|
||||
|
||||
element streamer.Producer
|
||||
conn core.Producer
|
||||
receivers []*core.Receiver
|
||||
senders []*core.Receiver
|
||||
|
||||
lastErr error
|
||||
tracks []*streamer.Track
|
||||
|
||||
state state
|
||||
mu sync.Mutex
|
||||
workerID int
|
||||
}
|
||||
|
||||
func (p *Producer) Dial() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
conn, err := GetProducer(p.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.conn = conn
|
||||
p.state = stateMedias
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Producer) GetMedias() []*core.Media {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.conn.GetMedias()
|
||||
}
|
||||
|
||||
func (p *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
return nil, errors.New("get track from none state")
|
||||
}
|
||||
|
||||
for _, track := range p.receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
|
||||
track, err := p.conn.GetTrack(media, codec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.receivers = append(p.receivers, track)
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
return errors.New("add track from none state")
|
||||
}
|
||||
|
||||
if err := p.conn.(core.Consumer).AddTrack(media, codec, track); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.senders = append(p.senders, track)
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Producer) SetSource(s string) {
|
||||
if p.template == "" {
|
||||
p.template = p.url
|
||||
@@ -41,64 +117,12 @@ func (p *Producer) SetSource(s string) {
|
||||
p.url = strings.Replace(p.template, "{input}", s, 1)
|
||||
}
|
||||
|
||||
func (p *Producer) GetMedias() []*streamer.Media {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
|
||||
|
||||
p.element, p.lastErr = GetProducer(p.url)
|
||||
if p.lastErr != nil || p.element == nil {
|
||||
log.Error().Err(p.lastErr).Str("url", p.url).Caller().Send()
|
||||
return nil
|
||||
}
|
||||
|
||||
p.state = stateMedias
|
||||
}
|
||||
|
||||
// if element in reconnect state
|
||||
if p.element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.element.GetMedias()
|
||||
}
|
||||
|
||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, track := range p.tracks {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
track := p.element.GetTrack(media, codec)
|
||||
if track == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.tracks = append(p.tracks, track)
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if p.element != nil {
|
||||
return json.Marshal(p.element)
|
||||
if p.conn != nil {
|
||||
return json.Marshal(p.conn)
|
||||
}
|
||||
|
||||
info := streamer.Info{URL: p.url}
|
||||
info := core.Info{URL: p.url}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -117,11 +141,11 @@ func (p *Producer) start() {
|
||||
p.state = stateStart
|
||||
p.workerID++
|
||||
|
||||
go p.worker(p.element, p.workerID)
|
||||
go p.worker(p.conn, p.workerID)
|
||||
}
|
||||
|
||||
func (p *Producer) worker(element streamer.Producer, workerID int) {
|
||||
if err := element.Start(); err != nil {
|
||||
func (p *Producer) worker(conn core.Producer, workerID int) {
|
||||
if err := conn.Start(); err != nil {
|
||||
p.mu.Lock()
|
||||
closed := p.workerID != workerID
|
||||
p.mu.Unlock()
|
||||
@@ -147,9 +171,8 @@ func (p *Producer) reconnect(workerID int) {
|
||||
|
||||
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
|
||||
|
||||
p.element, p.lastErr = GetProducer(p.url)
|
||||
if p.lastErr != nil || p.element == nil {
|
||||
log.Debug().Msgf("[streams] producer=%s", p.lastErr)
|
||||
if err := p.Dial(); err != nil {
|
||||
log.Debug().Msgf("[streams] producer=%s", err)
|
||||
// TODO: dynamic timeout
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
p.reconnect(workerID)
|
||||
@@ -157,27 +180,37 @@ func (p *Producer) reconnect(workerID int) {
|
||||
return
|
||||
}
|
||||
|
||||
medias := p.element.GetMedias()
|
||||
for _, media := range p.conn.GetMedias() {
|
||||
switch media.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
for _, receiver := range p.receivers {
|
||||
codec := media.MatchCodec(receiver.Codec)
|
||||
if codec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
track, err := p.conn.GetTrack(media, codec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
receiver.Replace(track)
|
||||
break
|
||||
}
|
||||
|
||||
// move sink from old track to new track
|
||||
newTrack := p.element.GetTrack(media, codec)
|
||||
newTrack.GetSink(oldTrack)
|
||||
p.tracks[i] = newTrack
|
||||
case core.DirectionSendonly:
|
||||
for _, sender := range p.senders {
|
||||
codec := media.MatchCodec(sender.Codec)
|
||||
if codec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
_ = p.conn.(core.Consumer).AddTrack(media, codec, sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go p.worker(p.element, workerID)
|
||||
go p.worker(p.conn, workerID)
|
||||
}
|
||||
|
||||
func (p *Producer) stop() {
|
||||
@@ -197,11 +230,12 @@ func (p *Producer) stop() {
|
||||
|
||||
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
|
||||
|
||||
if p.element != nil {
|
||||
_ = p.element.Stop()
|
||||
p.element = nil
|
||||
if p.conn != nil {
|
||||
_ = p.conn.Stop()
|
||||
p.conn = nil
|
||||
}
|
||||
|
||||
p.state = stateNone
|
||||
p.tracks = nil
|
||||
p.receivers = nil
|
||||
p.senders = nil
|
||||
}
|
||||
|
||||
+65
-94
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -12,19 +12,19 @@ import (
|
||||
|
||||
type Stream struct {
|
||||
producers []*Producer
|
||||
consumers []*Consumer
|
||||
consumers []core.Consumer
|
||||
mu sync.Mutex
|
||||
requests int32
|
||||
}
|
||||
|
||||
func NewStream(source interface{}) *Stream {
|
||||
func NewStream(source any) *Stream {
|
||||
switch source := source.(type) {
|
||||
case string:
|
||||
s := new(Stream)
|
||||
prod := &Producer{url: source}
|
||||
s.producers = append(s.producers, prod)
|
||||
return s
|
||||
case []interface{}:
|
||||
case []any:
|
||||
s := new(Stream)
|
||||
for _, source := range source {
|
||||
prod := &Producer{url: source.(string)}
|
||||
@@ -33,12 +33,12 @@ func NewStream(source interface{}) *Stream {
|
||||
return s
|
||||
case *Stream:
|
||||
return source
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
return NewStream(source["url"])
|
||||
case nil:
|
||||
return new(Stream)
|
||||
default:
|
||||
panic("wrong source type")
|
||||
panic(core.Caller())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,57 +48,71 @@ func (s *Stream) SetSource(source string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
// support for multiple simultaneous requests from different consumers
|
||||
atomic.AddInt32(&s.requests, 1)
|
||||
|
||||
ic := len(s.consumers)
|
||||
|
||||
consumer := &Consumer{element: cons}
|
||||
var producers []*Producer // matched producers for consumer
|
||||
|
||||
var codecs string
|
||||
|
||||
// Step 1. Get consumer medias
|
||||
for icc, consMedia := range cons.GetMedias() {
|
||||
log.Trace().Stringer("media", consMedia).
|
||||
Msgf("[streams] consumer=%d candidate=%d", ic, icc)
|
||||
for _, consMedia := range cons.GetMedias() {
|
||||
|
||||
producers:
|
||||
for ip, prod := range s.producers {
|
||||
// Step 2. Get producer medias (not tracks yet)
|
||||
for ipc, prodMedia := range prod.GetMedias() {
|
||||
log.Trace().Stringer("media", prodMedia).
|
||||
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
|
||||
for _, prod := range s.producers {
|
||||
if err = prod.Dial(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2. Get producer medias (not tracks yet)
|
||||
for _, prodMedia := range prod.GetMedias() {
|
||||
collectCodecs(prodMedia, &codecs)
|
||||
|
||||
// Step 3. Match consumer/producer codecs list
|
||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodCodec != nil {
|
||||
log.Trace().Stringer("codec", prodCodec).
|
||||
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
|
||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodCodec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 4. Get producer track
|
||||
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
||||
if prodTrack == nil {
|
||||
log.Warn().Str("url", prod.url).Msg("[streams] can't get track")
|
||||
var track *core.Receiver
|
||||
|
||||
switch prodMedia.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
// Step 4. Get recvonly track from producer
|
||||
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't get track")
|
||||
continue
|
||||
}
|
||||
// Step 5. Add track to consumer
|
||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't add track")
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 5. Add track to consumer and get new track
|
||||
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
||||
|
||||
consumer.tracks = append(consumer.tracks, consTrack)
|
||||
producers = append(producers, prod)
|
||||
if !consMedia.MatchAll() {
|
||||
break producers
|
||||
case core.DirectionSendonly:
|
||||
// Step 4. Get recvonly track from consumer (backchannel)
|
||||
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't get track")
|
||||
continue
|
||||
}
|
||||
// Step 5. Add track to producer
|
||||
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't add track")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
producers = append(producers, prod)
|
||||
|
||||
if !consMedia.MatchAll() {
|
||||
break producers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop producers if they don't have readers
|
||||
if atomic.AddInt32(&s.requests, -1) == 0 {
|
||||
s.stopProducers()
|
||||
}
|
||||
@@ -118,7 +132,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.consumers = append(s.consumers, consumer)
|
||||
s.consumers = append(s.consumers, cons)
|
||||
s.mu.Unlock()
|
||||
|
||||
// there may be duplicates, but that's not a problem
|
||||
@@ -129,16 +143,13 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
||||
func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
||||
_ = cons.Stop()
|
||||
|
||||
s.mu.Lock()
|
||||
for i, consumer := range s.consumers {
|
||||
if consumer.element == cons {
|
||||
// remove consumer pads from all producers
|
||||
for _, track := range consumer.tracks {
|
||||
track.Unbind()
|
||||
}
|
||||
// remove consumer from slice
|
||||
s.removeConsumer(i)
|
||||
if consumer == cons {
|
||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -147,18 +158,18 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
||||
s.stopProducers()
|
||||
}
|
||||
|
||||
func (s *Stream) AddProducer(prod streamer.Producer) {
|
||||
producer := &Producer{element: prod, state: stateExternal}
|
||||
func (s *Stream) AddProducer(prod core.Producer) {
|
||||
producer := &Producer{conn: prod, state: stateExternal}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
||||
func (s *Stream) RemoveProducer(prod core.Producer) {
|
||||
s.mu.Lock()
|
||||
for i, producer := range s.producers {
|
||||
if producer.element == prod {
|
||||
s.removeProducer(i)
|
||||
if producer.conn == prod {
|
||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -169,8 +180,8 @@ func (s *Stream) stopProducers() {
|
||||
s.mu.Lock()
|
||||
producers:
|
||||
for _, producer := range s.producers {
|
||||
for _, track := range producer.tracks {
|
||||
if track.HasSink() {
|
||||
for _, track := range producer.receivers {
|
||||
if len(track.Senders()) > 0 {
|
||||
continue producers
|
||||
}
|
||||
}
|
||||
@@ -179,20 +190,6 @@ producers:
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
//func (s *Stream) Active() bool {
|
||||
// if len(s.consumers) > 0 {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for _, prod := range s.producers {
|
||||
// if prod.element != nil {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
//}
|
||||
|
||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
if !s.mu.TryLock() {
|
||||
log.Warn().Msgf("[streams] json locked")
|
||||
@@ -200,8 +197,8 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
var info struct {
|
||||
Producers []*Producer `json:"producers"`
|
||||
Consumers []*Consumer `json:"consumers"`
|
||||
Producers []*Producer `json:"producers"`
|
||||
Consumers []core.Consumer `json:"consumers"`
|
||||
}
|
||||
info.Producers = s.producers
|
||||
info.Consumers = s.consumers
|
||||
@@ -211,40 +208,14 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
func (s *Stream) removeConsumer(i int) {
|
||||
switch {
|
||||
case len(s.consumers) == 1: // only one element
|
||||
s.consumers = nil
|
||||
case i == 0: // first element
|
||||
s.consumers = s.consumers[1:]
|
||||
case i == len(s.consumers)-1: // last element
|
||||
s.consumers = s.consumers[:i]
|
||||
default: // middle element
|
||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) removeProducer(i int) {
|
||||
switch {
|
||||
case len(s.producers) == 1: // only one element
|
||||
s.producers = nil
|
||||
case i == 0: // first element
|
||||
s.producers = s.producers[1:]
|
||||
case i == len(s.producers)-1: // last element
|
||||
s.producers = s.producers[:i]
|
||||
default: // middle element
|
||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func collectCodecs(media *streamer.Media, codecs *string) {
|
||||
if media.Direction == streamer.DirectionRecvonly {
|
||||
func collectCodecs(media *core.Media, codecs *string) {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
return
|
||||
}
|
||||
|
||||
for _, codec := range media.Codecs {
|
||||
name := codec.Name
|
||||
if name == streamer.CodecAAC {
|
||||
if name == core.CodecAAC {
|
||||
name = "AAC"
|
||||
}
|
||||
if strings.Contains(*codecs, name) {
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ package tapo
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tapo"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ func Init() {
|
||||
streams.HandleFunc("tapo", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (streamer.Producer, error) {
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := tapo.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
## Userful links
|
||||
|
||||
- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html
|
||||
- https://www.ietf.org/id/draft-murillo-whep-01.html
|
||||
- https://github.com/Glimesh/broadcast-box/
|
||||
- https://github.com/obsproject/obs-studio/pull/7926
|
||||
- https://misi.github.io/webrtc-c0d3l4b/
|
||||
- https://github.com/webtorrent/webtorrent/blob/master/docs/faq.md
|
||||
+80
-54
@@ -4,39 +4,82 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var candidates []string
|
||||
var networks = []string{"udp", "tcp"}
|
||||
|
||||
func AddCandidate(address string) {
|
||||
candidates = append(candidates, address)
|
||||
type Address struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func asyncCandidates(tr *api.Transport) {
|
||||
for _, address := range candidates {
|
||||
address, err := webrtc.LookupIP(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
continue
|
||||
}
|
||||
var addresses []Address
|
||||
|
||||
for _, network := range networks {
|
||||
cand, err := webrtc.NewCandidate(network, address)
|
||||
func AddCandidate(address string) {
|
||||
var port int
|
||||
|
||||
// try to get port from address string
|
||||
if i := strings.LastIndexByte(address, ':'); i > 0 {
|
||||
if v, _ := strconv.Atoi(address[i+1:]); v != 0 {
|
||||
address = address[:i]
|
||||
port = v
|
||||
}
|
||||
}
|
||||
|
||||
// use default WebRTC port
|
||||
if port == 0 {
|
||||
port, _ = strconv.Atoi(Port)
|
||||
}
|
||||
|
||||
addresses = append(addresses, Address{Host: address, Port: port})
|
||||
}
|
||||
|
||||
func GetCandidates() (candidates []string) {
|
||||
for _, address := range addresses {
|
||||
// using stun server for receive public IP-address
|
||||
if address.Host == "stun" {
|
||||
ip, err := webrtc.GetCachedPublicIP()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
continue
|
||||
}
|
||||
|
||||
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
|
||||
|
||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
|
||||
// this is a copy, original host unchanged
|
||||
address.Host = ip.String()
|
||||
}
|
||||
|
||||
candidates = append(
|
||||
candidates,
|
||||
webrtc.CandidateManualHostUDP(address.Host, address.Port),
|
||||
webrtc.CandidateManualHostTCPPassive(address.Host, address.Port),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func asyncCandidates(tr *api.Transport, cons *webrtc.Conn) {
|
||||
tr.WithContext(func(ctx map[any]any) {
|
||||
if candidates, ok := ctx["candidate"].([]string); ok {
|
||||
// process candidates that receive before this moment
|
||||
for _, candidate := range candidates {
|
||||
_ = cons.AddCandidate(candidate)
|
||||
}
|
||||
|
||||
// remove already processed candidates
|
||||
delete(ctx, "candidate")
|
||||
}
|
||||
|
||||
// set variable for process candidates after this moment
|
||||
ctx["webrtc"] = cons
|
||||
})
|
||||
|
||||
for _, candidate := range GetCandidates() {
|
||||
log.Trace().Str("candidate", candidate).Msg("[webrtc] config")
|
||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: candidate})
|
||||
}
|
||||
}
|
||||
|
||||
func syncCanditates(answer string) (string, error) {
|
||||
if len(candidates) == 0 {
|
||||
if len(addresses) == 0 {
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
@@ -47,32 +90,8 @@ func syncCanditates(answer string) (string, error) {
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
_, end := md.Attribute("end-of-candidates")
|
||||
if end {
|
||||
md.Attributes = md.Attributes[:len(md.Attributes)-1]
|
||||
}
|
||||
|
||||
for _, address := range candidates {
|
||||
var err error
|
||||
address, err = webrtc.LookupIP(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
cand, err := webrtc.NewCandidate(network, address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
||||
continue
|
||||
}
|
||||
|
||||
md.WithPropertyAttribute(cand)
|
||||
}
|
||||
}
|
||||
|
||||
if end {
|
||||
md.WithPropertyAttribute("end-of-candidates")
|
||||
for _, candidate := range GetCandidates() {
|
||||
md.WithPropertyAttribute(candidate)
|
||||
}
|
||||
|
||||
data, err := sd.Marshal()
|
||||
@@ -84,13 +103,20 @@ func syncCanditates(answer string) (string, error) {
|
||||
}
|
||||
|
||||
func candidateHandler(tr *api.Transport, msg *api.Message) error {
|
||||
if tr.Consumer == nil {
|
||||
return nil
|
||||
}
|
||||
if conn := tr.Consumer.(*webrtc.Conn); conn != nil {
|
||||
s := msg.Value.(string)
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] remote")
|
||||
conn.AddCandidate(s)
|
||||
}
|
||||
// process incoming candidate in sync function
|
||||
tr.WithContext(func(ctx map[any]any) {
|
||||
candidate := msg.String()
|
||||
log.Trace().Str("candidate", candidate).Msg("[webrtc] remote")
|
||||
|
||||
if cons, ok := ctx["webrtc"].(*webrtc.Conn); ok {
|
||||
// if webrtc.Server already initialized - process candidate
|
||||
_ = cons.AddCandidate(candidate)
|
||||
} else {
|
||||
// or collect candidate and process it later
|
||||
list, _ := ctx["candidate"].([]string)
|
||||
ctx["candidate"] = append(list, candidate)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func streamsHandler(url string) (core.Producer, error) {
|
||||
url = url[7:]
|
||||
if i := strings.Index(url, "://"); i > 0 {
|
||||
switch url[:i] {
|
||||
case "ws", "wss":
|
||||
return asyncClient(url)
|
||||
case "http", "https":
|
||||
return syncClient(url)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unsupported url: " + url)
|
||||
}
|
||||
|
||||
// asyncClient can connect only to go2rtc server
|
||||
// ex: ws://localhost:1984/api/ws?src=camera1
|
||||
func asyncClient(url string) (core.Producer, error) {
|
||||
// 1. Connect to signalign server
|
||||
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = ws.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. Create PeerConnection
|
||||
pc, err := PeerConnection(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sendOffer core.Waiter
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WebSocket async"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
_ = ws.Close()
|
||||
|
||||
case *pion.ICECandidate:
|
||||
sendOffer.Wait()
|
||||
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||
_ = ws.WriteJSON(&api.Message{Type: "webrtc/candidate", Value: s})
|
||||
}
|
||||
})
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionSendonly},
|
||||
}
|
||||
|
||||
// 3. Create offer
|
||||
offer, err := prod.CreateOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Send offer
|
||||
msg := &api.Message{Type: "webrtc/offer", Value: offer}
|
||||
if err = ws.WriteJSON(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sendOffer.Done()
|
||||
|
||||
// 5. Get answer
|
||||
if err = ws.ReadJSON(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg.Type != "webrtc/answer" {
|
||||
return nil, errors.New("wrong answer: " + msg.Type)
|
||||
}
|
||||
|
||||
answer := msg.String()
|
||||
if err = prod.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. Continue to receiving candidates
|
||||
go func() {
|
||||
for {
|
||||
// receive data from remote
|
||||
msg := new(api.Message)
|
||||
if err = ws.ReadJSON(msg); err != nil {
|
||||
if cerr, ok := err.(*websocket.CloseError); ok {
|
||||
log.Trace().Err(err).Caller().Msgf("[webrtc] ws code=%d", cerr)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case "webrtc/candidate":
|
||||
if msg.Value != nil {
|
||||
_ = prod.AddCandidate(msg.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = ws.Close()
|
||||
}()
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
// syncClient - support WebRTC-HTTP Egress Protocol (WHEP)
|
||||
// ex: http://localhost:1984/api/webrtc?src=camera1
|
||||
func syncClient(url string) (core.Producer, error) {
|
||||
// 2. Create PeerConnection
|
||||
pc, err := PeerConnection(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WHEP sync"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
}
|
||||
|
||||
// 3. Create offer
|
||||
offer, err := prod.CreateCompleteOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
|
||||
req.Header.Set("Content-Type", MimeSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answer, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = prod.SetAnswer(string(answer)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Candidates []string `yaml:"candidates"`
|
||||
IceServers []pion.ICEServer `yaml:"ice_servers"`
|
||||
} `yaml:"webrtc"`
|
||||
}
|
||||
|
||||
cfg.Mod.Listen = ":8555/tcp"
|
||||
cfg.Mod.IceServers = []pion.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("webrtc")
|
||||
|
||||
address := cfg.Mod.Listen
|
||||
|
||||
// create pionAPI with custom codecs list and custom network settings
|
||||
serverAPI, err := webrtc.NewAPI(address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
// use same API for WebRTC server and client if no address
|
||||
clientAPI := serverAPI
|
||||
|
||||
if address != "" {
|
||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
||||
_, Port, _ = net.SplitHostPort(address)
|
||||
|
||||
clientAPI, _ = webrtc.NewAPI("")
|
||||
}
|
||||
|
||||
pionConf := pion.Configuration{
|
||||
ICEServers: cfg.Mod.IceServers,
|
||||
SDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,
|
||||
}
|
||||
|
||||
PeerConnection = func(active bool) (*pion.PeerConnection, error) {
|
||||
// active - client, passive - server
|
||||
if active {
|
||||
return clientAPI.NewPeerConnection(pionConf)
|
||||
} else {
|
||||
return serverAPI.NewPeerConnection(pionConf)
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range cfg.Mod.Candidates {
|
||||
AddCandidate(candidate)
|
||||
}
|
||||
|
||||
// async WebRTC server (two API versions)
|
||||
api.HandleWS("webrtc", asyncHandler)
|
||||
api.HandleWS("webrtc/offer", asyncHandler)
|
||||
api.HandleWS("webrtc/candidate", candidateHandler)
|
||||
|
||||
// sync WebRTC server (two API versions)
|
||||
api.HandleFunc("api/webrtc", syncHandler)
|
||||
|
||||
// WebRTC client
|
||||
streams.HandleFunc("webrtc", streamsHandler)
|
||||
}
|
||||
|
||||
var Port string
|
||||
var log zerolog.Logger
|
||||
|
||||
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
||||
|
||||
func asyncHandler(tr *api.Transport, msg *api.Message) error {
|
||||
var stream *streams.Stream
|
||||
var mode core.Mode
|
||||
|
||||
query := tr.Request.URL.Query()
|
||||
if name := query.Get("src"); name != "" {
|
||||
stream = streams.GetOrNew(name)
|
||||
mode = core.ModePassiveConsumer
|
||||
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
|
||||
} else if name = query.Get("dst"); name != "" {
|
||||
stream = streams.Get(name)
|
||||
mode = core.ModePassiveProducer
|
||||
log.Debug().Str("src", name).Msg("[webrtc] new producer")
|
||||
}
|
||||
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
// create new PeerConnection instance
|
||||
pc, err := PeerConnection(false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
var sendAnswer core.Waiter
|
||||
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = "WebRTC/WebSocket async"
|
||||
conn.Mode = mode
|
||||
conn.UserAgent = tr.Request.UserAgent()
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg != pion.PeerConnectionStateClosed {
|
||||
return
|
||||
}
|
||||
switch mode {
|
||||
case core.ModePassiveConsumer:
|
||||
stream.RemoveConsumer(conn)
|
||||
case core.ModePassiveProducer:
|
||||
stream.RemoveProducer(conn)
|
||||
}
|
||||
|
||||
case *pion.ICECandidate:
|
||||
sendAnswer.Wait()
|
||||
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
|
||||
}
|
||||
})
|
||||
|
||||
// V2 - json/object exchange, V1 - raw SDP exchange
|
||||
apiV2 := msg.Type == "webrtc"
|
||||
|
||||
// 1. SetOffer, so we can get remote client codecs
|
||||
var offer string
|
||||
if apiV2 {
|
||||
offer = msg.GetString("sdp")
|
||||
} else {
|
||||
offer = msg.String()
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case core.ModePassiveConsumer:
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Debug().Err(err).Msg("[webrtc] add consumer")
|
||||
_ = conn.Close()
|
||||
return err
|
||||
}
|
||||
case core.ModePassiveProducer:
|
||||
stream.AddProducer(conn)
|
||||
}
|
||||
|
||||
// 3. Exchange SDP without waiting all candidates
|
||||
answer, err := conn.GetAnswer()
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
if apiV2 {
|
||||
desc := pion.SessionDescription{Type: pion.SDPTypeAnswer, SDP: answer}
|
||||
tr.Write(&api.Message{Type: "webrtc", Value: desc})
|
||||
} else {
|
||||
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
|
||||
}
|
||||
|
||||
sendAnswer.Done()
|
||||
|
||||
asyncCandidates(tr, conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer string, err error) {
|
||||
pc, err := PeerConnection(false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
// create new webrtc instance
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = desc
|
||||
conn.Mode = core.ModePassiveConsumer
|
||||
conn.UserAgent = userAgent
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 1. SetOffer, so we can get remote client codecs
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
answer, err = conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = syncCanditates(answer)
|
||||
}
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MimeSDP = "application/sdp"
|
||||
|
||||
var sessions = map[string]*webrtc.Conn{}
|
||||
|
||||
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
query := r.URL.Query()
|
||||
if query.Get("src") != "" {
|
||||
// WHEP or JSON SDP or raw SDP exchange
|
||||
outputWebRTC(w, r)
|
||||
} else if query.Get("dst") != "" {
|
||||
// WHIP SDP exchange
|
||||
inputWebRTC(w, r)
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "PATCH":
|
||||
// TODO: WHEP/WHIP
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
|
||||
case "DELETE":
|
||||
if id := r.URL.Query().Get("id"); id != "" {
|
||||
if conn, ok := sessions[id]; ok {
|
||||
delete(sessions, id)
|
||||
_ = conn.Close()
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// outputWebRTC support API depending on Content-Type:
|
||||
// 1. application/json - receive {"type":"offer","sdp":"v=0\r\n..."} and response {"type":"answer","sdp":"v=0\r\n..."}
|
||||
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
|
||||
// 3. other - receive/response raw SDP
|
||||
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("src")
|
||||
stream := streams.Get(url)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := r.Header.Get("Content-Type")
|
||||
if mediaType != "" {
|
||||
mediaType, _, _ = strings.Cut(mediaType, ";")
|
||||
mediaType = strings.ToLower(strings.TrimSpace(mediaType))
|
||||
}
|
||||
|
||||
var offer string
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
var desc pion.SessionDescription
|
||||
if err := json.NewDecoder(r.Body).Decode(&desc); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offer = desc.SDP
|
||||
|
||||
default:
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
offer = string(body)
|
||||
}
|
||||
|
||||
var desc string
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
desc = "WebRTC/JSON sync"
|
||||
case MimeSDP:
|
||||
desc = "WebRTC/WHEP sync"
|
||||
default:
|
||||
desc = "WebRTC/HTTP sync"
|
||||
}
|
||||
|
||||
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
|
||||
v := pion.SessionDescription{
|
||||
Type: pion.SDPTypeAnswer, SDP: answer,
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(v)
|
||||
|
||||
case MimeSDP:
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
_, err = w.Write([]byte(answer))
|
||||
|
||||
default:
|
||||
_, err = w.Write([]byte(answer))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
dst := r.URL.Query().Get("dst")
|
||||
stream := streams.Get(dst)
|
||||
if stream == nil {
|
||||
stream = streams.New(dst, nil)
|
||||
}
|
||||
|
||||
// 1. Get offer
|
||||
offer, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] WHIP offer\n%s", offer)
|
||||
|
||||
pc, err := PeerConnection(false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// create new webrtc instance
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WHIP sync"
|
||||
prod.Mode = core.ModePassiveProducer
|
||||
prod.UserAgent = r.UserAgent()
|
||||
|
||||
if err = prod.SetOffer(string(offer)); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
answer, err := prod.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = syncCanditates(answer)
|
||||
}
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] WHIP answer\n%s", answer)
|
||||
|
||||
id := strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
sessions[id] = prod
|
||||
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveProducer(prod)
|
||||
if _, ok := sessions[id]; ok {
|
||||
delete(sessions, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
stream.AddProducer(prod)
|
||||
|
||||
w.Header().Set("Content-Type", MimeSDP)
|
||||
w.Header().Set("Location", "webrtc?id="+id)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
if _, err = w.Write([]byte(answer)); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Candidates []string `yaml:"candidates"`
|
||||
IceServers []pion.ICEServer `yaml:"ice_servers"`
|
||||
} `yaml:"webrtc"`
|
||||
}
|
||||
|
||||
cfg.Mod.Listen = ":8555"
|
||||
cfg.Mod.IceServers = []pion.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("webrtc")
|
||||
|
||||
address := cfg.Mod.Listen
|
||||
pionAPI, err := webrtc.NewAPI(address)
|
||||
if pionAPI == nil {
|
||||
log.Error().Err(err).Caller().Msg("webrtc.NewAPI")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] listen")
|
||||
} else if address != "" {
|
||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
||||
_, Port, _ = net.SplitHostPort(address)
|
||||
}
|
||||
|
||||
pionConf := pion.Configuration{
|
||||
ICEServers: cfg.Mod.IceServers,
|
||||
SDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,
|
||||
}
|
||||
|
||||
NewPConn = func() (*pion.PeerConnection, error) {
|
||||
return pionAPI.NewPeerConnection(pionConf)
|
||||
}
|
||||
|
||||
candidates = cfg.Mod.Candidates
|
||||
|
||||
api.HandleWS("webrtc/offer", asyncHandler)
|
||||
api.HandleWS("webrtc/candidate", candidateHandler)
|
||||
|
||||
api.HandleFunc("api/webrtc", syncHandler)
|
||||
}
|
||||
|
||||
var Port string
|
||||
var log zerolog.Logger
|
||||
|
||||
var NewPConn func() (*pion.PeerConnection, error)
|
||||
|
||||
func asyncHandler(tr *api.Transport, msg *api.Message) error {
|
||||
src := tr.Request.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
|
||||
|
||||
var err error
|
||||
|
||||
// create new webrtc instance
|
||||
conn := new(webrtc.Conn)
|
||||
conn.Conn, err = NewPConn()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
conn.UserAgent = tr.Request.UserAgent()
|
||||
conn.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
case *pion.ICECandidate:
|
||||
if msg != nil {
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 1. SetOffer, so we can get remote client codecs
|
||||
offer := msg.Value.(string)
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Debug().Err(err).Msg("[webrtc] add consumer")
|
||||
_ = conn.Conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Init()
|
||||
|
||||
// 3. Exchange SDP without waiting all candidates
|
||||
answer, err := conn.GetAnswer()
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
tr.Consumer = conn
|
||||
|
||||
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
|
||||
|
||||
asyncCandidates(tr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
|
||||
return
|
||||
}
|
||||
|
||||
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msg("ExchangeSDP")
|
||||
return
|
||||
}
|
||||
|
||||
// send SDP to client
|
||||
if _, err = w.Write([]byte(answer)); err != nil {
|
||||
log.Error().Err(err).Caller().Msg("w.Write")
|
||||
}
|
||||
}
|
||||
|
||||
func ExchangeSDP(
|
||||
stream *streams.Stream, offer string, userAgent string,
|
||||
) (answer string, err error) {
|
||||
// create new webrtc instance
|
||||
conn := new(webrtc.Conn)
|
||||
conn.Conn, err = NewPConn()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msg("NewPConn")
|
||||
return
|
||||
}
|
||||
|
||||
conn.UserAgent = userAgent
|
||||
conn.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 1. SetOffer, so we can get remote client codecs
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
log.Warn().Err(err).Caller().Msg("conn.SetOffer")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
|
||||
_ = conn.Conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
conn.Init()
|
||||
|
||||
// exchange sdp without waiting all candidates
|
||||
//answer, err := conn.ExchangeSDP(offer, false)
|
||||
answer, err = conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = syncCanditates(answer)
|
||||
}
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msg("conn.GetCompleteAnswer")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package webtorrent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Trackers []string `yaml:"trackers"`
|
||||
Shares map[string]struct {
|
||||
Pwd string `yaml:"pwd"`
|
||||
Src string `yaml:"src"`
|
||||
} `yaml:"shares"`
|
||||
} `yaml:"webtorrent"`
|
||||
}
|
||||
|
||||
cfg.Mod.Trackers = []string{"wss://tracker.openwebtorrent.com"}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
if len(cfg.Mod.Trackers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log = app.GetLogger("webtorrent")
|
||||
|
||||
streams.HandleFunc("webtorrent", streamHandle)
|
||||
|
||||
api.HandleFunc("api/webtorrent", apiHandle)
|
||||
|
||||
srv = &webtorrent.Server{
|
||||
URL: cfg.Mod.Trackers[0],
|
||||
Exchange: func(src, offer string) (answer string, err error) {
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
return "", errors.New(api.StreamNotFound)
|
||||
}
|
||||
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
|
||||
},
|
||||
}
|
||||
|
||||
if log.Debug().Enabled() {
|
||||
srv.Listen(func(msg any) {
|
||||
switch msg.(type) {
|
||||
case string, error:
|
||||
log.Debug().Msgf("[webtorrent] %s", msg)
|
||||
case *webtorrent.Message:
|
||||
log.Trace().Any("msg", msg).Msgf("[webtorrent]")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for name, share := range cfg.Mod.Shares {
|
||||
if len(name) < 8 {
|
||||
log.Warn().Str("name", name).Msgf("min share name len - 8 symbols")
|
||||
continue
|
||||
}
|
||||
if len(share.Pwd) < 4 {
|
||||
log.Warn().Str("name", name).Str("pwd", share.Pwd).Msgf("min share pwd len - 4 symbols")
|
||||
continue
|
||||
}
|
||||
if streams.Get(share.Src) == nil {
|
||||
log.Warn().Str("stream", share.Src).Msgf("stream not exists")
|
||||
continue
|
||||
}
|
||||
|
||||
srv.AddShare(name, share.Pwd, share.Src)
|
||||
|
||||
// adds to GET /api/webtorrent
|
||||
shares[name] = name
|
||||
}
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
var shares = map[string]string{}
|
||||
var srv *webtorrent.Server
|
||||
|
||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
share, ok := shares[src]
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
// support act as WebTorrent tracker (for testing purposes)
|
||||
if r.Header.Get("Connection") == "Upgrade" {
|
||||
tracker(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if src != "" {
|
||||
// response one share
|
||||
if ok {
|
||||
pwd := srv.GetSharePwd(share)
|
||||
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
|
||||
_, _ = w.Write([]byte(data))
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
// response all shares
|
||||
var items []api.Stream
|
||||
for src, share := range shares {
|
||||
pwd := srv.GetSharePwd(share)
|
||||
source := fmt.Sprintf("webtorrent:?share=%s&pwd=%s", share, pwd)
|
||||
items = append(items, api.Stream{Name: src, URL: source})
|
||||
}
|
||||
api.ResponseStreams(w, items)
|
||||
}
|
||||
|
||||
case "POST":
|
||||
// check if share already exist
|
||||
if ok {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// check if stream exists
|
||||
if stream := streams.Get(src); stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// create new random share
|
||||
share = core.RandString(10, 62)
|
||||
pwd := core.RandString(10, 62)
|
||||
srv.AddShare(share, pwd, src)
|
||||
|
||||
shares[src] = share
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
|
||||
_, _ = w.Write([]byte(data))
|
||||
|
||||
case "DELETE":
|
||||
if ok {
|
||||
srv.RemoveShare(share)
|
||||
delete(shares, src)
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func streamHandle(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
share := query.Get("share")
|
||||
pwd := query.Get("pwd")
|
||||
if len(share) < 8 || len(pwd) < 4 {
|
||||
return nil, errors.New("wrong URL: " + rawURL)
|
||||
}
|
||||
|
||||
pc, err := webrtc.PeerConnection(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return webtorrent.NewClient(srv.URL, share, pwd, pc)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package webtorrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var upgrader *websocket.Upgrader
|
||||
var hashes map[string]map[string]*websocket.Conn
|
||||
|
||||
func tracker(w http.ResponseWriter, r *http.Request) {
|
||||
if upgrader == nil {
|
||||
upgrader = &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 2028,
|
||||
}
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
defer ws.Close()
|
||||
|
||||
for {
|
||||
var msg webtorrent.Message
|
||||
if err = ws.ReadJSON(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//log.Trace().Msgf("[webtorrent] message=%v", msg)
|
||||
|
||||
if msg.InfoHash == "" || msg.PeerId == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if hashes == nil {
|
||||
hashes = map[string]map[string]*websocket.Conn{}
|
||||
}
|
||||
|
||||
// new or old client with offers
|
||||
clients := hashes[msg.InfoHash]
|
||||
if clients == nil {
|
||||
clients = map[string]*websocket.Conn{
|
||||
msg.PeerId: ws,
|
||||
}
|
||||
hashes[msg.InfoHash] = clients
|
||||
} else {
|
||||
clients[msg.PeerId] = ws
|
||||
}
|
||||
|
||||
switch {
|
||||
case msg.Offers != nil:
|
||||
// ask for ping
|
||||
raw := fmt.Sprintf(
|
||||
`{"action":"announce","interval":120,"info_hash":"%s","complete":0,"incomplete":1}`,
|
||||
msg.InfoHash,
|
||||
)
|
||||
if err = ws.WriteMessage(websocket.TextMessage, []byte(raw)); err != nil {
|
||||
log.Warn().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
// skip if no offers (server)
|
||||
if len(msg.Offers) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// get and check only first offer
|
||||
offer := msg.Offers[0]
|
||||
if offer.OfferId == "" || offer.Offer.Type != "offer" || offer.Offer.SDP == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// send offer to all clients (one of them - server)
|
||||
raw = fmt.Sprintf(
|
||||
`{"action":"announce","info_hash":"%s","peer_id":"%s","offer_id":"%s","offer":{"type":"offer","sdp":"%s"}}`,
|
||||
msg.InfoHash, msg.PeerId, offer.OfferId, offer.Offer.SDP,
|
||||
)
|
||||
|
||||
for _, server := range clients {
|
||||
if server != ws {
|
||||
_ = server.WriteMessage(websocket.TextMessage, []byte(raw))
|
||||
}
|
||||
}
|
||||
|
||||
case msg.OfferId != "" && msg.ToPeerId != "" && msg.Answer != nil:
|
||||
ws1, ok := clients[msg.ToPeerId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
raw := fmt.Sprintf(
|
||||
`{"action":"announce","info_hash":"%s","peer_id":"%s","offer_id":"%s","answer":{"type":"answer","sdp":"%s"}}`,
|
||||
msg.InfoHash, msg.PeerId, msg.OfferId, msg.Answer.SDP,
|
||||
)
|
||||
_ = ws1.WriteMessage(websocket.TextMessage, []byte(raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,52 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/brutella/hap v0.0.17
|
||||
github.com/deepch/vdk v0.0.19
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/mdns v1.0.5
|
||||
github.com/pion/ice/v2 v2.2.6
|
||||
github.com/pion/interceptor v0.1.11
|
||||
github.com/pion/rtcp v1.2.9
|
||||
github.com/pion/ice/v2 v2.3.1
|
||||
github.com/pion/interceptor v0.1.12
|
||||
github.com/pion/rtcp v1.2.10
|
||||
github.com/pion/rtp v1.7.13
|
||||
github.com/pion/sdp/v3 v3.0.5
|
||||
github.com/pion/srtp/v2 v2.0.10
|
||||
github.com/pion/stun v0.3.5
|
||||
github.com/pion/webrtc/v3 v3.1.43
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/pion/sdp/v3 v3.0.6
|
||||
github.com/pion/srtp/v2 v2.0.12
|
||||
github.com/pion/stun v0.4.0
|
||||
github.com/pion/webrtc/v3 v3.1.58
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/brutella/dnssd v1.2.3 // indirect
|
||||
github.com/brutella/dnssd v1.2.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi v1.5.4 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/pion/datachannel v1.5.2 // indirect
|
||||
github.com/pion/dtls/v2 v2.1.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/miekg/dns v1.1.52 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.6 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/mdns v0.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.2 // indirect
|
||||
github.com/pion/transport v0.13.1 // indirect
|
||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||
github.com/pion/udp v0.1.1 // indirect
|
||||
github.com/pion/sctp v1.8.6 // indirect
|
||||
github.com/pion/transport/v2 v2.0.2 // indirect
|
||||
github.com/pion/turn/v2 v2.1.0 // indirect
|
||||
github.com/pion/udp/v2 v2.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
|
||||
@@ -2,8 +2,9 @@ github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYX
|
||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
|
||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
|
||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
||||
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.5 h1:b8syhho41/5ikw3X2X4baR9NWEBSlpZnfQgujsv7bk4=
|
||||
github.com/brutella/dnssd v1.2.5/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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=
|
||||
@@ -40,13 +41,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c=
|
||||
github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -56,101 +61,106 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
||||
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
|
||||
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
|
||||
github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
|
||||
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
|
||||
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
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/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
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/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/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
|
||||
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
|
||||
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
|
||||
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
|
||||
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
|
||||
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
|
||||
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
|
||||
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
|
||||
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
|
||||
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
|
||||
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
|
||||
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
|
||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -168,25 +178,36 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -12,17 +12,20 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/hls"
|
||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||
"github.com/AlexxIT/go2rtc/cmd/http"
|
||||
"github.com/AlexxIT/go2rtc/cmd/isapi"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/tapo"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webtorrent"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -43,7 +46,9 @@ func main() {
|
||||
http.Init()
|
||||
dvrip.Init()
|
||||
tapo.Init()
|
||||
isapi.Init()
|
||||
mpegts.Init()
|
||||
roborock.Init()
|
||||
|
||||
srtp.Init()
|
||||
homekit.Init()
|
||||
@@ -53,6 +58,7 @@ func main() {
|
||||
hls.Init()
|
||||
mjpeg.Init()
|
||||
|
||||
webtorrent.Init()
|
||||
ngrok.Init()
|
||||
debug.Init()
|
||||
|
||||
|
||||
+39
-42
@@ -2,62 +2,59 @@ package aac
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"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
|
||||
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
return func(packet *rtp.Packet) {
|
||||
// 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)
|
||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||
|
||||
data := packet.Payload[2+headersSize:]
|
||||
if IsADTS(data) {
|
||||
data = data[7:]
|
||||
}
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAAC
|
||||
clone.Payload = data
|
||||
return push(&clone)
|
||||
data := packet.Payload[2+headersSize:]
|
||||
if IsADTS(data) {
|
||||
data = data[7:]
|
||||
}
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAAC
|
||||
clone.Payload = data
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||
func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
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)
|
||||
return func(packet *rtp.Packet) {
|
||||
if packet.Version != RTPPacketVersionAAC {
|
||||
handler(packet)
|
||||
return
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
Name string // H264, PCMU, PCMA, opus...
|
||||
ClockRate uint32 // 90000, 8000, 16000...
|
||||
Channels uint16 // 0, 1, 2
|
||||
FmtpLine string
|
||||
PayloadType uint8
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
|
||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
|
||||
}
|
||||
if c.Channels > 0 {
|
||||
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Codec) Text() string {
|
||||
switch c.Name {
|
||||
case CodecH264:
|
||||
if profile := DecodeH264(c.FmtpLine); profile != "" {
|
||||
return "H.264 " + profile
|
||||
}
|
||||
return c.Name
|
||||
}
|
||||
|
||||
s := c.Name
|
||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||
s += "/" + strconv.Itoa(int(c.ClockRate))
|
||||
}
|
||||
if c.Channels > 0 {
|
||||
s += "/" + strconv.Itoa(int(c.Channels))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Codec) IsRTP() bool {
|
||||
return c.PayloadType != PayloadTypeRAW
|
||||
}
|
||||
|
||||
func (c *Codec) Clone() *Codec {
|
||||
clone := *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *Codec) Match(remote *Codec) bool {
|
||||
switch remote.Name {
|
||||
case CodecAll, CodecAny:
|
||||
return true
|
||||
}
|
||||
|
||||
return c.Name == remote.Name &&
|
||||
(c.ClockRate == remote.ClockRate || remote.ClockRate == 0) &&
|
||||
(c.Channels == remote.Channels || remote.Channels == 0)
|
||||
}
|
||||
|
||||
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
c := &Codec{PayloadType: byte(atoi(payloadType))}
|
||||
|
||||
for _, attr := range md.Attributes {
|
||||
switch {
|
||||
case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
|
||||
i := strings.IndexByte(attr.Value, ' ')
|
||||
ss := strings.Split(attr.Value[i+1:], "/")
|
||||
|
||||
c.Name = strings.ToUpper(ss[0])
|
||||
// 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" {
|
||||
c.Channels = 2
|
||||
}
|
||||
case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
|
||||
if i := strings.IndexByte(attr.Value, ' '); i > 0 {
|
||||
c.FmtpLine = attr.Value[i+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Name == "" {
|
||||
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||
switch payloadType {
|
||||
case "0":
|
||||
c.Name = CodecPCMU
|
||||
c.ClockRate = 8000
|
||||
case "8":
|
||||
c.Name = CodecPCMA
|
||||
c.ClockRate = 8000
|
||||
case "14":
|
||||
c.Name = CodecMP3
|
||||
c.ClockRate = 44100
|
||||
case "26":
|
||||
c.Name = CodecJPEG
|
||||
c.ClockRate = 90000
|
||||
default:
|
||||
c.Name = payloadType
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func atoi(s string) (i int) {
|
||||
i, _ = strconv.Atoi(s)
|
||||
return
|
||||
}
|
||||
|
||||
func DecodeH264(fmtp string) string {
|
||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||
var profile string
|
||||
switch sps[1] {
|
||||
case 0x42:
|
||||
profile = "Baseline"
|
||||
case 0x4D:
|
||||
profile = "Main"
|
||||
case 0x58:
|
||||
profile = "Extended"
|
||||
case 0x64:
|
||||
profile = "High"
|
||||
default:
|
||||
profile = fmt.Sprintf("0x%02X", sps[1])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package core
|
||||
|
||||
const (
|
||||
DirectionRecvonly = "recvonly"
|
||||
DirectionSendonly = "sendonly"
|
||||
DirectionSendRecv = "sendrecv"
|
||||
)
|
||||
|
||||
const (
|
||||
KindVideo = "video"
|
||||
KindAudio = "audio"
|
||||
)
|
||||
|
||||
const (
|
||||
CodecH264 = "H264" // payloadType: 96
|
||||
CodecH265 = "H265"
|
||||
CodecVP8 = "VP8"
|
||||
CodecVP9 = "VP9"
|
||||
CodecAV1 = "AV1"
|
||||
CodecJPEG = "JPEG" // payloadType: 26
|
||||
|
||||
CodecPCMU = "PCMU" // payloadType: 0
|
||||
CodecPCMA = "PCMA" // payloadType: 8
|
||||
CodecAAC = "MPEG4-GENERIC"
|
||||
CodecOpus = "OPUS" // payloadType: 111
|
||||
CodecG722 = "G722"
|
||||
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
|
||||
CodecPCM = "L16" // Linear PCM
|
||||
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
|
||||
CodecAll = "ALL"
|
||||
CodecAny = "ANY"
|
||||
)
|
||||
|
||||
const PayloadTypeRAW byte = 255
|
||||
|
||||
type Producer interface {
|
||||
// GetMedias - return Media(s) with local Media.Direction:
|
||||
// - recvonly for Producer Video/Audio
|
||||
// - sendonly for Producer backchannel
|
||||
GetMedias() []*Media
|
||||
|
||||
// GetTrack - return Receiver, that can only produce rtp.Packet(s)
|
||||
GetTrack(media *Media, codec *Codec) (*Receiver, error)
|
||||
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type Consumer interface {
|
||||
// GetMedias - return Media(s) with local Media.Direction:
|
||||
// - sendonly for Consumer Video/Audio
|
||||
// - recvonly for Consumer backchannel
|
||||
GetMedias() []*Media
|
||||
|
||||
AddTrack(media *Media, codec *Codec, track *Receiver) error
|
||||
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type Mode byte
|
||||
|
||||
const (
|
||||
ModeActiveProducer Mode = iota + 1 // typical source (client)
|
||||
ModePassiveConsumer
|
||||
ModePassiveProducer
|
||||
ModeActiveConsumer
|
||||
)
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case ModeActiveProducer:
|
||||
return "active producer"
|
||||
case ModePassiveConsumer:
|
||||
return "passive consumer"
|
||||
case ModePassiveProducer:
|
||||
return "passive producer"
|
||||
case ModeActiveConsumer:
|
||||
return "active consumer"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
Medias []*Media `json:"medias,omitempty"`
|
||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||
Senders []*Sender `json:"senders,omitempty"`
|
||||
Recv int `json:"recv,omitempty"`
|
||||
Send int `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
UnsupportedCodec = "unsupported codec"
|
||||
WrongMediaDirection = "wrong media direction"
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"github.com/rs/zerolog/log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
||||
|
||||
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
|
||||
func RandString(size, base byte) string {
|
||||
b := make([]byte, size)
|
||||
if _, err := cryptorand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := byte(0); i < size; i++ {
|
||||
b[i] = symbols[b[i]%base]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
s = s[i+len(sub1):]
|
||||
|
||||
if len(sub2) == 1 {
|
||||
i = strings.IndexByte(s, sub2[0])
|
||||
} else {
|
||||
i = strings.Index(s, sub2)
|
||||
}
|
||||
if i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func Assert(ok bool) {
|
||||
if !ok {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
panic(file + ":" + strconv.Itoa(line))
|
||||
}
|
||||
}
|
||||
|
||||
func Caller() string {
|
||||
log.Error().Caller(0).Send()
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
return file + ":" + strconv.Itoa(line)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package core
|
||||
|
||||
type EventFunc func(msg any)
|
||||
|
||||
// Listener base struct for all classes with support feedback
|
||||
type Listener struct {
|
||||
events []EventFunc
|
||||
}
|
||||
|
||||
func (l *Listener) Listen(f EventFunc) {
|
||||
l.events = append(l.events, f)
|
||||
}
|
||||
|
||||
func (l *Listener) Fire(msg any) {
|
||||
for _, f := range l.events {
|
||||
f(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Media take best from:
|
||||
// - deepch/vdk/format/rtsp/sdp.Media
|
||||
// - pion/sdp.MediaDescription
|
||||
type Media struct {
|
||||
Kind string `json:"kind,omitempty"` // video or audio
|
||||
Direction string `json:"direction,omitempty"` // sendonly, recvonly
|
||||
Codecs []*Codec `json:"codecs,omitempty"`
|
||||
|
||||
ID string `json:"id,omitempty"` // MID for WebRTC, Control for RTSP
|
||||
}
|
||||
|
||||
func (m *Media) String() string {
|
||||
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
||||
for _, codec := range m.Codecs {
|
||||
name := codec.Text()
|
||||
|
||||
if strings.Contains(s, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
s += ", " + name
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *Media) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.String())
|
||||
}
|
||||
|
||||
func (m *Media) Clone() *Media {
|
||||
clone := *m
|
||||
clone.Codecs = make([]*Codec, len(m.Codecs))
|
||||
for i, codec := range m.Codecs {
|
||||
clone.Codecs[i] = codec.Clone()
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (m *Media) MatchMedia(remote *Media) (codec, remoteCodec *Codec) {
|
||||
// check same kind and opposite dirrection
|
||||
if m.Kind != remote.Kind ||
|
||||
m.Direction == DirectionSendonly && remote.Direction != DirectionRecvonly ||
|
||||
m.Direction == DirectionRecvonly && remote.Direction != DirectionSendonly {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, codec = range m.Codecs {
|
||||
for _, remoteCodec = range remote.Codecs {
|
||||
if codec.Match(remoteCodec) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Media) MatchCodec(remote *Codec) *Codec {
|
||||
for _, codec := range m.Codecs {
|
||||
if codec.Match(remote) {
|
||||
return codec
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Media) MatchAll() bool {
|
||||
for _, codec := range m.Codecs {
|
||||
if codec.Name == CodecAll {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
|
||||
return KindAudio
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
sd := &sdp.SessionDescription{
|
||||
Origin: sdp.Origin{
|
||||
Username: "-", SessionID: 1, SessionVersion: 1,
|
||||
NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0",
|
||||
},
|
||||
SessionName: sdp.SessionName(name),
|
||||
ConnectionInformation: &sdp.ConnectionInformation{
|
||||
NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{
|
||||
Address: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
TimeDescriptions: []sdp.TimeDescription{
|
||||
{Timing: sdp.Timing{}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, media := range medias {
|
||||
if media.Codecs == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := media.Codecs[0]
|
||||
|
||||
name := codec.Name
|
||||
if name == CodecELD {
|
||||
name = CodecAAC
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
MediaName: sdp.MediaName{
|
||||
Media: media.Kind,
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||
}
|
||||
|
||||
return sd.Marshal()
|
||||
}
|
||||
|
||||
func UnmarshalMedia(md *sdp.MediaDescription) *Media {
|
||||
m := &Media{
|
||||
Kind: md.MediaName.Media,
|
||||
}
|
||||
|
||||
for _, attr := range md.Attributes {
|
||||
switch attr.Key {
|
||||
case DirectionSendonly, DirectionRecvonly, DirectionSendRecv:
|
||||
m.Direction = attr.Key
|
||||
case "control", "mid":
|
||||
m.ID = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
for _, format := range md.MediaName.Formats {
|
||||
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func ParseQuery(query map[string][]string) (medias []*Media) {
|
||||
// set media candidates from query list
|
||||
for key, values := range query {
|
||||
switch key {
|
||||
case KindVideo, KindAudio:
|
||||
for _, value := range values {
|
||||
media := &Media{Kind: key, Direction: DirectionSendonly}
|
||||
|
||||
for _, name := range strings.Split(value, ",") {
|
||||
name = strings.ToUpper(name)
|
||||
|
||||
// check aliases
|
||||
switch name {
|
||||
case "", "COPY":
|
||||
name = CodecAny
|
||||
case "MJPEG":
|
||||
name = CodecJPEG
|
||||
case "AAC":
|
||||
name = CodecAAC
|
||||
case "MP3":
|
||||
name = CodecMP3
|
||||
}
|
||||
|
||||
media.Codecs = append(media.Codecs, &Codec{Name: name})
|
||||
}
|
||||
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package streamer
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
@@ -40,3 +42,22 @@ func TestParseQuery(t *testing.T) {
|
||||
}, medias)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
media1 := &Media{
|
||||
Kind: KindVideo,
|
||||
Direction: DirectionRecvonly,
|
||||
Codecs: []*Codec{
|
||||
{Name: CodecPCMU, ClockRate: 8000},
|
||||
},
|
||||
}
|
||||
media2 := media1.Clone()
|
||||
|
||||
p1 := fmt.Sprintf("%p", media1)
|
||||
p2 := fmt.Sprintf("%p", media2)
|
||||
require.NotEqualValues(t, p1, p2)
|
||||
|
||||
p3 := fmt.Sprintf("%p", media1.Codecs[0])
|
||||
p4 := fmt.Sprintf("%p", media2.Codecs[0])
|
||||
require.NotEqualValues(t, p3, p4)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package core
|
||||
|
||||
import "time"
|
||||
|
||||
type Probe struct {
|
||||
deadline time.Time
|
||||
items map[any]struct{}
|
||||
}
|
||||
|
||||
func NewProbe(enable bool) *Probe {
|
||||
if enable {
|
||||
return &Probe{
|
||||
deadline: time.Now().Add(time.Second * 3),
|
||||
items: map[any]struct{}{},
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Active return true if probe enabled and not finish
|
||||
func (p *Probe) Active() bool {
|
||||
return len(p.items) < 2 && time.Now().Before(p.deadline)
|
||||
}
|
||||
|
||||
// Append safe to run if Probe is nil
|
||||
func (p *Probe) Append(v any) {
|
||||
if p != nil {
|
||||
p.items[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pion/rtp"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrCantGetTrack = errors.New("can't get track")
|
||||
|
||||
type Receiver struct {
|
||||
Codec *Codec
|
||||
Media *Media
|
||||
|
||||
ID byte // Channel for RTSP, PayloadType for MPEG-TS
|
||||
|
||||
senders map[*Sender]chan *rtp.Packet
|
||||
mu sync.Mutex
|
||||
bytes int
|
||||
}
|
||||
|
||||
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
||||
Assert(codec != nil)
|
||||
return &Receiver{Codec: codec, Media: media}
|
||||
}
|
||||
|
||||
// WriteRTP - fast and non blocking write to all readers buffers
|
||||
func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||
t.mu.Lock()
|
||||
t.bytes += len(packet.Payload)
|
||||
for sender, buffer := range t.senders {
|
||||
if len(buffer) < cap(buffer) {
|
||||
buffer <- packet
|
||||
} else {
|
||||
sender.overflow++
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Receiver) Senders() (senders []*Sender) {
|
||||
t.mu.Lock()
|
||||
for sender := range t.senders {
|
||||
senders = append(senders, sender)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Receiver) Close() {
|
||||
t.mu.Lock()
|
||||
// close all sender channel buffers and erase senders list
|
||||
for _, buffer := range t.senders {
|
||||
close(buffer)
|
||||
}
|
||||
t.senders = nil
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Receiver) Replace(target *Receiver) {
|
||||
// move this receiver senders to new receiver
|
||||
t.mu.Lock()
|
||||
senders := t.senders
|
||||
t.mu.Unlock()
|
||||
|
||||
target.mu.Lock()
|
||||
target.senders = senders
|
||||
target.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Receiver) String() string {
|
||||
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
|
||||
if t.mu.TryLock() {
|
||||
s += fmt.Sprintf(", senders=%d", len(t.senders))
|
||||
t.mu.Unlock()
|
||||
} else {
|
||||
s += fmt.Sprintf(", senders=?")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Receiver) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
Codec *Codec
|
||||
Media *Media
|
||||
|
||||
Handler HandlerFunc
|
||||
|
||||
receivers []*Receiver
|
||||
mu sync.Mutex
|
||||
bytes int
|
||||
|
||||
overflow int
|
||||
}
|
||||
|
||||
func NewSender(media *Media, codec *Codec) *Sender {
|
||||
return &Sender{Codec: codec, Media: media}
|
||||
}
|
||||
|
||||
// HandlerFunc like http.HandlerFunc
|
||||
type HandlerFunc func(packet *rtp.Packet)
|
||||
|
||||
func (s *Sender) HandleRTP(track *Receiver) {
|
||||
bufferSize := 100
|
||||
|
||||
if GetKind(track.Codec.Name) == KindVideo {
|
||||
if track.Codec.IsRTP() {
|
||||
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
|
||||
// H.265 5120x1440 can have 700+ packets between two keyframes
|
||||
bufferSize = 1000
|
||||
} else {
|
||||
bufferSize = 50
|
||||
}
|
||||
}
|
||||
|
||||
buffer := make(chan *rtp.Packet, bufferSize)
|
||||
|
||||
track.mu.Lock()
|
||||
if track.senders == nil {
|
||||
track.senders = map[*Sender]chan *rtp.Packet{}
|
||||
}
|
||||
track.senders[s] = buffer
|
||||
track.mu.Unlock()
|
||||
|
||||
s.mu.Lock()
|
||||
s.receivers = append(s.receivers, track)
|
||||
s.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
// read packets from buffer channel until it will be closed
|
||||
for packet := range buffer {
|
||||
s.bytes += len(packet.Payload)
|
||||
s.Handler(packet)
|
||||
}
|
||||
|
||||
// remove current receiver from list
|
||||
// it can only happen when receiver close buffer channel
|
||||
s.mu.Lock()
|
||||
for i, receiver := range s.receivers {
|
||||
if receiver == track {
|
||||
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Sender) Close() {
|
||||
s.mu.Lock()
|
||||
// remove this sender from all receivers list
|
||||
for _, receiver := range s.receivers {
|
||||
receiver.mu.Lock()
|
||||
if buffer := receiver.senders[s]; buffer != nil {
|
||||
// remove channel from list
|
||||
delete(receiver.senders, s)
|
||||
// close channel
|
||||
close(buffer)
|
||||
}
|
||||
receiver.mu.Unlock()
|
||||
}
|
||||
s.receivers = nil
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Sender) String() string {
|
||||
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
|
||||
if s.mu.TryLock() {
|
||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
||||
s.mu.Unlock()
|
||||
} else {
|
||||
info += ", receivers=?"
|
||||
}
|
||||
if s.overflow > 0 {
|
||||
info += ", overflow=" + strconv.Itoa(s.overflow)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (s *Sender) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Waiter support:
|
||||
// - autotart on first Wait
|
||||
// - block new waiters after last Done
|
||||
// - safe Done after finish
|
||||
type Waiter struct {
|
||||
sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
state int // state < 0 means finish
|
||||
}
|
||||
|
||||
func (w *Waiter) Add(delta int) {
|
||||
w.mu.Lock()
|
||||
if w.state >= 0 {
|
||||
w.state += delta
|
||||
w.WaitGroup.Add(delta)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *Waiter) Wait() {
|
||||
w.mu.Lock()
|
||||
// first wait auto start waiter
|
||||
if w.state == 0 {
|
||||
w.state++
|
||||
w.WaitGroup.Add(1)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
|
||||
w.WaitGroup.Wait()
|
||||
}
|
||||
|
||||
func (w *Waiter) Done() {
|
||||
w.mu.Lock()
|
||||
|
||||
// safe run Done only when have tasks
|
||||
if w.state > 0 {
|
||||
w.state--
|
||||
w.WaitGroup.Done()
|
||||
}
|
||||
|
||||
// block waiter for any operations after last done
|
||||
if w.state == 0 {
|
||||
w.state = -1
|
||||
}
|
||||
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *Waiter) WaitChan() <-chan struct{} {
|
||||
var ch chan struct{}
|
||||
|
||||
w.mu.Lock()
|
||||
|
||||
if w.state >= 0 {
|
||||
ch = make(chan struct{})
|
||||
go func() {
|
||||
w.Wait()
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
w.mu.Unlock()
|
||||
|
||||
return ch
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
timer *time.Timer
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewWorker run f after d
|
||||
func NewWorker(d time.Duration, f func() time.Duration) *Worker {
|
||||
timer := time.NewTimer(d)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if d = f(); d > 0 {
|
||||
timer.Reset(d)
|
||||
continue
|
||||
}
|
||||
case <-done:
|
||||
timer.Stop()
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
|
||||
return &Worker{timer: timer, done: done}
|
||||
}
|
||||
|
||||
// Do - instant timer run
|
||||
func (w *Worker) Do() {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
w.timer.Reset(0)
|
||||
}
|
||||
|
||||
func (w *Worker) Stop() {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case w.done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
+40
-31
@@ -8,9 +8,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net"
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
uri string
|
||||
conn net.Conn
|
||||
@@ -28,17 +28,20 @@ type Client struct {
|
||||
seq uint32
|
||||
stream string
|
||||
|
||||
medias []*streamer.Media
|
||||
videoTrack *streamer.Track
|
||||
audioTrack *streamer.Track
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
videoTrack *core.Receiver
|
||||
audioTrack *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
|
||||
recv uint32
|
||||
}
|
||||
|
||||
type Response map[string]interface{}
|
||||
type Response map[string]any
|
||||
|
||||
const Login = uint16(1000)
|
||||
const OPMonitorClaim = uint16(1413)
|
||||
@@ -196,7 +199,7 @@ func (c *Client) Handle() error {
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
|
||||
_ = c.videoTrack.WriteRTP(packet)
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FD: // PFrame
|
||||
@@ -210,7 +213,7 @@ func (c *Client) Handle() error {
|
||||
|
||||
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
|
||||
|
||||
_ = c.videoTrack.WriteRTP(packet)
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FA, 0x1F9: // audio
|
||||
@@ -245,7 +248,7 @@ func (c *Client) Handle() error {
|
||||
|
||||
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
||||
|
||||
_ = c.audioTrack.WriteRTP(packet)
|
||||
c.audioTrack.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +298,8 @@ func (c *Client) Response() (b []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += 20
|
||||
|
||||
if b[0] != 255 {
|
||||
return nil, errors.New("read error")
|
||||
}
|
||||
@@ -307,6 +312,8 @@ func (c *Client) Response() (b []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += size
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -328,21 +335,21 @@ func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
}
|
||||
|
||||
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
|
||||
var codec *streamer.Codec
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 2:
|
||||
codec = &streamer.Codec{
|
||||
Name: streamer.CodecH264,
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: h264.GetFmtpLine(payload),
|
||||
}
|
||||
|
||||
case 0x03, 0x13:
|
||||
codec = &streamer.Codec{
|
||||
Name: streamer.CodecH265,
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: "profile-id=1",
|
||||
}
|
||||
|
||||
@@ -369,14 +376,15 @@ func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
c.videoTrack = streamer.NewTrack(codec, media.Direction)
|
||||
c.videoTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.videoTrack)
|
||||
}
|
||||
|
||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||
@@ -384,15 +392,15 @@ var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100,
|
||||
func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
|
||||
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
||||
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
||||
var codec *streamer.Codec
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 10: // G711U
|
||||
codec = &streamer.Codec{
|
||||
Name: streamer.CodecPCMU,
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMU,
|
||||
}
|
||||
case 14: // G711A
|
||||
codec = &streamer.Codec{
|
||||
Name: streamer.CodecPCMA,
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMA,
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported audio codec:", mediaCode)
|
||||
@@ -403,14 +411,15 @@ func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
|
||||
codec.ClockRate = sampleRates[sampleRate-1]
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
c.audioTrack = streamer.NewTrack(codec, media.Direction)
|
||||
c.audioTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.audioTrack)
|
||||
}
|
||||
|
||||
func SofiaHash(password string) string {
|
||||
|
||||
+25
-9
@@ -1,19 +1,21 @@
|
||||
package dvrip
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
if c.videoTrack != nil && c.videoTrack.Codec == codec {
|
||||
return c.videoTrack
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
if c.audioTrack != nil && c.audioTrack.Codec == codec {
|
||||
return c.audioTrack
|
||||
}
|
||||
return nil
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
@@ -21,5 +23,19 @@ func (c *Client) Start() error {
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "DVRIP active producer",
|
||||
RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
Medias: c.medias,
|
||||
Receivers: c.receivers,
|
||||
Recv: int(c.recv),
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+123
-9
@@ -3,7 +3,7 @@ package h264
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
@@ -26,6 +26,122 @@ func AnnexB2AVC(b []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
const forbiddenZeroBit = 0x80
|
||||
const nalUnitType = 0x1F
|
||||
|
||||
// DecodeStream - find and return first AU in AVC format
|
||||
// useful for processing live streams with unknown separator size
|
||||
func DecodeStream(annexb []byte) ([]byte, int) {
|
||||
startPos := -1
|
||||
|
||||
i := 0
|
||||
for {
|
||||
// search next separator
|
||||
if i = IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// move i to next AU
|
||||
if i += 3; i >= len(annexb) {
|
||||
break
|
||||
}
|
||||
|
||||
// check if AU type valid
|
||||
octet := annexb[i]
|
||||
if octet&forbiddenZeroBit != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 0 => AUD => SPS/IF/PF => AUD
|
||||
// 0 => SPS/PF => SPS/PF
|
||||
nalType := octet & nalUnitType
|
||||
if startPos >= 0 {
|
||||
switch nalType {
|
||||
case NALUTypeAUD, NALUTypeSPS, NALUTypePFrame:
|
||||
if annexb[i-4] == 0 {
|
||||
return DecodeAnnexB(annexb[startPos : i-4]), i - 4
|
||||
} else {
|
||||
return DecodeAnnexB(annexb[startPos : i-3]), i - 3
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch nalType {
|
||||
case NALUTypeSPS, NALUTypePFrame:
|
||||
if i >= 4 && annexb[i-4] == 0 {
|
||||
startPos = i - 4
|
||||
} else {
|
||||
startPos = i - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
// DecodeAnnexB - convert AnnexB to AVC format
|
||||
// support unknown separator size
|
||||
func DecodeAnnexB(b []byte) []byte {
|
||||
if b[2] == 1 {
|
||||
// convert: 0 0 1 => 0 0 0 1
|
||||
b = append([]byte{0}, b...)
|
||||
}
|
||||
|
||||
startPos := 0
|
||||
|
||||
i := 4
|
||||
for {
|
||||
// search next separato
|
||||
if i = IndexFrom(b, []byte{0, 0, 1}, i); i < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// move i to next AU
|
||||
if i += 3; i >= len(b) {
|
||||
break
|
||||
}
|
||||
|
||||
// check if AU type valid
|
||||
octet := b[i]
|
||||
if octet&forbiddenZeroBit != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch octet & nalUnitType {
|
||||
case NALUTypePFrame, NALUTypeIFrame, NALUTypeSPS, NALUTypePPS:
|
||||
if b[i-4] != 0 {
|
||||
// prefix: 0 0 1
|
||||
binary.BigEndian.PutUint32(b[startPos:], uint32(i-startPos-7))
|
||||
tmp := make([]byte, 0, len(b)+1)
|
||||
tmp = append(tmp, b[:i]...)
|
||||
tmp = append(tmp, 0)
|
||||
b = append(tmp, b[i:]...)
|
||||
startPos = i - 3
|
||||
} else {
|
||||
// prefix: 0 0 0 1
|
||||
binary.BigEndian.PutUint32(b[startPos:], uint32(i-startPos-8))
|
||||
startPos = i - 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b[startPos:], uint32(len(b)-startPos-4))
|
||||
return b
|
||||
}
|
||||
|
||||
func IndexFrom(b []byte, sep []byte, from int) int {
|
||||
if from > 0 {
|
||||
if from < len(b) {
|
||||
if i := bytes.Index(b[from:], sep); i >= 0 {
|
||||
return from + i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
return bytes.Index(b, sep)
|
||||
}
|
||||
|
||||
func EncodeAVC(nals ...[]byte) (avc []byte) {
|
||||
var i, n int
|
||||
|
||||
@@ -48,17 +164,15 @@ func EncodeAVC(nals ...[]byte) (avc []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
|
||||
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
func RepairAVC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
sps, pps := GetParameterSet(codec.FmtpLine)
|
||||
ps := EncodeAVC(sps, pps)
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) (err error) {
|
||||
if NALUType(packet.Payload) == NALUTypeIFrame {
|
||||
packet.Payload = Join(ps, packet.Payload)
|
||||
}
|
||||
return push(packet)
|
||||
return func(packet *rtp.Packet) {
|
||||
if NALUType(packet.Payload) == NALUTypeIFrame {
|
||||
packet.Payload = Join(ps, packet.Payload)
|
||||
}
|
||||
handler(packet)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -62,11 +62,11 @@ func GetProfileLevelID(fmtp string) string {
|
||||
var conf []byte
|
||||
// some cameras has wrong profile-level-id
|
||||
// https://github.com/AlexxIT/go2rtc/issues/155
|
||||
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
||||
if s := core.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
||||
if sps, _ := base64.StdEncoding.DecodeString(s); len(sps) >= 4 {
|
||||
conf = sps[1:4]
|
||||
}
|
||||
} else if s = streamer.Between(fmtp, "profile-level-id=", ";"); s != "" {
|
||||
} else if s = core.Between(fmtp, "profile-level-id=", ";"); s != "" {
|
||||
conf, _ = hex.DecodeString(s)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func GetParameterSet(fmtp string) (sps, pps []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
s := streamer.Between(fmtp, "sprop-parameter-sets=", ";")
|
||||
s := core.Between(fmtp, "sprop-parameter-sets=", ";")
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
+18
-29
@@ -4,8 +4,8 @@ import "encoding/binary"
|
||||
|
||||
// Payloader payloads H264 packets
|
||||
type Payloader struct {
|
||||
IsAVC bool
|
||||
spsNalu, ppsNalu []byte
|
||||
IsAVC bool
|
||||
stapANalu []byte
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -92,36 +92,25 @@ func (p *Payloader) Payload(mtu uint16, payload []byte) [][]byte {
|
||||
naluType := nalu[0] & naluTypeBitmask
|
||||
naluRefIdc := nalu[0] & naluRefIdcBitmask
|
||||
|
||||
switch {
|
||||
case naluType == audNALUType || naluType == fillerNALUType:
|
||||
switch naluType {
|
||||
case audNALUType, fillerNALUType:
|
||||
return
|
||||
case naluType == spsNALUType:
|
||||
p.spsNalu = nalu
|
||||
return
|
||||
case naluType == ppsNALUType:
|
||||
p.ppsNalu = nalu
|
||||
return
|
||||
case p.spsNalu != nil && p.ppsNalu != nil:
|
||||
// Pack current NALU with SPS and PPS as STAP-A
|
||||
spsLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(spsLen, uint16(len(p.spsNalu)))
|
||||
|
||||
ppsLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(ppsLen, uint16(len(p.ppsNalu)))
|
||||
|
||||
stapANalu := []byte{outputStapAHeader}
|
||||
stapANalu = append(stapANalu, spsLen...)
|
||||
stapANalu = append(stapANalu, p.spsNalu...)
|
||||
stapANalu = append(stapANalu, ppsLen...)
|
||||
stapANalu = append(stapANalu, p.ppsNalu...)
|
||||
if len(stapANalu) <= int(mtu) {
|
||||
out := make([]byte, len(stapANalu))
|
||||
copy(out, stapANalu)
|
||||
payloads = append(payloads, out)
|
||||
case spsNALUType, ppsNALUType:
|
||||
if p.stapANalu == nil {
|
||||
p.stapANalu = []byte{outputStapAHeader}
|
||||
}
|
||||
p.stapANalu = append(p.stapANalu, byte(len(nalu)>>8), byte(len(nalu)))
|
||||
p.stapANalu = append(p.stapANalu, nalu...)
|
||||
return
|
||||
}
|
||||
|
||||
p.spsNalu = nil
|
||||
p.ppsNalu = nil
|
||||
if p.stapANalu != nil {
|
||||
// Pack current NALU with SPS and PPS as STAP-A
|
||||
// Supports multiple PPS in a row
|
||||
if len(p.stapANalu) <= int(mtu) {
|
||||
payloads = append(payloads, p.stapANalu)
|
||||
}
|
||||
p.stapANalu = nil
|
||||
}
|
||||
|
||||
// Single NALU
|
||||
|
||||
+90
-97
@@ -2,7 +2,7 @@ package h264
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
)
|
||||
@@ -11,119 +11,112 @@ const RTPPacketVersionAVC = 0
|
||||
|
||||
const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
depack := &codecs.H264Packet{IsAVC: true}
|
||||
|
||||
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
sps, pps := GetParameterSet(codec.FmtpLine)
|
||||
ps := EncodeAVC(sps, pps)
|
||||
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
//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)
|
||||
return func(packet *rtp.Packet) {
|
||||
//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)
|
||||
|
||||
payload, err := depack.Unmarshal(packet.Payload)
|
||||
if len(payload) == 0 || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||
// Reolink Duo 2: sends SPS with Marker and PPS without
|
||||
if packet.Marker && len(payload) < PSMaxSize {
|
||||
switch NALUType(payload) {
|
||||
case NALUTypeSPS, NALUTypePPS:
|
||||
buf = append(buf, payload...)
|
||||
return nil
|
||||
case NALUTypeSEI:
|
||||
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
for {
|
||||
// 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:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// should not be that huge SPS
|
||||
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
|
||||
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
||||
// https://github.com/AlexxIT/WebRTC/issues/391
|
||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||
AnnexB2AVC(payload)
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = payload
|
||||
return push(&clone)
|
||||
payload, err := depack.Unmarshal(packet.Payload)
|
||||
if len(payload) == 0 || err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||
// Reolink Duo 2: sends SPS with Marker and PPS without
|
||||
if packet.Marker && len(payload) < PSMaxSize {
|
||||
switch NALUType(payload) {
|
||||
case NALUTypeSPS, NALUTypePPS:
|
||||
buf = append(buf, payload...)
|
||||
return
|
||||
case NALUTypeSEI:
|
||||
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
for {
|
||||
// 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
|
||||
}
|
||||
|
||||
payload = payload[i:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// collect all NALs for Access Unit
|
||||
if !packet.Marker {
|
||||
buf = append(buf, payload...)
|
||||
return
|
||||
}
|
||||
|
||||
if len(buf) > 0 {
|
||||
payload = append(buf, payload...)
|
||||
buf = buf[:0]
|
||||
}
|
||||
|
||||
// should not be that huge SPS
|
||||
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
|
||||
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
||||
// https://github.com/AlexxIT/WebRTC/issues/391
|
||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||
AnnexB2AVC(payload)
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = payload
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||
payloader := &Payloader{IsAVC: true}
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
mtu -= 12 // rtp.Header size
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version != RTPPacketVersionAVC {
|
||||
return push(packet)
|
||||
}
|
||||
return func(packet *rtp.Packet) {
|
||||
if packet.Version != RTPPacketVersionAVC {
|
||||
handler(packet)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
return nil
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@ package h265
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,13 +62,13 @@ func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
s := streamer.Between(fmtp, "sprop-vps=", ";")
|
||||
s := core.Between(fmtp, "sprop-vps=", ";")
|
||||
vps, _ = base64.StdEncoding.DecodeString(s)
|
||||
|
||||
s = streamer.Between(fmtp, "sprop-sps=", ";")
|
||||
s = core.Between(fmtp, "sprop-sps=", ";")
|
||||
sps, _ = base64.StdEncoding.DecodeString(s)
|
||||
|
||||
s = streamer.Between(fmtp, "sprop-pps=", ";")
|
||||
s = core.Between(fmtp, "sprop-pps=", ";")
|
||||
pps, _ = base64.StdEncoding.DecodeString(s)
|
||||
|
||||
return
|
||||
|
||||
+126
-138
@@ -2,189 +2,177 @@ package h265
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
//vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
//vps, sps, pps := GetParameterSet(codec.FmtpLine)
|
||||
//ps := h264.EncodeAVC(vps, sps, pps)
|
||||
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
var nuStart int
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
data := packet.Payload
|
||||
nuType := (data[0] >> 1) & 0x3F
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
return func(packet *rtp.Packet) {
|
||||
data := packet.Payload
|
||||
nuType := (data[0] >> 1) & 0x3F
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||
if packet.Marker && len(data) < h264.PSMaxSize {
|
||||
switch nuType {
|
||||
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
|
||||
packet.Marker = false
|
||||
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
|
||||
return nil
|
||||
}
|
||||
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||
if packet.Marker && len(data) < h264.PSMaxSize {
|
||||
switch nuType {
|
||||
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
|
||||
packet.Marker = false
|
||||
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if nuType == NALUTypeFU {
|
||||
switch data[2] >> 6 {
|
||||
case 2: // begin
|
||||
nuType = data[2] & 0x3F
|
||||
if nuType == NALUTypeFU {
|
||||
switch data[2] >> 6 {
|
||||
case 2: // begin
|
||||
nuType = data[2] & 0x3F
|
||||
|
||||
// push PS data before keyframe
|
||||
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
||||
// buf = append(buf, ps...)
|
||||
//}
|
||||
// push PS data before keyframe
|
||||
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
||||
// buf = append(buf, ps...)
|
||||
//}
|
||||
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||
buf = append(buf, data[3:]...)
|
||||
return nil
|
||||
case 0: // continue
|
||||
buf = append(buf, data[3:]...)
|
||||
return nil
|
||||
case 1: // end
|
||||
buf = append(buf, data[3:]...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||
}
|
||||
} else {
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = append(buf, data...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
||||
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||
buf = append(buf, data[3:]...)
|
||||
return
|
||||
case 0: // continue
|
||||
buf = append(buf, data[3:]...)
|
||||
return
|
||||
case 1: // end
|
||||
buf = append(buf, data[3:]...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||
}
|
||||
|
||||
// collect all NAL Units for Access Unit
|
||||
if !packet.Marker {
|
||||
return nil
|
||||
}
|
||||
|
||||
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
|
||||
|
||||
clone := *packet
|
||||
clone.Version = h264.RTPPacketVersionAVC
|
||||
clone.Payload = buf
|
||||
|
||||
buf = buf[:0]
|
||||
|
||||
return push(&clone)
|
||||
} else {
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = append(buf, data...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
||||
}
|
||||
|
||||
// collect all NAL Units for Access Unit
|
||||
if !packet.Marker {
|
||||
return
|
||||
}
|
||||
|
||||
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
|
||||
|
||||
clone := *packet
|
||||
clone.Version = h264.RTPPacketVersionAVC
|
||||
clone.Payload = buf
|
||||
|
||||
buf = buf[:0]
|
||||
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||
payloader := &Payloader{}
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
mtu -= 12 // rtp.Header size
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return push(packet)
|
||||
}
|
||||
return func(packet *rtp.Packet) {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
handler(packet)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
return nil
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SafariPay - generate Safari friendly payload for H265
|
||||
// https://github.com/AlexxIT/Blog/issues/5
|
||||
func SafariPay(mtu uint16) streamer.WrapperFunc {
|
||||
func SafariPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
size := int(mtu - 12) // rtp.Header size
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return push(packet)
|
||||
return func(packet *rtp.Packet) {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
handler(packet)
|
||||
return
|
||||
}
|
||||
|
||||
// protect original packets from modification
|
||||
au := make([]byte, len(packet.Payload))
|
||||
copy(au, packet.Payload)
|
||||
|
||||
var start byte
|
||||
|
||||
for i := 0; i < len(au); {
|
||||
size := int(binary.BigEndian.Uint32(au[i:])) + 4
|
||||
|
||||
// convert AVC to Annex-B
|
||||
au[i] = 0
|
||||
au[i+1] = 0
|
||||
au[i+2] = 0
|
||||
au[i+3] = 1
|
||||
|
||||
switch NALUType(au[i:]) {
|
||||
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||
start = 3
|
||||
default:
|
||||
if start == 0 {
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
// protect original packets from modification
|
||||
au := make([]byte, len(packet.Payload))
|
||||
copy(au, packet.Payload)
|
||||
i += size
|
||||
}
|
||||
|
||||
var start byte
|
||||
// rtp.Packet payload
|
||||
b := make([]byte, 1, size)
|
||||
size-- // minus header byte
|
||||
|
||||
for i := 0; i < len(au); {
|
||||
size := int(binary.BigEndian.Uint32(au[i:])) + 4
|
||||
for au != nil {
|
||||
b[0] = start
|
||||
|
||||
// convert AVC to Annex-B
|
||||
au[i] = 0
|
||||
au[i+1] = 0
|
||||
au[i+2] = 0
|
||||
au[i+3] = 1
|
||||
|
||||
switch NALUType(au[i:]) {
|
||||
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||
start = 3
|
||||
default:
|
||||
if start == 0 {
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
i += size
|
||||
if start > 1 {
|
||||
start -= 2
|
||||
}
|
||||
|
||||
// rtp.Packet payload
|
||||
b := make([]byte, 1, size)
|
||||
size-- // minus header byte
|
||||
|
||||
for au != nil {
|
||||
b[0] = start
|
||||
|
||||
if start > 1 {
|
||||
start -= 2
|
||||
}
|
||||
|
||||
if len(au) > size {
|
||||
b = append(b, au[:size]...)
|
||||
au = au[size:]
|
||||
} else {
|
||||
b = append(b, au...)
|
||||
au = nil
|
||||
}
|
||||
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: au == nil,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: b,
|
||||
}
|
||||
if err := push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b = b[:1] // clear buffer
|
||||
if len(au) > size {
|
||||
b = append(b, au[:size]...)
|
||||
au = au[size:]
|
||||
} else {
|
||||
b = append(b, au...)
|
||||
au = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: au == nil,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: b,
|
||||
}
|
||||
handler(&clone)
|
||||
|
||||
b = b[:1] // clear buffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -11,14 +11,14 @@ import (
|
||||
)
|
||||
|
||||
type Character struct {
|
||||
AID int `json:"aid,omitempty"`
|
||||
IID int `json:"iid"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Event interface{} `json:"ev,omitempty"`
|
||||
Perms []string `json:"perms,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AID int `json:"aid,omitempty"`
|
||||
IID int `json:"iid"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Event any `json:"ev,omitempty"`
|
||||
Perms []string `json:"perms,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
//MaxDataLen int `json:"maxDataLen"`
|
||||
|
||||
listeners map[io.Writer]bool
|
||||
@@ -91,7 +91,7 @@ func (c *Character) GenerateEvent() (data []byte, err error) {
|
||||
}
|
||||
|
||||
// Set new value and NotifyListeners
|
||||
func (c *Character) Set(v interface{}) (err error) {
|
||||
func (c *Character) Set(v any) (err error) {
|
||||
if err = c.Write(v); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (c *Character) Set(v interface{}) (err error) {
|
||||
}
|
||||
|
||||
// Write new value with right format
|
||||
func (c *Character) Write(v interface{}) (err error) {
|
||||
func (c *Character) Write(v any) (err error) {
|
||||
switch c.Format {
|
||||
case characteristic.FormatTLV8:
|
||||
var data []byte
|
||||
@@ -120,7 +120,7 @@ func (c *Character) Write(v interface{}) (err error) {
|
||||
}
|
||||
|
||||
// ReadTLV8 value to right struct
|
||||
func (c *Character) ReadTLV8(v interface{}) (err error) {
|
||||
func (c *Character) ReadTLV8(v any) (err error) {
|
||||
var data []byte
|
||||
if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil {
|
||||
return
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/brutella/hap"
|
||||
"github.com/brutella/hap/chacha20poly1305"
|
||||
"github.com/brutella/hap/curve25519"
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
// Conn for HomeKit. DevicePublic can be null.
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
DeviceAddress string // including port
|
||||
DeviceID string
|
||||
@@ -35,7 +35,7 @@ type Conn struct {
|
||||
ClientPrivate []byte
|
||||
|
||||
OnEvent func(res *http.Response)
|
||||
Output func(msg interface{})
|
||||
Output func(msg any)
|
||||
|
||||
conn net.Conn
|
||||
secure *Secure
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ type PairVerifyPayload struct {
|
||||
Signature []byte `tlv8:"10,optional"`
|
||||
}
|
||||
|
||||
//func (c *Character) Unmarshal(value interface{}) error {
|
||||
//func (c *Character) Unmarshal(value any) error {
|
||||
// switch c.Format {
|
||||
// case characteristic.FormatTLV8:
|
||||
// data, err := base64.StdEncoding.DecodeString(c.Value.(string))
|
||||
@@ -50,7 +50,7 @@ type PairVerifyPayload struct {
|
||||
// return nil
|
||||
//}
|
||||
|
||||
//func (c *Character) Marshal(value interface{}) error {
|
||||
//func (c *Character) Marshal(value any) error {
|
||||
// switch c.Format {
|
||||
// case characteristic.FormatTLV8:
|
||||
// data, err := tlv8.Marshal(value)
|
||||
|
||||
+33
-33
@@ -4,10 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/brutella/hap/characteristic"
|
||||
"github.com/brutella/hap/rtp"
|
||||
"net"
|
||||
@@ -16,15 +16,15 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
conn *hap.Conn
|
||||
exit chan error
|
||||
server *srtp.Server
|
||||
url string
|
||||
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
|
||||
sessions []*srtp.Session
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (c *Client) Dial() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
if c.medias == nil {
|
||||
c.medias = c.getMedias()
|
||||
}
|
||||
@@ -70,20 +70,20 @@ 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 {
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
|
||||
track := streamer.NewTrack(codec, media.Direction)
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
track := core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, track)
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
if c.tracks == nil {
|
||||
if c.receivers == nil {
|
||||
return errors.New("producer without tracks")
|
||||
}
|
||||
|
||||
@@ -161,11 +161,11 @@ func (c *Client) Start() error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, track := range c.tracks {
|
||||
for _, track := range c.receivers {
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
vs.Track = track
|
||||
case streamer.CodecELD:
|
||||
case core.CodecELD:
|
||||
as.Track = track
|
||||
}
|
||||
}
|
||||
@@ -188,8 +188,8 @@ func (c *Client) Stop() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) getMedias() []*streamer.Media {
|
||||
var medias []*streamer.Media
|
||||
func (c *Client) getMedias() []*core.Media {
|
||||
var medias []*core.Media
|
||||
|
||||
accs, err := c.conn.GetAccessories()
|
||||
if err != nil {
|
||||
@@ -206,20 +206,20 @@ func (c *Client) getMedias() []*streamer.Media {
|
||||
}
|
||||
|
||||
for _, hkCodec := range v1.Codecs {
|
||||
codec := &streamer.Codec{ClockRate: 90000}
|
||||
codec := &core.Codec{ClockRate: 90000}
|
||||
|
||||
switch hkCodec.Type {
|
||||
case rtp.VideoCodecType_H264:
|
||||
codec.Name = streamer.CodecH264
|
||||
codec.Name = core.CodecH264
|
||||
codec.FmtpLine = "profile-level-id=420029"
|
||||
default:
|
||||
fmt.Printf("unknown codec: %d", hkCodec.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo, Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
@@ -231,7 +231,7 @@ func (c *Client) getMedias() []*streamer.Media {
|
||||
}
|
||||
|
||||
for _, hkCodec := range v2.Codecs {
|
||||
codec := &streamer.Codec{
|
||||
codec := &core.Codec{
|
||||
Channels: uint16(hkCodec.Parameters.Channels),
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func (c *Client) getMedias() []*streamer.Media {
|
||||
|
||||
switch hkCodec.Type {
|
||||
case rtp.AudioCodecType_AAC_ELD:
|
||||
codec.Name = streamer.CodecELD
|
||||
codec.Name = core.CodecELD
|
||||
// only this value supported by FFmpeg
|
||||
codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
|
||||
default:
|
||||
@@ -256,9 +256,9 @@ func (c *Client) getMedias() []*streamer.Media {
|
||||
continue
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio, Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
@@ -272,12 +272,12 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
recv += atomic.LoadUint32(&session.Recv)
|
||||
}
|
||||
|
||||
info := &streamer.Info{
|
||||
Type: "HomeKit source",
|
||||
URL: c.conn.URL(),
|
||||
Medias: c.medias,
|
||||
Tracks: c.tracks,
|
||||
Recv: recv,
|
||||
info := &core.Info{
|
||||
Type: "HomeKit active producer",
|
||||
URL: c.conn.URL(),
|
||||
Medias: c.medias,
|
||||
Receivers: c.receivers,
|
||||
Recv: int(recv),
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+7
-7
@@ -27,7 +27,7 @@ func NewReader(b []byte) *AMF0 {
|
||||
return &AMF0{buf: b}
|
||||
}
|
||||
|
||||
func (a *AMF0) ReadMetaData() map[string]interface{} {
|
||||
func (a *AMF0) ReadMetaData() map[string]any {
|
||||
if b, _ := a.ReadByte(); b != TypeString {
|
||||
return nil
|
||||
}
|
||||
@@ -48,8 +48,8 @@ func (a *AMF0) ReadMetaData() map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AMF0) ReadMap() (map[interface{}]interface{}, error) {
|
||||
dict := make(map[interface{}]interface{})
|
||||
func (a *AMF0) ReadMap() (map[any]any, error) {
|
||||
dict := make(map[any]any)
|
||||
|
||||
for a.pos < len(a.buf) {
|
||||
k, err := a.ReadItem()
|
||||
@@ -66,7 +66,7 @@ func (a *AMF0) ReadMap() (map[interface{}]interface{}, error) {
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
func (a *AMF0) ReadItem() (interface{}, error) {
|
||||
func (a *AMF0) ReadItem() (any, error) {
|
||||
dataType, err := a.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -131,8 +131,8 @@ func (a *AMF0) ReadString() (string, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (a *AMF0) ReadObject() (map[string]interface{}, error) {
|
||||
obj := make(map[string]interface{})
|
||||
func (a *AMF0) ReadObject() (map[string]any, error) {
|
||||
obj := make(map[string]any)
|
||||
|
||||
for {
|
||||
k, err := a.ReadString()
|
||||
@@ -155,7 +155,7 @@ func (a *AMF0) ReadObject() (map[string]interface{}, error) {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (a *AMF0) ReadEcmaArray() (map[string]interface{}, error) {
|
||||
func (a *AMF0) ReadEcmaArray() (map[string]any, error) {
|
||||
if a.pos+4 >= len(a.buf) {
|
||||
return nil, Err
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ func (c *Conn) Close() (err error) {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func parseAudioConfig(meta map[string]interface{}) av.CodecData {
|
||||
func parseAudioConfig(meta map[string]any) av.CodecData {
|
||||
if meta["audiocodecid"] != float64(10) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package isapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
url string
|
||||
channel string
|
||||
conn net.Conn
|
||||
|
||||
medias []*core.Media
|
||||
sender *core.Sender
|
||||
send int
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
// check if url is valid url
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.Scheme = "http"
|
||||
u.Path = ""
|
||||
|
||||
return &Client{url: u.String()}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels"
|
||||
req, err := http.NewRequest("GET", link, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
tcp.Close(res)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xml := string(b)
|
||||
|
||||
codec := core.Between(xml, `<audioCompressionType>`, `<`)
|
||||
switch codec {
|
||||
case "G.711ulaw":
|
||||
codec = core.CodecPCMU
|
||||
case "G.711alaw":
|
||||
codec = core.CodecPCMA
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
c.channel = core.Between(xml, `<id>`, `<`)
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: codec, ClockRate: 8000},
|
||||
},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Open() (err error) {
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel
|
||||
|
||||
req, err := http.NewRequest("PUT", link+"/open", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tcp.Close(res)
|
||||
|
||||
ctx, pconn := tcp.WithConn()
|
||||
req, err = http.NewRequestWithContext(ctx, "PUT", link+"/audioData", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Length", "0")
|
||||
|
||||
res, err = tcp.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = *pconn
|
||||
|
||||
// just block until c.conn closed
|
||||
b := make([]byte, 1)
|
||||
_, _ = c.conn.Read(b)
|
||||
|
||||
tcp.Close(res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() (err error) {
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel + "/close"
|
||||
req, err := http.NewRequest("PUT", link+"/open", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tcp.Close(res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//type XMLChannels struct {
|
||||
// Channels []Channel `xml:"TwoWayAudioChannel"`
|
||||
//}
|
||||
|
||||
//type Channel struct {
|
||||
// ID string `xml:"id"`
|
||||
// Enabled string `xml:"enabled"`
|
||||
// Codec string `xml:"audioCompressionType"`
|
||||
//}
|
||||
@@ -0,0 +1,63 @@
|
||||
package isapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if c.sender == nil {
|
||||
c.sender = core.NewSender(media, track.Codec)
|
||||
c.sender.Handler = func(packet *rtp.Packet) {
|
||||
if c.conn == nil {
|
||||
return
|
||||
}
|
||||
c.send += len(packet.Payload)
|
||||
_, _ = c.conn.Write(packet.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
c.sender.HandleRTP(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() (err error) {
|
||||
if err = c.Open(); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Stop() (err error) {
|
||||
if c.sender != nil {
|
||||
c.sender.Close()
|
||||
}
|
||||
|
||||
if c.conn != nil {
|
||||
_ = c.Close()
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "ISAPI active consumer",
|
||||
Medias: c.medias,
|
||||
Send: c.send,
|
||||
}
|
||||
if c.sender != nil {
|
||||
info.Senders = []*core.Sender{c.sender}
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
+15
-13
@@ -1,13 +1,15 @@
|
||||
package iso
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
|
||||
switch codec {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
m.StartAtom("avc1")
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
m.StartAtom("hev1")
|
||||
default:
|
||||
panic("unsupported iso video: " + codec)
|
||||
@@ -30,9 +32,9 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
m.WriteUint16(0xFFFF) // color table id (-1)
|
||||
|
||||
switch codec {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
m.StartAtom("avcC")
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
m.StartAtom("hvcC")
|
||||
}
|
||||
m.Write(conf)
|
||||
@@ -43,13 +45,13 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
|
||||
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
|
||||
switch codec {
|
||||
case streamer.CodecAAC, streamer.CodecMP3:
|
||||
case core.CodecAAC, core.CodecMP3:
|
||||
m.StartAtom("mp4a")
|
||||
case streamer.CodecOpus:
|
||||
case core.CodecOpus:
|
||||
m.StartAtom("Opus")
|
||||
case streamer.CodecPCMU:
|
||||
case core.CodecPCMU:
|
||||
m.StartAtom("ulaw")
|
||||
case streamer.CodecPCMA:
|
||||
case core.CodecPCMA:
|
||||
m.StartAtom("alaw")
|
||||
default:
|
||||
panic("unsupported iso audio: " + codec)
|
||||
@@ -66,16 +68,16 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
||||
m.WriteFloat32(float64(sampleRate)) // sample_rate
|
||||
|
||||
switch codec {
|
||||
case streamer.CodecAAC:
|
||||
case core.CodecAAC:
|
||||
m.WriteEsdsAAC(conf)
|
||||
case streamer.CodecMP3:
|
||||
case core.CodecMP3:
|
||||
m.WriteEsdsMP3()
|
||||
case streamer.CodecOpus:
|
||||
case core.CodecOpus:
|
||||
// don't know what means this magic
|
||||
m.StartAtom("dOps")
|
||||
m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0)
|
||||
m.EndAtom()
|
||||
case streamer.CodecPCMU, streamer.CodecPCMA:
|
||||
case core.CodecPCMU, core.CodecPCMA:
|
||||
// don't know what means this magic
|
||||
m.StartAtom("chan")
|
||||
m.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0)
|
||||
|
||||
+22
-31
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -28,13 +27,14 @@ const (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
ID string
|
||||
|
||||
conn *websocket.Conn
|
||||
medias []*streamer.Media
|
||||
tracks map[byte]*streamer.Track
|
||||
conn *websocket.Conn
|
||||
|
||||
medias []*core.Media
|
||||
receiver *core.Receiver
|
||||
|
||||
msg *message
|
||||
t0 time.Time
|
||||
@@ -43,7 +43,7 @@ type Client struct {
|
||||
state State
|
||||
mu sync.Mutex
|
||||
|
||||
recv uint32
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(id string) *Client {
|
||||
@@ -107,12 +107,11 @@ func (c *Client) Handle() error {
|
||||
return err
|
||||
}
|
||||
|
||||
track := c.tracks[c.msg.Track]
|
||||
if track != nil {
|
||||
if c.receiver != nil && c.receiver.ID == c.msg.Track {
|
||||
c.mu.Lock()
|
||||
if c.state == StateHandle {
|
||||
c.buffer <- data
|
||||
atomic.AddUint32(&c.recv, uint32(len(data)))
|
||||
c.recv += len(data)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
@@ -139,12 +138,11 @@ func (c *Client) Handle() error {
|
||||
return err
|
||||
}
|
||||
|
||||
track = c.tracks[msg.Track]
|
||||
if track != nil {
|
||||
if c.receiver != nil && c.receiver.ID == msg.Track {
|
||||
c.mu.Lock()
|
||||
if c.state == StateHandle {
|
||||
c.buffer <- data
|
||||
atomic.AddUint32(&c.recv, uint32(len(data)))
|
||||
c.recv += len(data)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
@@ -173,8 +171,6 @@ func (c *Client) Close() error {
|
||||
}
|
||||
|
||||
func (c *Client) getTracks() error {
|
||||
c.tracks = map[byte]*streamer.Track{}
|
||||
|
||||
for {
|
||||
_, data, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
@@ -197,15 +193,15 @@ func (c *Client) getTracks() error {
|
||||
switch s {
|
||||
case "avc1": // avc1.4d0029
|
||||
// skip multiple identical init
|
||||
if c.tracks[msg.TrackID] != nil {
|
||||
if c.receiver != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecH264,
|
||||
codec := &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
|
||||
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||
@@ -225,15 +221,15 @@ func (c *Client) getTracks() error {
|
||||
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},
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
track := streamer.NewTrack(codec, streamer.DirectionSendonly)
|
||||
c.tracks[msg.TrackID] = track
|
||||
c.receiver = core.NewReceiver(media, codec)
|
||||
c.receiver.ID = msg.TrackID
|
||||
|
||||
case "mp4a": // mp4a.40.2
|
||||
}
|
||||
@@ -249,11 +245,6 @@ func (c *Client) getTracks() error {
|
||||
}
|
||||
|
||||
func (c *Client) worker(buffer chan []byte) {
|
||||
var track *streamer.Track
|
||||
for _, track = range c.tracks {
|
||||
break
|
||||
}
|
||||
|
||||
for data := range buffer {
|
||||
moof := &fmp4io.MovieFrag{}
|
||||
if _, err := moof.Unmarshal(data, 0); err != nil {
|
||||
@@ -289,7 +280,7 @@ func (c *Client) worker(buffer chan []byte) {
|
||||
Header: rtp.Header{Timestamp: ts * 90},
|
||||
Payload: data[:entry.Size],
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
c.receiver.WriteRTP(packet)
|
||||
|
||||
data = data[entry.Size:]
|
||||
ts += entry.Duration
|
||||
|
||||
+15
-19
@@ -2,22 +2,18 @@ package ivideon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"sync/atomic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
func (c *Client) GetMedias() []*core.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
|
||||
}
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
if c.receiver != nil {
|
||||
return c.receiver, nil
|
||||
}
|
||||
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
@@ -29,21 +25,21 @@ func (c *Client) Start() error {
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
if c.receiver != nil {
|
||||
c.receiver.Close()
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
var tracks []*streamer.Track
|
||||
for _, track := range c.tracks {
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
|
||||
info := &streamer.Info{
|
||||
Type: "Ivideon source",
|
||||
info := &core.Info{
|
||||
Type: "Ivideon active producer",
|
||||
URL: c.ID,
|
||||
Medias: c.medias,
|
||||
Tracks: tracks,
|
||||
Recv: atomic.LoadUint32(&c.recv),
|
||||
Recv: c.recv,
|
||||
}
|
||||
if c.receiver != nil {
|
||||
info.Receivers = []*core.Receiver{c.receiver}
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+18
-14
@@ -3,7 +3,7 @@ package mjpeg
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
@@ -11,12 +11,11 @@ import (
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
@@ -24,9 +23,10 @@ type Client struct {
|
||||
closed bool
|
||||
res *http.Response
|
||||
|
||||
medias []*streamer.Media
|
||||
track *streamer.Track
|
||||
recv uint32
|
||||
medias []*core.Media
|
||||
receiver *core.Receiver
|
||||
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(res *http.Response) *Client {
|
||||
@@ -40,9 +40,9 @@ func (c *Client) startJPEG() error {
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
c.receiver.WriteRTP(packet)
|
||||
|
||||
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
||||
c.recv += len(buf)
|
||||
|
||||
req := c.res.Request
|
||||
|
||||
@@ -61,10 +61,12 @@ func (c *Client) startJPEG() error {
|
||||
return err
|
||||
}
|
||||
|
||||
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
if c.receiver != nil {
|
||||
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
c.receiver.WriteRTP(packet)
|
||||
}
|
||||
|
||||
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
||||
c.recv += len(buf)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -109,10 +111,12 @@ func (c *Client) startMJPEG(boundary string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
if c.receiver != nil {
|
||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
c.receiver.WriteRTP(packet)
|
||||
}
|
||||
|
||||
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
||||
c.recv += len(buf)
|
||||
|
||||
if _, err = r.Discard(2); err != nil {
|
||||
return err
|
||||
|
||||
+44
-25
@@ -2,52 +2,71 @@ package mjpeg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
codecs []*streamer.Codec
|
||||
start bool
|
||||
medias []*core.Media
|
||||
sender *core.Sender
|
||||
|
||||
send uint32
|
||||
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) GetMedias() []*core.Media {
|
||||
if c.medias == nil {
|
||||
c.medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecJPEG},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
push := func(packet *rtp.Packet) error {
|
||||
c.Fire(packet.Payload)
|
||||
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
|
||||
return nil
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if c.sender == nil {
|
||||
c.sender = core.NewSender(media, track.Codec)
|
||||
c.sender.Handler = func(packet *rtp.Packet) {
|
||||
c.Fire(packet.Payload)
|
||||
c.send += len(packet.Payload)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
c.sender.Handler = RTPDepay(c.sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper := RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
c.sender.HandleRTP(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
func (c *Consumer) Stop() error {
|
||||
if c.sender != nil {
|
||||
c.sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
Type: "MJPEG client",
|
||||
info := &core.Info{
|
||||
Type: "MJPEG passive consumer",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Send: atomic.LoadUint32(&c.send),
|
||||
Medias: c.medias,
|
||||
Send: c.send,
|
||||
}
|
||||
if c.sender != nil {
|
||||
info.Senders = []*core.Sender{c.sender}
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+21
-15
@@ -3,19 +3,18 @@ package mjpeg
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
if c.medias == nil {
|
||||
c.medias = []*streamer.Media{{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
c.medias = []*core.Media{{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
|
||||
Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW,
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -23,11 +22,11 @@ func (c *Client) GetMedias() []*streamer.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
if c.track == nil {
|
||||
c.track = streamer.NewTrack(codec, streamer.DirectionSendonly)
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
if c.receiver == nil {
|
||||
c.receiver = core.NewReceiver(media, codec)
|
||||
}
|
||||
return c.track
|
||||
return c.receiver, nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
@@ -46,6 +45,9 @@ func (c *Client) Start() error {
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
if c.receiver != nil {
|
||||
c.receiver.Close()
|
||||
}
|
||||
// important for close reader/writer gorutines
|
||||
_ = c.res.Body.Close()
|
||||
c.closed = true
|
||||
@@ -53,12 +55,16 @@ func (c *Client) Stop() error {
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
Type: "MJPEG source",
|
||||
info := &core.Info{
|
||||
Type: "MJPEG active producer",
|
||||
URL: c.res.Request.URL.String(),
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Recv: atomic.LoadUint32(&c.recv),
|
||||
Medias: c.medias,
|
||||
Recv: c.recv,
|
||||
}
|
||||
if c.receiver != nil {
|
||||
info.Receivers = []*core.Receiver{c.receiver}
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+141
-149
@@ -3,86 +3,84 @@ package mjpeg
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
return func(packet *rtp.Packet) {
|
||||
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", 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
|
||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
||||
b := packet.Payload
|
||||
|
||||
// 3.1. JPEG header
|
||||
t := b[4]
|
||||
// 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 len(buf) == 0 {
|
||||
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 sizes more than 2040
|
||||
switch {
|
||||
// 512x1920 512x1440
|
||||
case w == cutSize(2560) && (h == 1920 || h == 1440):
|
||||
w = 2560
|
||||
// 1792x112
|
||||
case w == cutSize(3840) && h == cutSize(2160):
|
||||
w = 3840
|
||||
h = 2160
|
||||
// 256x1296
|
||||
case w == cutSize(2304) && h == 1296:
|
||||
w = 2304
|
||||
}
|
||||
|
||||
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
|
||||
}
|
||||
|
||||
// 3.1.9. JPEG Payload
|
||||
buf = append(buf, b...)
|
||||
|
||||
if !packet.Marker {
|
||||
return nil
|
||||
}
|
||||
|
||||
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||
buf = append(buf, 0xFF, 0xD9)
|
||||
}
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = buf
|
||||
|
||||
buf = buf[:0] // clear buffer
|
||||
|
||||
return push(&clone)
|
||||
// 3.1.7. Restart Marker header
|
||||
if 64 <= t && t <= 127 {
|
||||
b = b[12:] // skip it
|
||||
} else {
|
||||
b = b[8:]
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
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 sizes more than 2040
|
||||
switch {
|
||||
// 512x1920 512x1440
|
||||
case w == cutSize(2560) && (h == 1920 || h == 1440):
|
||||
w = 2560
|
||||
// 1792x112
|
||||
case w == cutSize(3840) && h == cutSize(2160):
|
||||
w = 3840
|
||||
h = 2160
|
||||
// 256x1296
|
||||
case w == cutSize(2304) && h == 1296:
|
||||
w = 2304
|
||||
}
|
||||
|
||||
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
|
||||
}
|
||||
|
||||
// 3.1.9. JPEG Payload
|
||||
buf = append(buf, b...)
|
||||
|
||||
if !packet.Marker {
|
||||
return
|
||||
}
|
||||
|
||||
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||
buf = append(buf, 0xFF, 0xD9)
|
||||
}
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = buf
|
||||
|
||||
buf = buf[:0] // clear buffer
|
||||
|
||||
handlerFunc(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,102 +88,96 @@ func cutSize(size uint16) uint16 {
|
||||
return ((size >> 3) & 0xFF) << 3
|
||||
}
|
||||
|
||||
func RTPPay() streamer.WrapperFunc {
|
||||
func RTPPay(handlerFunc core.HandlerFunc) core.HandlerFunc {
|
||||
const packetSize = 1436
|
||||
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
// reincode image to more common form
|
||||
p, err := Transcode(packet.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return func(packet *rtp.Packet) {
|
||||
// reincode image to more common form
|
||||
p, err := Transcode(packet.Payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h1 := make([]byte, 8)
|
||||
h1[4] = 1 // Type
|
||||
h1[5] = 255 // Q
|
||||
|
||||
// MBZ=0, Precision=0, Length=128
|
||||
h2 := make([]byte, 4, 132)
|
||||
h2[3] = 128
|
||||
|
||||
var jpgData []byte
|
||||
for jpgData == nil {
|
||||
// 2 bytes h1
|
||||
if p[0] != 0xFF {
|
||||
return
|
||||
}
|
||||
|
||||
h1 := make([]byte, 8)
|
||||
h1[4] = 1 // Type
|
||||
h1[5] = 255 // Q
|
||||
size := binary.BigEndian.Uint16(p[2:]) + 2
|
||||
|
||||
// MBZ=0, Precision=0, Length=128
|
||||
h2 := make([]byte, 4, 132)
|
||||
h2[3] = 128
|
||||
|
||||
var jpgData []byte
|
||||
for jpgData == nil {
|
||||
// 2 bytes h1
|
||||
if p[0] != 0xFF {
|
||||
return nil
|
||||
// 2 bytes payload size (include 2 bytes)
|
||||
switch p[1] {
|
||||
case 0xD8: // 0. Start Of Image (size=0)
|
||||
p = p[2:]
|
||||
continue
|
||||
case 0xDB: // 1. Define Quantization Table (size=130)
|
||||
for i := uint16(4 + 1); i < size; i += 1 + 64 {
|
||||
h2 = append(h2, p[i:i+64]...)
|
||||
}
|
||||
|
||||
size := binary.BigEndian.Uint16(p[2:]) + 2
|
||||
|
||||
// 2 bytes payload size (include 2 bytes)
|
||||
switch p[1] {
|
||||
case 0xD8: // 0. Start Of Image (size=0)
|
||||
p = p[2:]
|
||||
continue
|
||||
case 0xDB: // 1. Define Quantization Table (size=130)
|
||||
for i := uint16(4 + 1); i < size; i += 1 + 64 {
|
||||
h2 = append(h2, p[i:i+64]...)
|
||||
}
|
||||
case 0xC0: // 2. Start Of Frame (size=15)
|
||||
if p[4] != 8 {
|
||||
return nil
|
||||
}
|
||||
h := binary.BigEndian.Uint16(p[5:])
|
||||
w := binary.BigEndian.Uint16(p[7:])
|
||||
h1[6] = uint8(w >> 3)
|
||||
h1[7] = uint8(h >> 3)
|
||||
case 0xC4: // 3. Define Huffman Table (size=416)
|
||||
case 0xDA: // 4. Start Of Scan (size=10)
|
||||
jpgData = p[size:]
|
||||
case 0xC0: // 2. Start Of Frame (size=15)
|
||||
if p[4] != 8 {
|
||||
return
|
||||
}
|
||||
|
||||
p = p[size:]
|
||||
h := binary.BigEndian.Uint16(p[5:])
|
||||
w := binary.BigEndian.Uint16(p[7:])
|
||||
h1[6] = uint8(w >> 3)
|
||||
h1[7] = uint8(h >> 3)
|
||||
case 0xC4: // 3. Define Huffman Table (size=416)
|
||||
case 0xDA: // 4. Start Of Scan (size=10)
|
||||
jpgData = p[size:]
|
||||
}
|
||||
|
||||
offset := 0
|
||||
p = make([]byte, 0)
|
||||
p = p[size:]
|
||||
}
|
||||
|
||||
for jpgData != nil {
|
||||
p = p[:0]
|
||||
offset := 0
|
||||
p = make([]byte, 0)
|
||||
|
||||
if offset > 0 {
|
||||
h1[1] = byte(offset >> 16)
|
||||
h1[2] = byte(offset >> 8)
|
||||
h1[3] = byte(offset)
|
||||
p = append(p, h1...)
|
||||
} else {
|
||||
p = append(p, h1...)
|
||||
p = append(p, h2...)
|
||||
}
|
||||
for jpgData != nil {
|
||||
p = p[:0]
|
||||
|
||||
dataLen := packetSize - len(p)
|
||||
if dataLen < len(jpgData) {
|
||||
p = append(p, jpgData[:dataLen]...)
|
||||
jpgData = jpgData[dataLen:]
|
||||
offset += dataLen
|
||||
} else {
|
||||
p = append(p, jpgData...)
|
||||
jpgData = nil
|
||||
}
|
||||
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: jpgData == nil,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: p,
|
||||
}
|
||||
if err := push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
if offset > 0 {
|
||||
h1[1] = byte(offset >> 16)
|
||||
h1[2] = byte(offset >> 8)
|
||||
h1[3] = byte(offset)
|
||||
p = append(p, h1...)
|
||||
} else {
|
||||
p = append(p, h1...)
|
||||
p = append(p, h2...)
|
||||
}
|
||||
|
||||
return nil
|
||||
dataLen := packetSize - len(p)
|
||||
if dataLen < len(jpgData) {
|
||||
p = append(p, jpgData[:dataLen]...)
|
||||
jpgData = jpgData[dataLen:]
|
||||
offset += dataLen
|
||||
} else {
|
||||
p = append(p, jpgData...)
|
||||
jpgData = nil
|
||||
}
|
||||
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: jpgData == nil,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: p,
|
||||
}
|
||||
handlerFunc(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+86
-97
@@ -3,176 +3,165 @@ package mp4
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
Medias []*streamer.Media
|
||||
Medias []*core.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *Muxer
|
||||
codecs []*streamer.Codec
|
||||
wait byte
|
||||
senders []*core.Sender
|
||||
|
||||
send uint32
|
||||
muxer *Muxer
|
||||
wait byte
|
||||
|
||||
send int
|
||||
}
|
||||
|
||||
// ParseQuery - like usual parse, but with mp4 param handler
|
||||
func ParseQuery(query map[string][]string) []*streamer.Media {
|
||||
if query["mp4"] != nil {
|
||||
cons := Consumer{}
|
||||
return cons.GetMedias()
|
||||
}
|
||||
|
||||
return streamer.ParseQuery(query)
|
||||
}
|
||||
|
||||
const (
|
||||
waitNone byte = iota
|
||||
waitKeyframe
|
||||
waitInit
|
||||
)
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
func (c *Consumer) GetMedias() []*core.Media {
|
||||
if c.Medias == nil {
|
||||
// default local medias
|
||||
c.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
trackID := byte(len(c.codecs))
|
||||
c.codecs = append(c.codecs, track.Codec)
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
trackID := byte(len(c.senders))
|
||||
|
||||
codec := track.Codec
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
handler := core.NewSender(media, track.Codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
if c.wait != waitNone {
|
||||
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
var wrapper streamer.WrapperFunc
|
||||
if codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
if c.wait != waitNone {
|
||||
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
case core.CodecAAC:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := aac.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
default:
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
handler.HandleRTP(track)
|
||||
c.senders = append(c.senders, handler)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
for _, sender := range c.senders {
|
||||
sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) Codecs() []*core.Codec {
|
||||
codecs := make([]*core.Codec, len(c.senders))
|
||||
for i, sender := range c.senders {
|
||||
codecs[i] = sender.Codec
|
||||
}
|
||||
return codecs
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeCodecs() string {
|
||||
return c.muxer.MimeCodecs(c.codecs)
|
||||
return c.muxer.MimeCodecs(c.Codecs())
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeType() string {
|
||||
@@ -181,7 +170,7 @@ func (c *Consumer) MimeType() string {
|
||||
|
||||
func (c *Consumer) Init() ([]byte, error) {
|
||||
c.muxer = &Muxer{}
|
||||
return c.muxer.GetInit(c.codecs)
|
||||
return c.muxer.GetInit(c.Codecs())
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() {
|
||||
@@ -190,14 +179,14 @@ func (c *Consumer) Start() {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
Type: "MP4 client",
|
||||
info := &core.Info{
|
||||
Type: "MP4 passive consumer",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Send: atomic.LoadUint32(&c.send),
|
||||
Medias: c.Medias,
|
||||
Senders: c.senders,
|
||||
Send: c.send,
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package mp4
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/pkg/core"
|
||||
|
||||
// ParseQuery - like usual parse, but with mp4 param handler
|
||||
func ParseQuery(query map[string][]string) []*core.Media {
|
||||
if query["mp4"] != nil {
|
||||
cons := Consumer{}
|
||||
return cons.GetMedias()
|
||||
}
|
||||
|
||||
return core.ParseQuery(query)
|
||||
}
|
||||
|
||||
const (
|
||||
waitNone byte = iota
|
||||
waitKeyframe
|
||||
waitInit
|
||||
)
|
||||
+12
-12
@@ -2,10 +2,10 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/iso"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
"github.com/pion/rtp"
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
MimeOpus = "opus"
|
||||
)
|
||||
|
||||
func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
|
||||
func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
|
||||
var s string
|
||||
|
||||
for i, codec := range codecs {
|
||||
@@ -33,15 +33,15 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
// H.265 profile=main level=5.1
|
||||
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
||||
s += MimeH265
|
||||
case streamer.CodecAAC:
|
||||
case core.CodecAAC:
|
||||
s += MimeAAC
|
||||
case streamer.CodecOpus:
|
||||
case core.CodecOpus:
|
||||
s += MimeOpus
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
||||
mv := iso.NewMovie(1024)
|
||||
mv.WriteFileType()
|
||||
|
||||
@@ -58,7 +58,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
|
||||
for i, codec := range codecs {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
@@ -77,7 +77,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
codecData.AVCDecoderConfRecordBytes(),
|
||||
)
|
||||
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
@@ -97,8 +97,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
codecData.AVCDecoderConfRecordBytes(),
|
||||
)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
case core.CodecAAC:
|
||||
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -108,7 +108,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
||||
)
|
||||
|
||||
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
||||
mv.WriteAudioTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
||||
)
|
||||
|
||||
+50
-43
@@ -2,48 +2,49 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Segment struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
Medias []*streamer.Media
|
||||
Medias []*core.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
senders []*core.Sender
|
||||
|
||||
MimeType string
|
||||
OnlyKeyframe bool
|
||||
|
||||
send uint32
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Segment) GetMedias() []*streamer.Media {
|
||||
func (c *Segment) GetMedias() []*core.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
// default local medias
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
func (c *Segment) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
muxer := &Muxer{}
|
||||
|
||||
codecs := []*streamer.Codec{track.Codec}
|
||||
codecs := []*core.Codec{track.Codec}
|
||||
|
||||
init, err := muxer.GetInit(codecs)
|
||||
if err != nil {
|
||||
@@ -52,26 +53,26 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
|
||||
c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"`
|
||||
|
||||
handler := core.NewSender(media, track.Codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
var push streamer.WriterFunc
|
||||
case core.CodecH264:
|
||||
|
||||
if c.OnlyKeyframe {
|
||||
push = func(packet *rtp.Packet) error {
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if !h264.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
buf := muxer.Marshal(0, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(append(init, buf...))
|
||||
|
||||
return nil
|
||||
c.send += len(buf)
|
||||
}
|
||||
} else {
|
||||
var buf []byte
|
||||
|
||||
push = func(packet *rtp.Packet) error {
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
// fist frame - send only IFrame
|
||||
// other frames - send IFrame and all PFrames
|
||||
@@ -81,9 +82,10 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
buf = append(buf, b...)
|
||||
}
|
||||
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, init...)
|
||||
muxer.Reset()
|
||||
@@ -93,51 +95,56 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
b := muxer.Marshal(0, packet)
|
||||
buf = append(buf, b...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var wrapper streamer.WrapperFunc
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
case core.CodecH265:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
buf := muxer.Marshal(0, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(append(init, buf...))
|
||||
|
||||
return nil
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper := h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
default:
|
||||
panic(core.UnsupportedCodec)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
handler.HandleRTP(track)
|
||||
c.senders = append(c.senders, handler)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Segment) Stop() error {
|
||||
for _, sender := range c.senders {
|
||||
sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Segment) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
Type: "WS/MP4 client",
|
||||
info := &core.Info{
|
||||
Type: "MP4/WebSocket passive consumer",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Send: atomic.LoadUint32(&c.send),
|
||||
Medias: c.Medias,
|
||||
Senders: c.senders,
|
||||
Send: c.send,
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package mp4
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
Medias []*streamer.Media
|
||||
Medias []*core.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
@@ -28,35 +28,35 @@ type Consumer struct {
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
func (c *Consumer) GetMedias() []*core.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
return []*streamer.Media{
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264, ClockRate: 90000},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC, ClockRate: 16000},
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
func (c *Consumer) AddTrack(media *core.Media, track *core.Track) *core.Track {
|
||||
codec := track.Codec
|
||||
trackID := int8(len(c.streams))
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
@@ -102,8 +102,8 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
case core.CodecAAC:
|
||||
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
@@ -3,6 +3,7 @@ package mp4
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
@@ -11,14 +12,14 @@ import (
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
Medias []*streamer.Media
|
||||
Medias []*core.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *Muxer
|
||||
codecs []*streamer.Codec
|
||||
codecs []*core.Codec
|
||||
wait byte
|
||||
|
||||
send uint32
|
||||
@@ -30,38 +31,38 @@ const (
|
||||
waitInit
|
||||
)
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
func (c *Consumer) GetMedias() []*core.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC},
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
func (c *Consumer) AddTrack(media *core.Media, track *core.Track) *core.Track {
|
||||
trackID := byte(len(c.codecs))
|
||||
c.codecs = append(c.codecs, track.Codec)
|
||||
|
||||
codec := track.Codec
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
@@ -93,7 +94,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
@@ -164,7 +165,7 @@ func (c *Consumer) Start() {
|
||||
//
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
info := &core.Info{
|
||||
Type: "MP4 client",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
@@ -2,17 +2,17 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Segment struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
Medias []*streamer.Media
|
||||
Medias []*core.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
@@ -22,28 +22,28 @@ type Segment struct {
|
||||
send uint32
|
||||
}
|
||||
|
||||
func (c *Segment) GetMedias() []*streamer.Media {
|
||||
func (c *Segment) GetMedias() []*core.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
func (c *Segment) AddTrack(media *core.Media, track *core.Track) *core.Track {
|
||||
muxer := &Muxer{}
|
||||
|
||||
codecs := []*streamer.Codec{track.Codec}
|
||||
codecs := []*core.Codec{track.Codec}
|
||||
|
||||
init, err := muxer.GetInit(codecs)
|
||||
if err != nil {
|
||||
@@ -53,8 +53,8 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
c.MimeType = muxer.MimeType(codecs)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
var push streamer.WriterFunc
|
||||
case core.CodecH264:
|
||||
var push core.WriterFunc
|
||||
|
||||
if c.OnlyKeyframe {
|
||||
push = func(packet *rtp.Packet) error {
|
||||
@@ -98,7 +98,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
}
|
||||
}
|
||||
|
||||
var wrapper streamer.WrapperFunc
|
||||
var wrapper core.WrapperFunc
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
} else {
|
||||
@@ -108,7 +108,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
@@ -133,7 +133,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
||||
}
|
||||
|
||||
func (c *Segment) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
info := &core.Info{
|
||||
Type: "WS/MP4 client",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
+10
-10
@@ -3,9 +3,9 @@ package mp4
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"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/codec/h264parser"
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
@@ -21,7 +21,7 @@ type Muxer struct {
|
||||
pts []uint32
|
||||
}
|
||||
|
||||
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
func (m *Muxer) MimeType(codecs []*core.Codec) string {
|
||||
s := `video/mp4; codecs="`
|
||||
|
||||
for i, codec := range codecs {
|
||||
@@ -30,13 +30,13 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
// H.265 profile=main level=5.1
|
||||
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
||||
s += "hvc1.1.6.L153.B0"
|
||||
case streamer.CodecAAC:
|
||||
case core.CodecAAC:
|
||||
s += "mp4a.40.2"
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,12 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
return s + `"`
|
||||
}
|
||||
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
||||
moov := MOOV()
|
||||
|
||||
for i, codec := range codecs {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
@@ -92,7 +92,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
|
||||
case streamer.CodecH265:
|
||||
case core.CodecH265:
|
||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
@@ -136,8 +136,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
case core.CodecAAC:
|
||||
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+29
-23
@@ -1,17 +1,19 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
medias []*streamer.Media
|
||||
tracks map[byte]*streamer.Track
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
|
||||
res *http.Response
|
||||
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(res *http.Response) *Client {
|
||||
@@ -19,46 +21,50 @@ func NewClient(res *http.Response) *Client {
|
||||
}
|
||||
|
||||
func (c *Client) Handle() error {
|
||||
if c.tracks == nil {
|
||||
c.tracks = map[byte]*streamer.Track{}
|
||||
}
|
||||
|
||||
reader := NewReader()
|
||||
|
||||
b := make([]byte, 1024*1024*256) // 256K
|
||||
|
||||
probe := streamer.NewProbe(c.medias == nil)
|
||||
probe := core.NewProbe(c.medias == nil)
|
||||
for probe == nil || probe.Active() {
|
||||
n, err := c.res.Body.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.recv += n
|
||||
|
||||
reader.AppendBuffer(b[:n])
|
||||
|
||||
reading:
|
||||
for {
|
||||
packet := reader.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
|
||||
track := c.tracks[packet.PayloadType]
|
||||
if track == nil {
|
||||
// count track on probe state even if not support it
|
||||
probe.Append(packet.PayloadType)
|
||||
|
||||
media := GetMedia(packet)
|
||||
if media == nil {
|
||||
continue // unsupported codec
|
||||
for _, receiver := range c.receivers {
|
||||
if receiver.ID == packet.PayloadType {
|
||||
receiver.WriteRTP(packet)
|
||||
continue reading
|
||||
}
|
||||
|
||||
track = streamer.NewTrack2(media, nil)
|
||||
|
||||
c.medias = append(c.medias, media)
|
||||
c.tracks[packet.PayloadType] = track
|
||||
}
|
||||
|
||||
_ = track.WriteRTP(packet)
|
||||
// count track on probe state even if not support it
|
||||
probe.Append(packet.PayloadType)
|
||||
|
||||
media := GetMedia(packet)
|
||||
if media == nil {
|
||||
continue // unsupported codec
|
||||
}
|
||||
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
receiver := core.NewReceiver(media, media.Codecs[0])
|
||||
receiver.ID = packet.PayloadType
|
||||
c.receivers = append(c.receivers, receiver)
|
||||
|
||||
receiver.WriteRTP(packet)
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, pts: %d ts: %10d", h264.Types(packet.Payload), len(packet.Payload), pkt.PTS, packet.Timestamp)
|
||||
}
|
||||
|
||||
+23
-88
@@ -1,9 +1,8 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
@@ -14,7 +13,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
StreamTypePrivate = 0x06 // PCMU or PCMA from FFmpeg
|
||||
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
|
||||
StreamTypeAAC = 0x0F
|
||||
StreamTypeH264 = 0x1B
|
||||
StreamTypePCMATapo = 0x90
|
||||
@@ -54,13 +53,8 @@ func (p *PES) SetBuffer(size uint16, b []byte) {
|
||||
b = b[minHeaderSize+optSize:]
|
||||
|
||||
if p.StreamType == StreamTypeH264 {
|
||||
if bytes.HasPrefix(b, []byte{0, 0, 0, 1, h264.NALUTypeAUD}) {
|
||||
p.Mode = ModeStream
|
||||
b = b[5:]
|
||||
}
|
||||
}
|
||||
|
||||
if p.Mode == ModeUnknown {
|
||||
p.Mode = ModeStream
|
||||
} else {
|
||||
println("WARNING: mpegts: unknown zero-size stream")
|
||||
}
|
||||
} else {
|
||||
@@ -131,24 +125,23 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
|
||||
p.Payload = nil
|
||||
|
||||
case ModeStream:
|
||||
i := bytes.Index(p.Payload, []byte{0, 0, 0, 1, h264.NALUTypeAUD})
|
||||
if i < 0 {
|
||||
return
|
||||
}
|
||||
if i2 := IndexFrom(p.Payload, []byte{0, 0, 1}, i); i2 < 0 && i2 > 9 {
|
||||
payload, i := h264.DecodeStream(p.Payload)
|
||||
if payload == nil {
|
||||
return
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d", h264.Types(payload), len(payload))
|
||||
|
||||
p.Payload = p.Payload[i:]
|
||||
|
||||
pkt = &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
PayloadType: p.StreamType,
|
||||
Timestamp: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second),
|
||||
},
|
||||
Payload: DecodeAnnex3B(p.Payload[:i]),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
p.Payload = p.Payload[i+5:]
|
||||
|
||||
default:
|
||||
p.Payload = nil
|
||||
}
|
||||
@@ -160,92 +153,34 @@ func ParseTime(b []byte) uint32 {
|
||||
return (uint32(b[0]&0x0E) << 29) | (uint32(b[1]) << 22) | (uint32(b[2]&0xFE) << 14) | (uint32(b[3]) << 7) | (uint32(b[4]) >> 1)
|
||||
}
|
||||
|
||||
func GetMedia(pkt *rtp.Packet) *streamer.Media {
|
||||
var codec *streamer.Codec
|
||||
func GetMedia(pkt *rtp.Packet) *core.Media {
|
||||
var codec *core.Codec
|
||||
var kind string
|
||||
|
||||
switch pkt.PayloadType {
|
||||
case StreamTypeH264:
|
||||
codec = &streamer.Codec{
|
||||
Name: streamer.CodecH264,
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: h264.GetFmtpLine(pkt.Payload),
|
||||
}
|
||||
kind = streamer.KindVideo
|
||||
kind = core.KindVideo
|
||||
|
||||
case StreamTypePCMATapo:
|
||||
codec = &streamer.Codec{
|
||||
Name: streamer.CodecPCMA,
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMA,
|
||||
ClockRate: 8000,
|
||||
}
|
||||
kind = streamer.KindAudio
|
||||
kind = core.KindAudio
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return &streamer.Media{
|
||||
return &core.Media{
|
||||
Kind: kind,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeAnnex3B(annexb []byte) (avc []byte) {
|
||||
// depends on AU delimeter size
|
||||
i0 := bytes.Index(annexb, []byte{0, 0, 1})
|
||||
if i0 < 0 || i0 > 9 {
|
||||
return nil
|
||||
}
|
||||
|
||||
annexb = annexb[i0+3:] // skip first separator
|
||||
i0 = 0
|
||||
|
||||
for {
|
||||
// search next separato
|
||||
iN := IndexFrom(annexb, []byte{0, 0, 1}, i0)
|
||||
if iN < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// move i0 to next AU
|
||||
if i0 = iN + 3; i0 >= len(annexb) {
|
||||
break
|
||||
}
|
||||
|
||||
// check if AU type valid
|
||||
octet := annexb[i0]
|
||||
const forbiddenZeroBit = 0x80
|
||||
if octet&forbiddenZeroBit == 0 {
|
||||
const nalUnitType = 0x1F
|
||||
switch octet & nalUnitType {
|
||||
case h264.NALUTypePFrame, h264.NALUTypeIFrame, h264.NALUTypeSPS, h264.NALUTypePPS:
|
||||
// add AU in AVC format
|
||||
avc = append(avc, byte(iN>>24), byte(iN>>16), byte(iN>>8), byte(iN))
|
||||
avc = append(avc, annexb[:iN]...)
|
||||
|
||||
// cut search to next AU start
|
||||
annexb = annexb[i0:]
|
||||
i0 = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size := len(annexb)
|
||||
avc = append(avc, byte(size>>24), byte(size>>16), byte(size>>8), byte(size))
|
||||
return append(avc, annexb...)
|
||||
}
|
||||
|
||||
func IndexFrom(b []byte, sep []byte, from int) int {
|
||||
if from > 0 {
|
||||
if from < len(b) {
|
||||
if i := bytes.Index(b[from:], sep); i >= 0 {
|
||||
return from + i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
return bytes.Index(b, sep)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -12,3 +14,35 @@ func TestTime(t *testing.T) {
|
||||
ts := ParseTime(w.Bytes())
|
||||
assert.Equal(t, uint32(0xFFFFFFFF), ts)
|
||||
}
|
||||
|
||||
func dec(s string) []byte {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
b, _ := hex.DecodeString(s)
|
||||
return b
|
||||
}
|
||||
|
||||
//func TestStream(t *testing.T) {
|
||||
// // ffmpeg
|
||||
// annexb := dec("00000001 09f0 00000001 6764001fac2484014016ec0440000003004000000c23c60c92 00000001 68ee32c8b0 000001 6588808003 00000001 09")
|
||||
// avc, i := ParseAVC(annexb)
|
||||
// assert.Equal(t, dec("00000019 6764001fac2484014016ec0440000003004000000c23c60c92 00000005 68ee32c8b0 00000005 6588808003"), avc)
|
||||
// assert.Equal(t, dec("00000001 09"), annexb[i:])
|
||||
//
|
||||
// // http mpeg ts
|
||||
// annexb = dec("00000001 0950 000001 6764001facd2014016e8400000fa400030e081 000001 68ea8f2c 000001 65b8400eff 00000001 09")
|
||||
// avc, i = ParseAVC(annexb)
|
||||
// assert.Equal(t, dec("00000013 6764001facd2014016e8400000fa400030e081 00000004 68ea8f2c 00000005 65b8400eff"), avc)
|
||||
// assert.Equal(t, dec("00000001 09"), annexb[i:])
|
||||
//
|
||||
// // tapo TC60
|
||||
// annexb = dec("00000001 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000001 68ee04c92240 00000001 45b80000d0 00000001 67")
|
||||
// avc, i = ParseAVC(annexb)
|
||||
// assert.Equal(t, dec("0000001C 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000006 68ee04c92240 00000005 45b80000d0"), avc)
|
||||
// assert.Equal(t, dec("00000001 67"), annexb[i:])
|
||||
//
|
||||
// // Tapo ?
|
||||
// annexb = dec("00000001 674d0032e90048014742000007d2000138d108 00000001 68ea8f20 00000001 65b8400cff 00000001 67")
|
||||
// avc, i = ParseAVC(annexb)
|
||||
// assert.Equal(t, dec("00000013 674d0032e90048014742000007d2000138d108 00000004 68ea8f20 00000005 65b8400cff"), avc)
|
||||
// assert.Equal(t, dec("00000001 67"), annexb[i:])
|
||||
//}
|
||||
|
||||
+21
-6
@@ -1,20 +1,21 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
for _, track := range c.tracks {
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
@@ -22,5 +23,19 @@ func (c *Client) Start() error {
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "MPEG-TS active producer",
|
||||
URL: c.res.Request.URL.String(),
|
||||
Medias: c.medias,
|
||||
Receivers: c.receivers,
|
||||
Recv: c.recv,
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
+70
-48
@@ -3,24 +3,26 @@ package mpegts
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/ts"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
core.Listener
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
senders []*core.Sender
|
||||
|
||||
buf *bytes.Buffer
|
||||
muxer *ts.Muxer
|
||||
mimeType string
|
||||
@@ -28,35 +30,36 @@ type Consumer struct {
|
||||
start bool
|
||||
init []byte
|
||||
|
||||
send uint32
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
return []*streamer.Media{
|
||||
func (c *Consumer) GetMedias() []*core.Media {
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
},
|
||||
},
|
||||
//{
|
||||
// Kind: streamer.KindAudio,
|
||||
// Direction: streamer.DirectionRecvonly,
|
||||
// Codecs: []*streamer.Codec{
|
||||
// {Name: streamer.CodecAAC},
|
||||
// Kind: core.KindAudio,
|
||||
// Direction: core.DirectionSendonly,
|
||||
// Codecs: []*core.Codec{
|
||||
// {Name: core.CodecAAC},
|
||||
// },
|
||||
//},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
codec := track.Codec
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
trackID := int8(len(c.streams))
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
handler := core.NewSender(media, track.Codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
|
||||
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -66,21 +69,21 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
c.mimeType += ","
|
||||
}
|
||||
|
||||
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
c.mimeType += "avc1." + h264.GetProfileLevelID(track.Codec.FmtpLine)
|
||||
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
ts2time := time.Second / time.Duration(track.Codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
if !c.start {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
@@ -91,28 +94,26 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
pkt.Time = newTime
|
||||
|
||||
if err = c.muxer.WritePacket(pkt); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// clone bytes from buffer, so next packet won't overwrite it
|
||||
buf := append([]byte{}, c.buf.Bytes()...)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
|
||||
c.buf.Reset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := h264.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
|
||||
} else {
|
||||
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
case core.CodecAAC:
|
||||
s := core.Between(track.Codec.FmtpLine, "config=", ";")
|
||||
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
@@ -133,11 +134,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
|
||||
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
ts2time := time.Second / time.Duration(track.Codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if !c.start {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
@@ -147,29 +148,31 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
}
|
||||
pkt.Time = newTime
|
||||
|
||||
if err := c.muxer.WritePacket(pkt); err != nil {
|
||||
return err
|
||||
if err = c.muxer.WritePacket(pkt); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// clone bytes from buffer, so next packet won't overwrite it
|
||||
buf := append([]byte{}, c.buf.Bytes()...)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
|
||||
c.buf.Reset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := aac.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
default:
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
handler.HandleRTP(track)
|
||||
c.senders = append(c.senders, handler)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeCodecs() string {
|
||||
@@ -192,3 +195,22 @@ func (c *Consumer) Init() ([]byte, error) {
|
||||
func (c *Consumer) Start() {
|
||||
c.start = true
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
for _, sender := range c.senders {
|
||||
sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "TS passive consumer",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Medias: c.GetMedias(),
|
||||
Senders: c.senders,
|
||||
Send: c.send,
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const Timeout = time.Second * 5
|
||||
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
mid uint16
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn) *Client {
|
||||
return &Client{conn: conn, mid: 2}
|
||||
}
|
||||
|
||||
func (c *Client) Connect(clientID, username, password string) (err error) {
|
||||
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg := NewConnect(clientID, username, password)
|
||||
if _, err = c.conn.Write(msg.b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b := make([]byte, 4)
|
||||
if _, err = io.ReadFull(c.conn, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(b, []byte{CONNACK, 2, 0, 0}) {
|
||||
return errors.New("wrong login")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Subscribe(topic string) (err error) {
|
||||
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mid++
|
||||
msg := NewSubscribe(c.mid, topic, 1)
|
||||
_, err = c.conn.Write(msg.b)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Publish(topic string, payload []byte) (err error) {
|
||||
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mid++
|
||||
msg := NewPublishQOS1(c.mid, topic, payload)
|
||||
_, err = c.conn.Write(msg.b)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Read() (string, []byte, error) {
|
||||
if err := c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
b := make([]byte, 1)
|
||||
if _, err := io.ReadFull(c.conn, b); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
size, err := ReadLen(c.conn)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
b0 := b[0]
|
||||
b = make([]byte, size)
|
||||
if _, err = io.ReadFull(c.conn, b); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if b0&0xF0 != PUBLISH {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
i := binary.BigEndian.Uint16(b)
|
||||
if uint32(i) > size {
|
||||
return "", nil, errors.New("wrong topic size")
|
||||
}
|
||||
|
||||
b = b[2:]
|
||||
|
||||
if qos := (b0 >> 1) & 0b11; qos == 0 {
|
||||
return string(b[:i]), b[i:], nil
|
||||
}
|
||||
|
||||
// response with packet ID
|
||||
_, _ = c.conn.Write([]byte{PUBACK, 2, b[i], b[i+1]})
|
||||
|
||||
return string(b[2:i]), b[i+2:], nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
// TODO: Teardown
|
||||
return c.conn.Close()
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
// https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
|
||||
const (
|
||||
CONNECT = 0x10
|
||||
CONNACK = 0x20
|
||||
PUBLISH = 0x30
|
||||
PUBACK = 0x40
|
||||
SUBSCRIBE = 0x82
|
||||
SUBACK = 0x90
|
||||
QOS1 = 0x02
|
||||
)
|
||||
|
||||
func (m *Message) WriteByte(b byte) {
|
||||
m.b = append(m.b, b)
|
||||
}
|
||||
|
||||
func (m *Message) WriteBytes(b []byte) {
|
||||
m.b = append(m.b, b...)
|
||||
}
|
||||
|
||||
func (m *Message) WriteUint16(i uint16) {
|
||||
m.b = append(m.b, byte(i>>8), byte(i))
|
||||
}
|
||||
|
||||
func (m *Message) WriteLen(i int) {
|
||||
for i > 0 {
|
||||
b := byte(i % 128)
|
||||
if i /= 128; i > 0 {
|
||||
b |= 0x80
|
||||
}
|
||||
m.WriteByte(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) WriteString(s string) {
|
||||
m.WriteUint16(uint16(len(s)))
|
||||
m.b = append(m.b, s...)
|
||||
}
|
||||
|
||||
func (m *Message) Bytes() []byte {
|
||||
return m.b
|
||||
}
|
||||
|
||||
const (
|
||||
flagCleanStart = 0x02
|
||||
flagUsername = 0x80
|
||||
flagPassword = 0x40
|
||||
)
|
||||
|
||||
func NewConnect(clientID, username, password string) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(CONNECT)
|
||||
m.WriteLen(16 + len(clientID) + len(username) + len(password))
|
||||
|
||||
m.WriteString("MQTT")
|
||||
m.WriteByte(4) // MQTT version
|
||||
m.WriteByte(flagCleanStart | flagUsername | flagPassword)
|
||||
m.WriteUint16(30) // keepalive
|
||||
|
||||
m.WriteString(clientID)
|
||||
m.WriteString(username)
|
||||
m.WriteString(password)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewSubscribe(mid uint16, topic string, qos byte) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(SUBSCRIBE)
|
||||
m.WriteLen(5 + len(topic))
|
||||
|
||||
m.WriteUint16(mid)
|
||||
m.WriteString(topic)
|
||||
m.WriteByte(qos)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewPublish(topic string, payload []byte) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(PUBLISH)
|
||||
m.WriteLen(2 + len(topic) + len(payload))
|
||||
|
||||
m.WriteString(topic)
|
||||
m.WriteBytes(payload)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewPublishQOS1(mid uint16, topic string, payload []byte) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(PUBLISH | QOS1)
|
||||
m.WriteLen(4 + len(topic) + len(payload))
|
||||
|
||||
m.WriteString(topic)
|
||||
m.WriteUint16(mid)
|
||||
m.WriteBytes(payload)
|
||||
return m
|
||||
}
|
||||
|
||||
func ReadLen(r io.Reader) (uint32, error) {
|
||||
var i uint32
|
||||
var shift byte
|
||||
|
||||
b := []byte{0x80}
|
||||
for b[0]&0x80 != 0 {
|
||||
if _, err := r.Read(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i += uint32(b[0]&0x7F) << shift
|
||||
shift += 7
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user