mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-26 20:31:11 +08:00
Compare commits
62 Commits
bfd26560b1
...
v1.9.10
Author | SHA1 | Date | |
---|---|---|---|
![]() |
df95ce39d0 | ||
![]() |
26f16e392f | ||
![]() |
fcc837e570 | ||
![]() |
fd682306e7 | ||
![]() |
e072842b7a | ||
![]() |
3b976c6812 | ||
![]() |
40269328fb | ||
![]() |
45cbbaf1cf | ||
![]() |
3f542a642c | ||
![]() |
8b4df5f02c | ||
![]() |
788afb7189 | ||
![]() |
cd7fa5d09c | ||
![]() |
beb82045ff | ||
![]() |
33f0fd5fe6 | ||
![]() |
850051a299 | ||
![]() |
8f7cbdf60a | ||
![]() |
4577390130 | ||
![]() |
34b103bbcb | ||
![]() |
22fbd8bc9e | ||
![]() |
d175213369 | ||
![]() |
5a34483513 | ||
![]() |
230c80c70e | ||
![]() |
a4d7fd0d95 | ||
![]() |
ae8145f266 | ||
![]() |
7107508286 | ||
![]() |
cd2f90a7a1 | ||
![]() |
e1577b5ad3 | ||
![]() |
3c1f7e4181 | ||
![]() |
2ed67648c3 | ||
![]() |
6d37cceb91 | ||
![]() |
fce41f4fc1 | ||
![]() |
c50e894a42 | ||
![]() |
890fd78a6a | ||
![]() |
518cae1476 | ||
![]() |
545a105ba0 | ||
![]() |
70b4bf779e | ||
![]() |
7cf672da84 | ||
![]() |
80f57a0292 | ||
![]() |
3b7309d9f7 | ||
![]() |
6df1e68a5f | ||
![]() |
df2e982090 | ||
![]() |
902af5e5d7 | ||
![]() |
7fe23c7bc5 | ||
![]() |
d0c3cb066c | ||
![]() |
5666943559 | ||
![]() |
1b41f61247 | ||
![]() |
e1342f06b7 | ||
![]() |
f535595d1f | ||
![]() |
7415776e4d | ||
![]() |
bad7caa187 | ||
![]() |
2473eee66b | ||
![]() |
f45fef29d8 | ||
![]() |
699a995e8c | ||
![]() |
ce02b03a73 | ||
![]() |
3e1b01073b | ||
![]() |
3e4dce2413 | ||
![]() |
fef3091ecc | ||
![]() |
af7509ebaf | ||
![]() |
487527f5a5 | ||
![]() |
0669cfbebf | ||
![]() |
db2937d4a3 | ||
![]() |
fe10a7e55f |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with: { go-version: '1.24' }
|
||||
with: { go-version: '1.25' }
|
||||
|
||||
- name: Build go2rtc_win64
|
||||
env: { GOOS: windows, GOARCH: amd64 }
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win32
|
||||
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
|
||||
env: { GOOS: windows, GOARCH: 386 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win32
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_amd64
|
||||
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
|
||||
env: { GOOS: darwin, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_amd64
|
||||
uses: actions/upload-artifact@v4
|
||||
|
321
README.md
321
README.md
@@ -8,7 +8,7 @@
|
||||
[](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
|
||||
</h1>
|
||||
|
||||
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
|
||||
Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
|
||||
|
||||

|
||||
|
||||
@@ -20,11 +20,11 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
|
||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||
- 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)
|
||||
- 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
|
||||
- auto-match client-supported codecs
|
||||
- [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)
|
||||
@@ -116,7 +116,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
||||
|
||||
- `go2rtc_win64.zip` - Windows 10+ 64-bit
|
||||
- `go2rtc_win32.zip` - Windows 7+ 32-bit
|
||||
- `go2rtc_win32.zip` - Windows 10+ 32-bit
|
||||
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
||||
- `go2rtc_linux_amd64` - Linux 64-bit
|
||||
- `go2rtc_linux_i386` - Linux 32-bit
|
||||
@@ -124,7 +124,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
||||
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
||||
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
|
||||
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
|
||||
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
|
||||
- `go2rtc_mac_amd64.zip` - macOS 11+ Intel 64-bit
|
||||
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
|
||||
- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit
|
||||
- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit
|
||||
@@ -182,11 +182,11 @@ Available modules:
|
||||
|
||||
### Module: Streams
|
||||
|
||||
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
|
||||
**go2rtc** supports different stream source types. You can config one or multiple links of any type as a stream source.
|
||||
|
||||
Available source types:
|
||||
|
||||
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support
|
||||
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two-way audio](#two-way-audio) support
|
||||
- [rtmp](#source-rtmp) - `RTMP` streams
|
||||
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
|
||||
- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol
|
||||
@@ -203,16 +203,16 @@ Available source types:
|
||||
- [gopro](#source-gopro) - GoPro cameras
|
||||
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
|
||||
- [isapi](#source-isapi) - two-way audio for Hikvision (ISAPI) cameras
|
||||
- [roborock](#source-roborock) - Roborock vacuums with cameras
|
||||
- [webrtc](#source-webrtc) - WebRTC/WHEP sources
|
||||
- [webtorrent](#source-webtorrent) - WebTorrent source from another go2rtc
|
||||
|
||||
Read more about [incoming sources](#incoming-sources)
|
||||
|
||||
#### Two way audio
|
||||
#### Two-way audio
|
||||
|
||||
Supported for sources:
|
||||
Supported sources:
|
||||
|
||||
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||
- [DVRIP](#source-dvrip) cameras
|
||||
@@ -222,9 +222,9 @@ Supported for sources:
|
||||
- [Exec](#source-exec) audio on server
|
||||
- [Any Browser](#incoming-browser) as IP-camera
|
||||
|
||||
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)).
|
||||
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.
|
||||
go2rtc also supports [play audio](#stream-to-camera) files and live streams on this cameras.
|
||||
|
||||
#### Source: RTSP
|
||||
|
||||
@@ -242,13 +242,13 @@ streams:
|
||||
|
||||
**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
|
||||
- **Amcrest Doorbell** users may want to disable two-way audio, because with an active stream, you won't have a working call button. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
||||
- **Dahua Doorbell** users may want to change [audio codec](https://github.com/AlexxIT/go2rtc/issues/49#issuecomment-2127107379) for proper 2-way audio. Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio
|
||||
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
|
||||
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful, unusable stream implementation
|
||||
- **Ubiquiti UniFi** 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 your camera has two RTSP links, you can add both as sources. This is useful when streams have different codecs, for 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 don't use transcoding
|
||||
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
|
||||
|
||||
**Other options**
|
||||
@@ -257,7 +257,7 @@ Format: `rtsp...#{param1}#{param2}#{param3}`
|
||||
|
||||
- Add custom timeout `#timeout=30` (in seconds)
|
||||
- Ignore audio - `#media=video` or ignore video - `#media=audio`
|
||||
- Ignore two way audio API `#backchannel=0` - important for some glitchy cameras
|
||||
- Ignore two-way audio API `#backchannel=0` - important for some glitchy cameras
|
||||
- Use WebSocket transport `#transport=ws...`
|
||||
|
||||
**RTSP over WebSocket**
|
||||
@@ -272,7 +272,7 @@ streams:
|
||||
|
||||
#### Source: RTMP
|
||||
|
||||
You can get stream from RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
|
||||
You can get a stream from an RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -288,7 +288,7 @@ Support Content-Type:
|
||||
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
|
||||
- **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream)
|
||||
|
||||
Source also support HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
|
||||
Source also supports HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -308,7 +308,7 @@ streams:
|
||||
custom_header: "https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"
|
||||
```
|
||||
|
||||
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
|
||||
**PS.** Dahua camera has a bug: if you select MJPEG codec for RTSP second stream, snapshot won't work.
|
||||
|
||||
#### Source: ONVIF
|
||||
|
||||
@@ -316,7 +316,7 @@ streams:
|
||||
|
||||
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
|
||||
|
||||
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
|
||||
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use Docker, you must use "network host".
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -327,7 +327,7 @@ streams:
|
||||
|
||||
#### Source: FFmpeg
|
||||
|
||||
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
|
||||
You can get any stream, file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
|
||||
|
||||
- FFmpeg preistalled for **Docker** and **Hass Add-on** users
|
||||
- **Hass Add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
|
||||
@@ -342,7 +342,7 @@ streams:
|
||||
# [FILE] video will be transcoded to H264, audio will be skipped
|
||||
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
|
||||
|
||||
# [FILE] video will be copied, audio will be transcoded to pcmu
|
||||
# [FILE] video will be copied, audio will be transcoded to PCMU
|
||||
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
|
||||
|
||||
# [HLS] video will be copied, audio will be skipped
|
||||
@@ -355,9 +355,9 @@ streams:
|
||||
rotate: ffmpeg: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/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
All transcoding formats have [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
|
||||
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
|
||||
But you can override them via YAML config. You can also add your own formats to the config and use them with source params.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
@@ -385,12 +385,12 @@ Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/H
|
||||
|
||||
#### Source: FFmpeg Device
|
||||
|
||||
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
|
||||
You can get video from any USB camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
|
||||
|
||||
- check available devices in Web interface
|
||||
- check available devices in web interface
|
||||
- `video_size` and `framerate` must be supported by your camera!
|
||||
- for Linux supported only video for now
|
||||
- for macOS you can stream Facetime camera or whole Desktop!
|
||||
- for macOS you can stream FaceTime camera or whole desktop!
|
||||
- for macOS important to set right framerate
|
||||
|
||||
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
|
||||
@@ -408,7 +408,7 @@ streams:
|
||||
|
||||
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
|
||||
|
||||
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
|
||||
If you want to use **RTSP** transport, the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
|
||||
|
||||
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**.
|
||||
|
||||
@@ -418,11 +418,11 @@ The source can be used with:
|
||||
- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
|
||||
- any your own software
|
||||
- any of your own software
|
||||
|
||||
Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):
|
||||
|
||||
- `killsignal` - signal which will be send to stop the process (numeric form)
|
||||
- `killsignal` - signal which will be sent to stop the process (numeric form)
|
||||
- `killtimeout` - time in seconds for forced termination with sigkill
|
||||
- `backchannel` - enable backchannel for two-way audio
|
||||
|
||||
@@ -439,7 +439,7 @@ streams:
|
||||
|
||||
#### Source: Echo
|
||||
|
||||
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
|
||||
Some sources may have a dynamic link. And you will need to get it using a Bash or Python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
|
||||
|
||||
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
|
||||
|
||||
@@ -461,20 +461,20 @@ Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/ex
|
||||
**Important:**
|
||||
|
||||
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
|
||||
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home) - you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc - you can't pair it with iPhone
|
||||
- HomeKit device should be in same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between device and go2rtc
|
||||
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home), you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc, you can't pair it with an iPhone
|
||||
- HomeKit device should be on the same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between the device and go2rtc
|
||||
|
||||
go2rtc support import paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you using Hass, I recommend pairing devices with it, it will give you more options.
|
||||
go2rtc supports importing paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you are using Hass, I recommend pairing devices with it; it will give you more options.
|
||||
|
||||
You can pair device with go2rtc on the HomeKit page. If you can't see your devices - reload the page. Also try reboot your HomeKit device (power off). If you still can't see it - you have a problems with mDNS.
|
||||
You can pair device with go2rtc on the HomeKit page. If you can't see your devices, reload the page. Also, try rebooting your HomeKit device (power off). If you still can't see it, you have a problem with mDNS.
|
||||
|
||||
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
|
||||
If you see a device but it does not have a pairing button, it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete the device from that ecosystem, and it will be available for pairing. If you cannot unpair the device, you will have to reset it.
|
||||
|
||||
**Important:**
|
||||
|
||||
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
|
||||
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violations
|
||||
- Audio can't be played in `VLC` and probably any other player
|
||||
- Audio should be transcoded for using with MSE, WebRTC, etc.
|
||||
- Audio should be transcoded for use with MSE, WebRTC, etc.
|
||||
|
||||
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
|
||||
|
||||
@@ -496,7 +496,7 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
|
||||
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
|
||||
|
||||
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
|
||||
- setup separate streams for different channels and streams
|
||||
- set up separate streams for different channels and streams
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -510,7 +510,7 @@ streams:
|
||||
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
|
||||
- set up separate streams for different channels
|
||||
- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream
|
||||
- only the TCP protocol is supported
|
||||
|
||||
@@ -531,8 +531,8 @@ streams:
|
||||
|
||||
- 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
|
||||
- some new camera firmwares requires SHA256 instead of MD5
|
||||
- you can also use **UPPERCASE** MD5 hash from your cloud password with `admin` username
|
||||
- some new camera firmwares require SHA256 instead of MD5
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -542,6 +542,10 @@ streams:
|
||||
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
|
||||
# admin username and UPPERCASE SHA256 cloud-password hash
|
||||
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
|
||||
# VGA stream (the so called substream, the lower resolution one)
|
||||
camera4: tapo://cloud-password@192.168.1.123?subtype=1
|
||||
# HD stream (default)
|
||||
camera5: tapo://cloud-password@192.168.1.123?subtype=0
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -573,7 +577,7 @@ Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or
|
||||
|
||||
#### Source: Ivideon
|
||||
|
||||
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||
Support public cameras from the service [Ivideon](https://tv.ivideon.com/).
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -591,7 +595,7 @@ Support import camera links from [Home Assistant](https://www.home-assistant.io/
|
||||
|
||||
```yaml
|
||||
hass:
|
||||
config: "/config" # skip this setting if you Hass Add-on user
|
||||
config: "/config" # skip this setting if you Hass add-on user
|
||||
|
||||
streams:
|
||||
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
|
||||
@@ -600,9 +604,9 @@ streams:
|
||||
|
||||
**WebRTC Cameras** (*from [v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*)
|
||||
|
||||
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
|
||||
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this format.
|
||||
|
||||
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available ram on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream.
|
||||
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available RAM on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -614,13 +618,13 @@ streams:
|
||||
|
||||
**RTSP Cameras**
|
||||
|
||||
By default, the Home Assistant API does not allow you to get dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
|
||||
By default, the Home Assistant API does not allow you to get a dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others, can also be imported using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
|
||||
|
||||
#### Source: ISAPI
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
|
||||
This source type supports only backchannel audio for the Hikvision ISAPI protocol. So it should be used as a second source in addition to the RTSP protocol.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -633,9 +637,9 @@ streams:
|
||||
|
||||
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
|
||||
|
||||
Currently only WebRTC cameras are supported.
|
||||
Currently, only WebRTC cameras are supported.
|
||||
|
||||
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
|
||||
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters, Nest/WebRTC source will work without Hass.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -646,29 +650,29 @@ streams:
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source type support Roborock vacuums with cameras. Known working models:
|
||||
This source type supports Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||
- Roborock S7 MaxV - video and two way audio
|
||||
- Roborock Qrevo MaxV - video and two way audio
|
||||
- Roborock S7 MaxV - video and two-way audio
|
||||
- Roborock Qrevo MaxV - video and two-way audio
|
||||
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
Source supports loading Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
|
||||
If you have a graphic PIN for your vacuum, add it as a numeric PIN (lines: 123, 456, 789) to the end of the `roborock` link.
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source type support four connection formats.
|
||||
This source type supports four connection formats.
|
||||
|
||||
**whep**
|
||||
|
||||
[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) - is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
|
||||
**go2rtc**
|
||||
|
||||
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
|
||||
This format is only supported in go2rtc. Unlike WHEP, it supports asynchronous WebRTC connections and two-way audio.
|
||||
|
||||
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
|
||||
|
||||
@@ -676,11 +680,11 @@ Support connection to [OpenIPC](https://openipc.org/) cameras.
|
||||
|
||||
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
|
||||
|
||||
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
|
||||
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use the [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
|
||||
|
||||
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
|
||||
|
||||
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
||||
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify the signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
||||
|
||||
**switchbot**
|
||||
|
||||
@@ -696,7 +700,7 @@ streams:
|
||||
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}]
|
||||
```
|
||||
|
||||
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
|
||||
**PS.** For `kinesis` sources, you can use [echo](#source-echo) to get connection params using `bash`, `python` or any other script language.
|
||||
|
||||
#### Source: WebTorrent
|
||||
|
||||
@@ -715,9 +719,9 @@ By default, go2rtc establishes a connection to the source when any client reques
|
||||
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** 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
|
||||
- You can push data only to an existing stream (create a stream with empty source in config)
|
||||
- You can push multiple incoming sources to the same stream
|
||||
- You can push data to a non-empty stream, so it will have additional codecs inside
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -742,11 +746,11 @@ By default, go2rtc establishes a connection to the source when any client reques
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
|
||||
You can turn the browser of any PC or mobile into an IP camera with support for video and two-way audio. Or even broadcast your PC screen:
|
||||
|
||||
1. Create empty stream in the `go2rtc.yaml`
|
||||
2. Go to go2rtc WebUI
|
||||
3. Open `links` page for you stream
|
||||
3. Open `links` page for your stream
|
||||
4. Select `camera+microphone` or `display+speaker` option
|
||||
5. Open `webrtc` local page (your go2rtc **should work over HTTPS!**) or `share link` via [WebTorrent](#module-webtorrent) technology (work over HTTPS by default)
|
||||
|
||||
@@ -762,7 +766,7 @@ You can use **OBS Studio** or any other broadcast software with [WHIP](https://w
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
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 (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
|
||||
go2rtc supports playing 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 (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
|
||||
|
||||
API example:
|
||||
|
||||
@@ -775,7 +779,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
||||
- 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 files over `http` link, you need to add `#input=file` params for transcoding, so the 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
|
||||
@@ -787,10 +791,10 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||
|
||||
- Supported codecs: H264 for video and AAC for audio
|
||||
- AAC audio is required for YouTube, videos without audio will not work
|
||||
- AAC audio is required for YouTube; videos without audio will not work
|
||||
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||
|
||||
You can use API:
|
||||
You can use the API:
|
||||
|
||||
```
|
||||
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
|
||||
@@ -822,7 +826,7 @@ streams:
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
|
||||
**Important!** go2rtc passes requests from localhost and from unix socket without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to API. If not properly configured, an attacker can gain access to your cameras and even your server.
|
||||
**Important!** go2rtc passes requests from localhost and from Unix sockets without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.
|
||||
|
||||
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
|
||||
|
||||
@@ -830,7 +834,7 @@ The HTTP API is the main part for interacting with the application. Default addr
|
||||
|
||||
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
||||
- you can change the API `base_path` and host go2rtc on your main app webserver suburl
|
||||
- all files from `static_dir` hosted on root path: `/`
|
||||
- you can use raw TLS cert/key content or path to files
|
||||
|
||||
@@ -839,7 +843,7 @@ api:
|
||||
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
|
||||
username: "admin" # default "", Basic auth for WebUI
|
||||
password: "pass" # default "", Basic auth for WebUI
|
||||
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
|
||||
base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api)
|
||||
static_dir: "www" # default "", folder for static files (custom web interface)
|
||||
origin: "*" # default "", allow CORS requests (only * supported)
|
||||
tls_listen: ":443" # default "", enable HTTPS server
|
||||
@@ -863,7 +867,7 @@ api:
|
||||
|
||||
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
|
||||
|
||||
You can enable external password protection for your RTSP streams. Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server).
|
||||
You can enable external password protection for your RTSP streams. Password protection is always disabled for localhost calls (ex. FFmpeg or Hass on the same server).
|
||||
|
||||
```yaml
|
||||
rtsp:
|
||||
@@ -888,7 +892,7 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format.
|
||||
[Incoming stream](#incoming-sources) in RTMP format tested only with [OBS Studio](https://obsproject.com/) and a Dahua camera. Different FFmpeg versions have different problems with this format.
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
@@ -897,12 +901,12 @@ rtmp:
|
||||
|
||||
### Module: WebRTC
|
||||
|
||||
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.
|
||||
In most cases, [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses a 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.
|
||||
It can automatically detect your external IP via a public [STUN](https://en.wikipedia.org/wiki/STUN) server.
|
||||
It can establish an external direct connection via [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology even if you do 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/).
|
||||
But about 10-20% of users may need to configure additional settings for external access if **mobile phone** or **go2rtc server** is behind [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
|
||||
@@ -915,25 +919,25 @@ webrtc:
|
||||
|
||||
**Static public IP**
|
||||
|
||||
- forward the port 8555 on your router (you can use same 8555 port or any other as external port)
|
||||
- add your external IP-address and external port to YAML config
|
||||
- forward the port 8555 on your router (you can use the same 8555 port or any other as external port)
|
||||
- add your external IP address and external port to the YAML config
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
candidates:
|
||||
- 216.58.210.174:8555 # if you have static public IP-address
|
||||
- 216.58.210.174:8555 # if you have a static public IP address
|
||||
```
|
||||
|
||||
**Dynamic public IP**
|
||||
|
||||
- forward the port 8555 on your router (you can use same 8555 port or any other as the external port)
|
||||
- forward the port 8555 on your router (you can use the same 8555 port or any other as the external port)
|
||||
- add `stun` word and external port to YAML config
|
||||
- go2rtc automatically detects your external address with STUN-server
|
||||
- go2rtc automatically detects your external address with STUN server
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
candidates:
|
||||
- stun:8555 # if you have dynamic public IP-address
|
||||
- stun:8555 # if you have a dynamic public IP address
|
||||
```
|
||||
|
||||
**Private IP**
|
||||
@@ -947,7 +951,7 @@ ngrok:
|
||||
|
||||
**Hard tech way 1. Own TCP-tunnel**
|
||||
|
||||
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
|
||||
If you have a personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create a TCP tunnel and setup in the same way as "Static public IP". But use your VPS IP address in the YAML config.
|
||||
|
||||
**Hard tech way 2. Using TURN-server**
|
||||
|
||||
@@ -973,7 +977,7 @@ HomeKit module can work in two modes:
|
||||
|
||||
**Important**
|
||||
|
||||
- HomeKit cameras supports only H264 video and OPUS audio
|
||||
- HomeKit cameras support only H264 video and OPUS audio
|
||||
|
||||
**Minimal config**
|
||||
|
||||
@@ -1020,17 +1024,17 @@ homekit:
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This module support:
|
||||
This module supports:
|
||||
|
||||
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
|
||||
- Get any [incoming stream](#incoming-browser) from PC or mobile via [WebTorrent](https://webtorrent.io/) technology
|
||||
- Get any remote [go2rtc source](#source-webtorrent) via [WebTorrent](https://webtorrent.io/) technology
|
||||
|
||||
Securely and free. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT) you may need to set up external access to [WebRTC module](#module-webrtc).
|
||||
Securely and freely. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT), you may need to set up external access to [WebRTC module](#module-webrtc).
|
||||
|
||||
To generate sharing link or incoming link - goto go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
|
||||
To generate a sharing link or incoming link, go to the go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
|
||||
|
||||
You can create permanent external links in go2rtc config:
|
||||
You can create permanent external links in the go2rtc config:
|
||||
|
||||
```yaml
|
||||
webtorrent:
|
||||
@@ -1042,22 +1046,22 @@ webtorrent:
|
||||
|
||||
Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
|
||||
|
||||
TODO: article how it works...
|
||||
TODO: article on how it works...
|
||||
|
||||
### Module: ngrok
|
||||
|
||||
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
|
||||
With ngrok integration, you can get external access to your streams in situations when you have Internet with a private IP address.
|
||||
|
||||
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
|
||||
- ngrok is pre-installed for **Docker** and **Hass add-on** users
|
||||
- you may need external access for two different things:
|
||||
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
|
||||
- ngrok support authorization for your web interface
|
||||
- WebRTC stream, so you need a tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need a tunnel API HTTP port (ex. 1984)
|
||||
- ngrok supports authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for WebRTC stream)
|
||||
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||
|
||||
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
||||
@@ -1075,7 +1079,7 @@ ngrok:
|
||||
|
||||
**Tunnel for WebRTC and Web interface**
|
||||
|
||||
You need to create `ngrok.yaml` config file and add it to go2rtc config:
|
||||
You need to create `ngrok.yaml` config file and add it to the go2rtc config:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
@@ -1089,12 +1093,12 @@ version: "2"
|
||||
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
tunnels:
|
||||
api:
|
||||
addr: 1984 # use the same port as in go2rtc config
|
||||
addr: 1984 # use the same port as in the go2rtc config
|
||||
proto: http
|
||||
basic_auth:
|
||||
- admin:password # you can set login/pass for your web interface
|
||||
webrtc:
|
||||
addr: 8555 # use the same port as in go2rtc config
|
||||
addr: 8555 # use the same port as in the go2rtc config
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
@@ -1102,9 +1106,9 @@ See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for mo
|
||||
|
||||
### Module: Hass
|
||||
|
||||
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
|
||||
The best and easiest way to use go2rtc inside Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom Lovelace card.
|
||||
|
||||
But go2rtc is also compatible and can be used with [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration.
|
||||
But go2rtc is also compatible and can be used with the [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration.
|
||||
|
||||
You have several options on how to add a camera to Home Assistant:
|
||||
|
||||
@@ -1122,10 +1126,10 @@ You have several options on how to watch the stream from the cameras in Home Ass
|
||||
- Install any [go2rtc](#fast-start)
|
||||
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
|
||||
- RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
|
||||
- Use Picture Entity or Picture Glance lovelace card
|
||||
- Use Picture Entity or Picture Glance Lovelace card
|
||||
3. `Camera Entity` or `Camera URL` => [WebRTC Camera](https://github.com/AlexxIT/WebRTC) => Technology: `WebRTC/MSE/MP4/MJPEG`, codecs: `H264/H265/AAC/PCMU/PCMA/OPUS`, best latency, best compatibility.
|
||||
- Install and add [WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom integration
|
||||
- Use WebRTC Camera custom lovelace card
|
||||
- Use WebRTC Camera custom Lovelace card
|
||||
|
||||
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
|
||||
|
||||
@@ -1134,7 +1138,7 @@ streams:
|
||||
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
|
||||
```
|
||||
|
||||
**PS.** Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
|
||||
**PS.** Default Home Assistant lovelace cards don't support two-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons), but you need to use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
|
||||
|
||||
**PS.** There is also another nice card with go2rtc support - [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card).
|
||||
|
||||
@@ -1144,7 +1148,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. 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.
|
||||
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 in this case.
|
||||
|
||||
API examples:
|
||||
|
||||
@@ -1178,13 +1182,13 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
### Module: MJPEG
|
||||
|
||||
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
|
||||
**Important.** For stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has an MJPEG codec, you can receive **MJPEG stream** or **JPEG snapshots** via API.
|
||||
|
||||
You can receive an MJPEG stream in several ways:
|
||||
|
||||
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
|
||||
- some cameras has HTTP link with [MJPEG stream](#source-http)
|
||||
- some cameras has HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
|
||||
- some cameras have an HTTP link with [MJPEG stream](#source-http)
|
||||
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
|
||||
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
|
||||
|
||||
With this example, your stream will have both H264 and MJPEG codecs:
|
||||
@@ -1225,7 +1229,7 @@ log:
|
||||
|
||||
## Security
|
||||
|
||||
By default `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as use port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
|
||||
By default, `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as uses port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
|
||||
|
||||
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
|
||||
|
||||
@@ -1241,13 +1245,13 @@ webrtc:
|
||||
```
|
||||
|
||||
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
|
||||
- local access to API is not a problem for [Home Assistant Add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on same server and Add-on Web UI protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
|
||||
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
|
||||
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
|
||||
- local access to API is not a problem for the [Home Assistant add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on the same server, and the add-on web UI is protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
|
||||
- external access to WebRTC TCP port is not a problem, because it is used only for transmitting encrypted media data
|
||||
- anyway you need to open this port to your local network and to the Internet for WebRTC to work
|
||||
|
||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
|
||||
If you need web interface protection without the Home Assistant add-on, you need to use a reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
|
||||
|
||||
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
|
||||
PS. Additionally, WebRTC will try to use the 8555 UDP port to transmit encrypted media. It works without problems on the local network, and sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
|
||||
|
||||
## Codecs filters
|
||||
|
||||
@@ -1270,11 +1274,11 @@ Some examples:
|
||||
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)
|
||||
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices
|
||||
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)
|
||||
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non standard audio codecs, won't work on some players
|
||||
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non-standard audio codecs, won't work on some players
|
||||
|
||||
## Codecs madness
|
||||
|
||||
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
||||
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has many limitations in supporting different devices and browsers. It's all about patents and money; you can't do anything about it.
|
||||
|
||||
| Device | WebRTC | MSE | HTTP* | HLS |
|
||||
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
|
||||
@@ -1287,7 +1291,7 @@ Some examples:
|
||||
|
||||
[1]: https://apps.apple.com/app/home-assistant/id1099568401
|
||||
|
||||
`HTTP*` - HTTP Progressive Streaming, not related with [Progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
|
||||
`HTTP*` - HTTP Progressive Streaming, not related to [progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
|
||||
|
||||
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
|
||||
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
|
||||
@@ -1298,7 +1302,7 @@ Some examples:
|
||||
|
||||
- Go2rtc support [automatic repack](#built-in-transcoding) `PCMA/PCMU/PCM` codecs to `FLAC` for MSE/MP4/HLS so they will work almost anywhere
|
||||
- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
|
||||
- `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple)
|
||||
- `OPUS` and `MP3` inside **MP4** are part of the standard, but some players do not support them anyway (especially Apple)
|
||||
|
||||
**Apple devices**
|
||||
|
||||
@@ -1320,7 +1324,7 @@ Some examples:
|
||||
|
||||
There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.
|
||||
|
||||
But go2rtc has some simple algorithms. They are turned on automatically, you do not need to set them up additionally.
|
||||
But go2rtc has some simple algorithms. They are turned on automatically; you do not need to set them up additionally.
|
||||
|
||||
**PCM for MSE/MP4/HLS**
|
||||
|
||||
@@ -1332,7 +1336,7 @@ PCMA/PCMU => PCM => FLAC => MSE/MP4/HLS
|
||||
|
||||
**Resample PCMA/PCMU for WebRTC**
|
||||
|
||||
By default WebRTC support only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codec with with a different sample rate. Also go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
|
||||
By default WebRTC supports only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codecs with a different sample rate. Also, go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
|
||||
|
||||
```
|
||||
PCM/xxx => PCMA/8000 => WebRTC
|
||||
@@ -1342,24 +1346,24 @@ PCMU/xxx => PCMU/8000 => WebRTC
|
||||
|
||||
**Important**
|
||||
|
||||
- FLAC codec not supported in a RTSP stream. If you using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio - you should setup transcoding to AAC codec.
|
||||
- PCMA and PCMU are VERY low quality codecs. Them support only 256! different sounds. Use them only when you have no other options.
|
||||
- FLAC codec not supported in an RTSP stream. If you are using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio, you should set up transcoding to the AAC codec.
|
||||
- PCMA and PCMU are VERY low-quality codecs. They support only 256! different sounds. Use them only when you have no other options.
|
||||
|
||||
## Codecs negotiation
|
||||
|
||||
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
|
||||
|
||||
- this camera support 2-way audio standard **ONVIF Profile T**
|
||||
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
|
||||
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
|
||||
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
|
||||
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
|
||||
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
|
||||
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
|
||||
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
|
||||
- this camera supports two-way audio standard **ONVIF Profile T**
|
||||
- this camera supports codecs **H264, H265** for send video, and you select `H264` in camera settings
|
||||
- this camera supports codecs **AAC, PCMU, PCMA** for sending audio (from mic), and you select `AAC/16000` in camera settings
|
||||
- this camera supports codecs **AAC, PCMU, PCMA** for receiving audio (to speaker), you don't need to select them
|
||||
- your browser supports codecs **H264, VP8, VP9, AV1** for receiving video, you don't need to select them
|
||||
- your browser supports codecs **OPUS, PCMU, PCMA** for sending and receiving audio, you don't need to select them
|
||||
- you can't get camera audio directly, because its audio codecs don't match with your browser codecs
|
||||
- so you decide to use transcoding via FFmpeg and add this setting to the config YAML file
|
||||
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
|
||||
|
||||
Now you have stream with two sources - **RTSP and FFmpeg**:
|
||||
Now you have a stream with two sources - **RTSP and FFmpeg**:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -1368,22 +1372,23 @@ streams:
|
||||
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
|
||||
```
|
||||
|
||||
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
|
||||
**go2rtc** automatically matches codecs for your browser and all your stream sources. This is called **multi-source two-way codec negotiation**. And this is one of the main features of this app.
|
||||
|
||||

|
||||
|
||||
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
|
||||
**PS.** You can select `PCMU` or `PCMA` codec in camera settings and not use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
|
||||
|
||||
## Projects using go2rtc
|
||||
|
||||
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
|
||||
- [Frigate](https://frigate.video/) 0.12+ - open-source NVR built around real-time AI object detection
|
||||
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
|
||||
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - Alternative IP Camera firmware from an open community
|
||||
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
|
||||
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
|
||||
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
|
||||
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
|
||||
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
||||
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - alternative IP camera firmware from an open community
|
||||
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - custom firmware for Wyze cameras
|
||||
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - a small project that provides a video/audio stream from Eufy cameras that don't directly support RTSP
|
||||
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for controlling Eufy security devices
|
||||
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² module
|
||||
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring-to-MQTT bridge
|
||||
- [lightNVR](https://github.com/opensensor/lightNVR)
|
||||
|
||||
**Distributions**
|
||||
|
||||
@@ -1391,20 +1396,20 @@ streams:
|
||||
- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc)
|
||||
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
|
||||
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
|
||||
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
||||
- [Proxmox Helper Scripts](https://github.com/community-scripts/ProxmoxVE/)
|
||||
- [QNAP](https://www.myqnap.org/product/go2rtc/)
|
||||
- [Synology NAS](https://synocommunity.com/package/go2rtc)
|
||||
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
|
||||
|
||||
## Cameras experience
|
||||
## Camera experience
|
||||
|
||||
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
|
||||
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
|
||||
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol implementation, many bugs in SDP
|
||||
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
|
||||
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
|
||||
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
|
||||
- [Reolink](https://reolink.com/) - some models have an awful, unusable RTSP implementation and not the best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
|
||||
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not the best protocol implementation
|
||||
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
|
||||
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
|
||||
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usually have `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol implementation, low stream quality, few settings, packet loss?
|
||||
|
||||
## TIPS
|
||||
|
||||
@@ -1421,22 +1426,22 @@ streams:
|
||||
|
||||
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
|
||||
|
||||
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance.
|
||||
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default Lovelace Picture Entity or Picture Glance.
|
||||
|
||||
**Q. Should I use go2rtc addon or WebRTC Camera integration?**
|
||||
**Q. Should I use the go2rtc add-on or WebRTC Camera integration?**
|
||||
|
||||
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
|
||||
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time Hass is rebooted, all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
|
||||
|
||||
Basic users can use **WebRTC Camera** integration. Advanced users can use go2rtc addon or Frigate 12+ addon.
|
||||
Basic users can use the **WebRTC Camera** integration. Advanced users can use the go2rtc add-on or the Frigate 0.12+ add-on.
|
||||
|
||||
**Q. Which RTSP link should I use inside Hass?**
|
||||
|
||||
You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
|
||||
You can use a direct link to your cameras there (as you always do). **go2rtc** supports zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
|
||||
|
||||
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
|
||||
Also, you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this add-on with additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as a source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connections - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
|
||||
|
||||
Use any config what you like.
|
||||
Use any config that you like.
|
||||
|
||||
**Q. What about lovelace card with support 2-way audio?**
|
||||
**Q. What about Lovelace card with support for two-way audio?**
|
||||
|
||||
At this moment I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).
|
||||
At this moment, I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.24"
|
||||
ARG GO_VERSION="1.25"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
|
@@ -4,7 +4,7 @@
|
||||
# only debian 13 (trixie) has latest ffmpeg
|
||||
# https://packages.debian.org/trixie/ffmpeg
|
||||
ARG DEBIAN_VERSION="trixie-slim"
|
||||
ARG GO_VERSION="1.24-bookworm"
|
||||
ARG GO_VERSION="1.25-bookworm"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.13-slim-bookworm"
|
||||
ARG GO_VERSION="1.24-bookworm"
|
||||
ARG GO_VERSION="1.25-bookworm"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
|
47
go.mod
47
go.mod
@@ -1,50 +1,49 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.20
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/expr-lang/expr v1.17.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.63
|
||||
github.com/pion/ice/v2 v2.3.37
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/miekg/dns v1.1.66
|
||||
github.com/pion/ice/v4 v4.0.10
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.11
|
||||
github.com/pion/sdp/v3 v3.0.10
|
||||
github.com/pion/srtp/v2 v2.0.20
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.3.5
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/pion/rtp v1.8.20
|
||||
github.com/pion/sdp/v3 v3.0.14
|
||||
github.com/pion/srtp/v3 v3.0.6
|
||||
github.com/pion/stun/v3 v3.0.0
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.52.0 // indirect
|
||||
github.com/asticode/go-astikit v0.56.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.36 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
)
|
||||
|
163
go.sum
163
go.sum
@@ -1,6 +1,8 @@
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y=
|
||||
github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ=
|
||||
github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astikit v0.56.0 h1:DmD2p7YnvxiPdF0h+dRmos3bsejNEXbycENsY5JfBqw=
|
||||
github.com/asticode/go-astikit v0.56.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -8,19 +10,17 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
|
||||
github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k=
|
||||
github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -32,49 +32,58 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
|
||||
github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
|
||||
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
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.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
|
||||
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
|
||||
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
|
||||
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
|
||||
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
|
||||
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
|
||||
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
|
||||
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
|
||||
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
|
||||
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
|
||||
github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg=
|
||||
github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk=
|
||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
@@ -82,96 +91,50 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
7
internal/alsa/alsa.go
Normal file
7
internal/alsa/alsa.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))
|
||||
|
||||
package alsa
|
||||
|
||||
func Init() {
|
||||
// not supported
|
||||
}
|
83
internal/alsa/alsa_linux.go
Normal file
83
internal/alsa/alsa_linux.go
Normal file
@@ -0,0 +1,83 @@
|
||||
//go:build linux && (386 || amd64 || arm || arm64 || mipsle)
|
||||
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa"
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("alsa", alsa.Open)
|
||||
|
||||
api.HandleFunc("api/alsa", apiAlsa)
|
||||
}
|
||||
|
||||
func apiAlsa(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir("/dev/snd/")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []*api.Source
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), "pcm") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := "/dev/snd/" + file.Name()
|
||||
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := dev.Info()
|
||||
if err == nil {
|
||||
formats := formatsToString(dev.ListFormats())
|
||||
r1, r2 := dev.RangeRates()
|
||||
c1, c2 := dev.RangeChannels()
|
||||
source := &api.Source{
|
||||
Name: info.ID,
|
||||
Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2),
|
||||
URL: "alsa:device?audio=" + path,
|
||||
}
|
||||
if !strings.Contains(source.Name, info.Name) {
|
||||
source.Name += ", " + info.Name
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
}
|
||||
|
||||
func formatsToString(formats []byte) string {
|
||||
var s string
|
||||
for i, format := range formats {
|
||||
if i > 0 {
|
||||
s += " "
|
||||
}
|
||||
switch format {
|
||||
case 2:
|
||||
s += "s16le"
|
||||
case 10:
|
||||
s += "s32le"
|
||||
default:
|
||||
s += strconv.Itoa(int(format))
|
||||
}
|
||||
|
||||
}
|
||||
return s
|
||||
}
|
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -132,7 +133,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
if err = handler(tr, msg); err != nil {
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
|
||||
errMsg := core.StripUserinfo(err.Error())
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
@@ -18,11 +19,16 @@ func LoadConfig(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
var configMu sync.Mutex
|
||||
|
||||
func PatchConfig(path []string, value any) error {
|
||||
if ConfigPath == "" {
|
||||
return errors.New("config file disabled")
|
||||
}
|
||||
|
||||
configMu.Lock()
|
||||
defer configMu.Unlock()
|
||||
|
||||
// empty config is OK
|
||||
b, _ := os.ReadFile(ConfigPath)
|
||||
|
||||
|
10
internal/eseecloud/eseecloud.go
Normal file
10
internal/eseecloud/eseecloud.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package eseecloud
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/eseecloud"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("eseecloud", eseecloud.Dial)
|
||||
}
|
12
internal/exec/README.md
Normal file
12
internal/exec/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## Backchannel
|
||||
|
||||
- You can check audio card names in the **Go2rtc > WebUI > Add**
|
||||
- You can specify multiple backchannel lines with different codecs
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
two_way_audio_win:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
- exec:ffplay -nodisp -probesize 32 -f s16le -ar 16000 -#backchannel=1#audio=s16le/16000
|
||||
- exec:ffplay -nodisp -probesize 32 -f alaw -ar 8000 -#backchannel=1#audio=alaw/8000
|
||||
```
|
@@ -19,9 +19,9 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/stdin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
}
|
||||
|
||||
if query.Get("backchannel") == "1" {
|
||||
return stdin.NewClient(cmd)
|
||||
return pcm.NewBackchannel(cmd, query.Get("audio"))
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
|
@@ -12,7 +12,7 @@ func Init() {
|
||||
log := app.GetLogger("expr")
|
||||
|
||||
streams.RedirectFunc("expr", func(url string) (string, error) {
|
||||
v, err := expr.Run(url[5:])
|
||||
v, err := expr.Eval(url[5:], nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@@ -113,6 +113,7 @@ var defaults = map[string]string{
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||
"pcml/16000": "-c:a pcm_s16le -ar:a 16000 -ac:a 1",
|
||||
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
|
||||
|
||||
// hardware Intel and AMD on Linux
|
||||
|
@@ -42,6 +42,7 @@ func NewProducer(url string) (core.Producer, error) {
|
||||
Codecs: []*core.Codec{
|
||||
// OPUS will always marked as OPUS/48000/2
|
||||
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
{Name: core.CodecPCM, ClockRate: 16000},
|
||||
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||
@@ -97,6 +98,8 @@ func (p *Producer) newURL() string {
|
||||
s += "#audio=opus"
|
||||
case core.CodecAAC:
|
||||
s += "#audio=aac/16000"
|
||||
case core.CodecPCML:
|
||||
s += "#audio=pcml/16000"
|
||||
case core.CodecPCM:
|
||||
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
|
||||
case core.CodecPCMA:
|
||||
|
@@ -36,8 +36,7 @@ func Init() {
|
||||
var log zerolog.Logger
|
||||
|
||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
stream := streams.GetOrPatch(r.URL.Query())
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (s *Stream) Play(source string) error {
|
||||
func (s *Stream) Play(urlOrProd any) error {
|
||||
s.mu.Lock()
|
||||
for _, producer := range s.producers {
|
||||
if producer.state == stateInternal && producer.conn != nil {
|
||||
@@ -16,12 +16,18 @@ func (s *Stream) Play(source string) error {
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var source string
|
||||
var src core.Producer
|
||||
|
||||
switch urlOrProd.(type) {
|
||||
case string:
|
||||
if source = urlOrProd.(string); source == "" {
|
||||
return nil
|
||||
}
|
||||
case core.Producer:
|
||||
src = urlOrProd.(core.Producer)
|
||||
}
|
||||
|
||||
for _, producer := range s.producers {
|
||||
if producer.conn == nil {
|
||||
continue
|
||||
@@ -140,10 +146,12 @@ func matchMedia(prod core.Producer, cons core.Consumer) bool {
|
||||
|
||||
track, err := prod.GetTrack(prodMedia, prodCodec)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[streams] can't get track")
|
||||
continue
|
||||
}
|
||||
|
||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
log.Warn().Err(err).Msg("[streams] can't add track")
|
||||
continue
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
|
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// streamsHandler supports:
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1600
|
||||
@@ -27,7 +28,6 @@ func crealityClient(url string) (core.Producer, error) {
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
}
|
||||
|
||||
// TODO: return webrtc.SessionDescription
|
||||
@@ -36,6 +36,8 @@ func crealityClient(url string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
body, err := offerToB64(offer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -61,6 +63,12 @@ func crealityClient(url string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] answer:\n%s", answer)
|
||||
|
||||
if answer, err = fixCrealitySDP(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = prod.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,3 +116,37 @@ func answerFromB64(r io.Reader) (string, error) {
|
||||
// string "v=0..."
|
||||
return v["sdp"], nil
|
||||
}
|
||||
|
||||
func fixCrealitySDP(value string) (string, error) {
|
||||
var sd sdp.SessionDescription
|
||||
if err := sd.UnmarshalString(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
// important to skip first codec, because second codec will be used
|
||||
skip := md.MediaName.Formats[0]
|
||||
md.MediaName.Formats = md.MediaName.Formats[1:]
|
||||
|
||||
attrs := make([]sdp.Attribute, 0, len(md.Attributes))
|
||||
for _, attr := range md.Attributes {
|
||||
switch attr.Key {
|
||||
case "fmtp", "rtpmap":
|
||||
// important to skip fmtp with x-google, because this is second fmtp for same codec
|
||||
// and pion library will fail parsing this SDP
|
||||
if strings.HasPrefix(attr.Value, skip) || strings.Contains(attr.Value, "x-google") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
|
||||
md.Attributes = attrs
|
||||
|
||||
b, err := sd.Marshal()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type kinesisRequest struct {
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// This package handles the Milestone WebRTC session lifecycle, including authentication,
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
|
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
const MimeSDP = "application/sdp"
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
|
@@ -2,10 +2,11 @@ package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -36,3 +37,37 @@ func TestWebRTCAPIv2(t *testing.T) {
|
||||
require.Equal(t, "v=0\n...", offer.SDP)
|
||||
require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0])
|
||||
}
|
||||
|
||||
func TestCrealitySDP(t *testing.T) {
|
||||
sdp := `v=0
|
||||
o=- 1495799811084970 1495799811084970 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=msid-semantic:WMS *
|
||||
a=group:BUNDLE 0
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 96 98
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=fmtp:96 profile-level-id=42e01f;level-asymmetry-allowed=1
|
||||
a=fmtp:98 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1
|
||||
a=fmtp:98 x-google-max-bitrate=6000;x-google-min-bitrate=2000;x-google-start-bitrate=4000
|
||||
a=rtpmap:96 H264/90000
|
||||
a=rtpmap:98 H264/90000
|
||||
a=ssrc:1 cname:pear
|
||||
c=IN IP4 0.0.0.0
|
||||
a=sendonly
|
||||
a=mid:0
|
||||
a=rtcp-mux
|
||||
a=ice-ufrag:7AVa
|
||||
a=ice-pwd:T+F/5y05Paw+mtG5Jrd8N3
|
||||
a=ice-options:trickle
|
||||
a=fingerprint:sha-256 A5:AB:C0:4E:29:5B:BD:3B:7D:88:24:6C:56:6B:2A:79:A3:76:99:35:57:75:AD:C8:5A:A6:34:20:88:1B:68:EF
|
||||
a=setup:passive
|
||||
a=candidate:1 1 UDP 2015363327 172.22.233.10 48929 typ host
|
||||
a=candidate:2 1 TCP 1015021823 172.22.233.10 0 typ host tcptype active
|
||||
a=candidate:3 1 TCP 1010827519 172.22.233.10 60677 typ host tcptype passive
|
||||
`
|
||||
sdp, err := fixCrealitySDP(sdp)
|
||||
require.Nil(t, err)
|
||||
require.False(t, strings.Contains(sdp, "x-google-max-bitrate"))
|
||||
}
|
||||
|
281
internal/wyoming/README.md
Normal file
281
internal/wyoming/README.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Wyoming
|
||||
|
||||
This module provide [Wyoming Protocol](https://www.home-assistant.io/integrations/wyoming/) support to create local voice assistants using [Home Assistant](https://www.home-assistant.io/).
|
||||
|
||||
- go2rtc can act as [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite)
|
||||
- go2rtc can act as [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external)
|
||||
- go2rtc can act as [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external)
|
||||
- any supported audio source with PCM codec can be used as audio input
|
||||
- any supported two-way audio source with PCM codec can be used as audio output
|
||||
- any desktop/server microphone/speaker can be used as two-way audio source
|
||||
- supported any OS via FFmpeg or any similar software
|
||||
- supported Linux via alsa source
|
||||
- you can change the behavior using the built-in scripting engine
|
||||
|
||||
## Typical Voice Pipeline
|
||||
|
||||
1. Audio stream (MIC)
|
||||
- any audio source with PCM codec support (include PCMA/PCMU)
|
||||
2. Voice Activity Detector (VAD)
|
||||
3. Wake Word (WAKE)
|
||||
- [OpenWakeWord](https://www.home-assistant.io/voice_control/create_wake_word/)
|
||||
4. Speech-to-Text (STT)
|
||||
- [Whisper](https://github.com/home-assistant/addons/blob/master/whisper/README.md)
|
||||
- [Vosk](https://github.com/rhasspy/hassio-addons/blob/master/vosk/README.md)
|
||||
5. Conversation agent (INTENT)
|
||||
- [Home Assistant](https://www.home-assistant.io/integrations/conversation/)
|
||||
6. Text-to-speech (TTS)
|
||||
- [Google Translate](https://www.home-assistant.io/integrations/google_translate/)
|
||||
- [Piper](https://github.com/home-assistant/addons/blob/master/piper/README.md)
|
||||
7. Audio stream (SND)
|
||||
- any source with two-way audio (backchannel) and PCM codec support (include PCMA/PCMU)
|
||||
|
||||
You can use a large number of different projects for WAKE, STT, INTENT and TTS thanks to the Home Assistant.
|
||||
|
||||
And you can use a large number of different technologies for MIC and SND thanks to Go2rtc.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can optionally specify WAKE service. So go2rtc will start transmitting audio to Home Assistant only after WAKE word. If the WAKE service cannot be connected to or not specified - go2rtc will pass all audio to Home Assistant. In this case WAKE service must be configured in your Voice Assistant pipeline.
|
||||
|
||||
You can optionally specify VAD threshold. So go2rtc will start transmitting audio to WAKE service only after some audio noise.
|
||||
|
||||
Your stream must support audio transmission in PCM codec (include PCMA/PCMU).
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
stream_name_from_streams_section:
|
||||
listen: :10700
|
||||
name: "My Satellite" # optional name
|
||||
wake_uri: tcp://192.168.1.23:10400 # optional WAKE service
|
||||
vad_threshold: 1 # optional VAD threshold (from 0.1 to 3.5)
|
||||
```
|
||||
|
||||
Home Assistant -> Settings -> Integrations -> Add -> Wyoming Protocol -> Host + Port from `go2rtc.yaml`
|
||||
|
||||
Select one or multiple wake words:
|
||||
```yaml
|
||||
wake_uri: tcp://192.168.1.23:10400?name=alexa_v0.1&name=hey_jarvis_v0.1&name=hey_mycroft_v0.1&name=hey_rhasspy_v0.1&name=ok_nabu_v0.1
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
You can add wyoming event handling using the [expr](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md) language. For example, to pronounce TTS on some media player from HA.
|
||||
|
||||
Turn on the logs to see what kind of events happens.
|
||||
|
||||
This is what the default scripts look like:
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
script_example:
|
||||
event:
|
||||
run-satellite: Detect()
|
||||
pause-satellite: Stop()
|
||||
voice-stopped: Pause()
|
||||
audio-stop: PlayAudio() && WriteEvent("played") && Detect()
|
||||
error: Detect()
|
||||
internal-run: WriteEvent("run-pipeline", '{"start_stage":"wake","end_stage":"tts"}') && Stream()
|
||||
internal-detection: WriteEvent("run-pipeline", '{"start_stage":"asr","end_stage":"tts"}') && Stream()
|
||||
```
|
||||
|
||||
Supported functions and variables:
|
||||
|
||||
- `Detect()` - start the VAD and WAKE word detection process
|
||||
- `Stream()` - start transmission of audio data to the client (Home Assistant)
|
||||
- `Stop()` - stop and disconnect stream without disconnecting client (Home Assistant)
|
||||
- `Pause()` - temporary pause of audio transfer, without disconnecting the stream
|
||||
- `PlayAudio()` - playing the last audio that was sent from client (Home Assistant)
|
||||
- `WriteEvent(type, data)` - send event to client (Home Assistant)
|
||||
- `Sleep(duration)` - temporary script pause (ex. `Sleep('1.5s')`)
|
||||
- `PlayFile(path)` - play audio from `wav` file
|
||||
- `Type` - type (name) of event
|
||||
- `Data` - event data in JSON format (ex. `{"text":"how are you"}`)
|
||||
- also available other functions from [expr](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md) module (ex. `fetch`)
|
||||
|
||||
If you write a script for an event - the default action is no longer executed. You need to repeat the necessary steps yourself.
|
||||
|
||||
In addition to the standard events, there are two additional events:
|
||||
|
||||
- `internal-run` - called after `Detect()` when VAD detected, but WAKE service unavailable
|
||||
- `internal-detection` - called after `Detect()` when WAKE word detected
|
||||
|
||||
**Example 1.** You want to play a sound file when a wake word detected (only `wav` supported):
|
||||
|
||||
- `PlayFile` and `PlayAudio` functions are executed synchronously, the following steps will be executed only after they are completed
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
script_example:
|
||||
event:
|
||||
internal-detection: PlayFile('/media/beep.wav') && WriteEvent("run-pipeline", '{"start_stage":"asr","end_stage":"tts"}') && Stream()
|
||||
```
|
||||
|
||||
**Example 2.** You want to play TTS on a Home Assistant media player:
|
||||
|
||||
Each event has a `Type` and `Data` in JSON format. You can use their values in scripts.
|
||||
|
||||
- in the `synthesize` step, we get the value of the `text` and call the HA REST API
|
||||
- in the `audio-stop` step we get the duration of the TTS in seconds, wait for this time and start the pipeline again
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
script_example:
|
||||
event:
|
||||
synthesize: |
|
||||
let text = fromJSON(Data).text;
|
||||
let token = 'eyJhbGci...';
|
||||
fetch('http://localhost:8123/api/services/tts/speak', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': 'Bearer '+token,'Content-Type': 'application/json'},
|
||||
body: toJSON({
|
||||
entity_id: 'tts.google_translate_com',
|
||||
media_player_entity_id: 'media_player.google_nest',
|
||||
message: text,
|
||||
language: 'en',
|
||||
}),
|
||||
}).ok
|
||||
audio-stop: |
|
||||
let timestamp = fromJSON(Data).timestamp;
|
||||
let delay = string(timestamp)+'s';
|
||||
Sleep(delay) && WriteEvent("played") && Detect()
|
||||
```
|
||||
|
||||
## Config examples
|
||||
|
||||
Satellite on Windows server using FFmpeg and FFplay.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
satellite_win:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
- exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050
|
||||
|
||||
wyoming:
|
||||
satellite_win:
|
||||
listen: :10700
|
||||
name: "Windows Satellite"
|
||||
wake_uri: tcp://192.168.1.23:10400
|
||||
vad_threshold: 1
|
||||
```
|
||||
|
||||
Satellite on Dahua camera with two-way audio support.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua_camera:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
|
||||
|
||||
wyoming:
|
||||
dahua_camera:
|
||||
listen: :10700
|
||||
name: "Dahua Satellite"
|
||||
wake_uri: tcp://192.168.1.23:10400
|
||||
vad_threshold: 1
|
||||
```
|
||||
|
||||
Satellite on external wyoming Microphone and Sound.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
wyoming_external:
|
||||
- wyoming://192.168.1.23:10600 # wyoming-mic-external
|
||||
- wyoming://192.168.1.23:10601?backchannel=1 # wyoming-snd-external
|
||||
|
||||
wyoming:
|
||||
wyoming_external:
|
||||
listen: :10700
|
||||
name: "Wyoming Satellite"
|
||||
wake_uri: tcp://192.168.1.23:10400
|
||||
vad_threshold: 1
|
||||
```
|
||||
|
||||
## Wyoming External Microphone and Sound
|
||||
|
||||
Advanced users, who want to enjoy the [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite) project, can use go2rtc as a [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external) or [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external).
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
wyoming_mic_external:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
wyoming_snd_external:
|
||||
- exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050
|
||||
|
||||
wyoming:
|
||||
wyoming_mic_external:
|
||||
listen: :10600
|
||||
mode: mic
|
||||
wyoming_snd_external:
|
||||
listen: :10601
|
||||
mode: snd
|
||||
```
|
||||
|
||||
**docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
satellite:
|
||||
build: wyoming-satellite # https://github.com/rhasspy/wyoming-satellite
|
||||
ports:
|
||||
- "10700:10700"
|
||||
command:
|
||||
- "--name"
|
||||
- "my satellite"
|
||||
- "--mic-uri"
|
||||
- "tcp://192.168.1.23:10600"
|
||||
- "--snd-uri"
|
||||
- "tcp://192.168.1.23:10601"
|
||||
- "--debug"
|
||||
```
|
||||
|
||||
## Wyoming External Source
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
wyoming_external:
|
||||
- wyoming://192.168.1.23:10600
|
||||
- wyoming://192.168.1.23:10601?backchannel=1
|
||||
```
|
||||
|
||||
**docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
microphone:
|
||||
build: wyoming-mic-external # https://github.com/rhasspy/wyoming-mic-external
|
||||
ports:
|
||||
- "10600:10600"
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
group_add:
|
||||
- audio
|
||||
command:
|
||||
- "--device"
|
||||
- "sysdefault"
|
||||
- "--debug"
|
||||
playback:
|
||||
build: wyoming-snd-external # https://github.com/rhasspy/wyoming-snd-external
|
||||
ports:
|
||||
- "10601:10601"
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
group_add:
|
||||
- audio
|
||||
command:
|
||||
- "--device"
|
||||
- "sysdefault"
|
||||
- "--debug"
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
```yaml
|
||||
log:
|
||||
wyoming: trace
|
||||
```
|
106
internal/wyoming/wyoming.go
Normal file
106
internal/wyoming/wyoming.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/wyoming"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("wyoming", wyoming.Dial)
|
||||
|
||||
// server
|
||||
var cfg struct {
|
||||
Mod map[string]struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Name string `yaml:"name"`
|
||||
Mode string `yaml:"mode"`
|
||||
Event map[string]string `yaml:"event"`
|
||||
WakeURI string `yaml:"wake_uri"`
|
||||
VADThreshold float32 `yaml:"vad_threshold"`
|
||||
} `yaml:"wyoming"`
|
||||
}
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("wyoming")
|
||||
|
||||
for name, cfg := range cfg.Mod {
|
||||
stream := streams.Get(name)
|
||||
if stream == nil {
|
||||
log.Warn().Msgf("[wyoming] missing stream: %s", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if cfg.Name == "" {
|
||||
cfg.Name = name
|
||||
}
|
||||
|
||||
srv := &wyoming.Server{
|
||||
Name: cfg.Name,
|
||||
Event: cfg.Event,
|
||||
VADThreshold: int16(1000 * cfg.VADThreshold), // 1.0 => 1000
|
||||
WakeURI: cfg.WakeURI,
|
||||
MicHandler: func(cons core.Consumer) error {
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
// not best solution
|
||||
if i, ok := cons.(interface{ OnClose(func()) }); ok {
|
||||
i.OnClose(func() {
|
||||
stream.RemoveConsumer(cons)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
},
|
||||
SndHandler: func(prod core.Producer) error {
|
||||
return stream.Play(prod)
|
||||
},
|
||||
Trace: func(format string, v ...any) {
|
||||
log.Trace().Msgf("[wyoming] "+format, v...)
|
||||
},
|
||||
Error: func(format string, v ...any) {
|
||||
log.Error().Msgf("[wyoming] "+format, v...)
|
||||
},
|
||||
}
|
||||
go serve(srv, cfg.Mode, cfg.Listen)
|
||||
}
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func serve(srv *wyoming.Server, mode, address string) {
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("[wyoming] listen")
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go handle(srv, mode, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func handle(srv *wyoming.Server, mode string, conn net.Conn) {
|
||||
addr := conn.RemoteAddr()
|
||||
|
||||
log.Trace().Msgf("[wyoming] %s connected", addr)
|
||||
|
||||
switch mode {
|
||||
case "mic":
|
||||
srv.HandleMic(conn)
|
||||
case "snd":
|
||||
srv.HandleSnd(conn)
|
||||
default:
|
||||
srv.Handle(conn)
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[wyoming] %s disconnected", addr)
|
||||
}
|
22
internal/yandex/README.md
Normal file
22
internal/yandex/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Yandex
|
||||
|
||||
Source for receiving stream from new [Yandex IP camera](https://alice.yandex.ru/smart-home/security/ipcamera).
|
||||
|
||||
## Get Yandex token
|
||||
|
||||
1. Install HomeAssistant integration [YandexStation](https://github.com/AlexxIT/YandexStation).
|
||||
2. Copy token from HomeAssistant config folder: `/config/.storage/core.config_entries`, key: `"x_token"`.
|
||||
|
||||
## Get device ID
|
||||
|
||||
1. Open this link in any browser: https://iot.quasar.yandex.ru/m/v3/user/devices
|
||||
2. Copy ID of your camera, key: `"id"`.
|
||||
|
||||
## Config examples
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
yandex_stream: yandex:?x_token=XXXX&device_id=XXXX
|
||||
yandex_snapshot: yandex:?x_token=XXXX&device_id=XXXX&snapshot
|
||||
yandex_snapshot_custom_size: yandex:?x_token=XXXX&device_id=XXXX&snapshot=h=540
|
||||
```
|
152
internal/yandex/goloom.go
Normal file
152
internal/yandex/goloom.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
xwebrtc "github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func goloomClient(serviceURL, serviceName, roomId, participantId, credentials string) (core.Producer, error) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serviceURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
time.Sleep(time.Second)
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
s := fmt.Sprintf(`{"hello": {
|
||||
"credentials":"%s","participantId":"%s","roomId":"%s","serviceName":"%s","sdkInitializationId":"%s",
|
||||
"capabilitiesOffer":{},"sendAudio":false,"sendSharing":false,"sendVideo":false,
|
||||
"sdkInfo":{"hwConcurrency":4,"implementation":"browser","version":"5.4.0"},
|
||||
"participantAttributes":{"description":"","name":"mike","role":"SPEAKER"},
|
||||
"participantMeta":{"description":"","name":"mike","role":"SPEAKER","sendAudio":false,"sendVideo":false}
|
||||
},"uid":"%s"}`,
|
||||
credentials, participantId, roomId, serviceName,
|
||||
uuid.NewString(), uuid.NewString(),
|
||||
)
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage, []byte(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, _, err = conn.ReadMessage(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc, err := webrtc.PeerConnection(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod := xwebrtc.NewConn(pc)
|
||||
prod.FormatName = "yandex"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "wss"
|
||||
|
||||
var connState core.Waiter
|
||||
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
switch msg {
|
||||
case pion.PeerConnectionStateConnecting:
|
||||
case pion.PeerConnectionStateConnected:
|
||||
connState.Done(nil)
|
||||
default:
|
||||
connState.Done(errors.New("webrtc: " + msg.String()))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
var msg map[string]json.RawMessage
|
||||
if err = conn.ReadJSON(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range msg {
|
||||
switch k {
|
||||
case "uid":
|
||||
continue
|
||||
case "serverHello":
|
||||
case "subscriberSdpOffer":
|
||||
var sdp subscriberSdp
|
||||
if err = json.Unmarshal(v, &sdp); err != nil {
|
||||
return
|
||||
}
|
||||
//log.Trace().Msgf("offer:\n%s", sdp.Sdp)
|
||||
if err = prod.SetOffer(sdp.Sdp); err != nil {
|
||||
return
|
||||
}
|
||||
if sdp.Sdp, err = prod.GetAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
//log.Trace().Msgf("answer:\n%s", sdp.Sdp)
|
||||
|
||||
var raw []byte
|
||||
if raw, err = json.Marshal(sdp); err != nil {
|
||||
return
|
||||
}
|
||||
s = fmt.Sprintf(`{"uid":"%s","subscriberSdpAnswer":%s}`, uuid.NewString(), raw)
|
||||
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return
|
||||
}
|
||||
case "webrtcIceCandidate":
|
||||
var candidate webrtcIceCandidate
|
||||
if err = json.Unmarshal(v, &candidate); err != nil {
|
||||
return
|
||||
}
|
||||
if err = prod.AddCandidate(candidate.Candidate); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
//log.Trace().Msgf("%s : %s", k, v)
|
||||
}
|
||||
|
||||
if msg["ack"] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s = fmt.Sprintf(`{"uid":%s,"ack":{"status":{"code":"OK"}}}`, msg["uid"])
|
||||
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = connState.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = fmt.Sprintf(`{"uid":"%s","setSlots":{"slots":[{"width":0,"height":0}],"audioSlotsCount":0,"key":1,"shutdownAllVideo":false,"withSelfView":false,"selfViewVisibility":"ON_LOADING_THEN_HIDE","gridConfig":{}}}`, uuid.NewString())
|
||||
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
type subscriberSdp struct {
|
||||
PcSeq int `json:"pcSeq"`
|
||||
Sdp string `json:"sdp"`
|
||||
}
|
||||
|
||||
type webrtcIceCandidate struct {
|
||||
PcSeq int `json:"pcSeq"`
|
||||
Target string `json:"target"`
|
||||
Candidate string `json:"candidate"`
|
||||
SdpMid string `json:"sdpMid"`
|
||||
SdpMlineIndex int `json:"sdpMlineIndex"`
|
||||
}
|
44
internal/yandex/yandex.go
Normal file
44
internal/yandex/yandex.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yandex"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("yandex", func(source string) (core.Producer, error) {
|
||||
u, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
token := query.Get("x_token")
|
||||
|
||||
session, err := yandex.GetSession(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceID := query.Get("device_id")
|
||||
|
||||
if query.Has("snapshot") {
|
||||
rawURL, err := session.GetSnapshotURL(deviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawURL += "/current.jpg?" + query.Get("snapshot") + "#header=Cookie:" + session.GetCookieString(rawURL)
|
||||
return streams.GetProducer(rawURL)
|
||||
}
|
||||
|
||||
room, err := session.WebrtcCreateRoom(deviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return goloomClient(room.ServiceUrl, room.ServiceName, room.RoomId, room.ParticipantId, room.Credentials)
|
||||
})
|
||||
}
|
10
main.go
10
main.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/alsa"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/doorbird"
|
||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||
"github.com/AlexxIT/go2rtc/internal/eseecloud"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
"github.com/AlexxIT/go2rtc/internal/expr"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
@@ -36,11 +38,13 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/webtorrent"
|
||||
"github.com/AlexxIT/go2rtc/internal/wyoming"
|
||||
"github.com/AlexxIT/go2rtc/internal/yandex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.9.9"
|
||||
app.Version = "1.9.10"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
|
||||
@@ -67,6 +71,7 @@ func main() {
|
||||
hass.Init() // hass source, Hass API server
|
||||
onvif.Init() // onvif source, ONVIF API server
|
||||
webtorrent.Init() // webtorrent source, WebTorrent module
|
||||
wyoming.Init()
|
||||
|
||||
// 5. Other sources
|
||||
|
||||
@@ -89,7 +94,10 @@ func main() {
|
||||
gopro.Init() // gopro source
|
||||
doorbird.Init() // doorbird source
|
||||
v4l2.Init() // v4l2 source
|
||||
alsa.Init() // alsa source
|
||||
flussonic.Init()
|
||||
eseecloud.Init()
|
||||
yandex.Init()
|
||||
|
||||
// 6. Helper modules
|
||||
|
||||
|
@@ -13,6 +13,7 @@ Some formats and protocols go2rtc supports exclusively. They have no equivalent
|
||||
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|
||||
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
|
||||
| adts | http,tcp,pipe | http | aac | | `http:` |
|
||||
| alsa | pipe | | | pcm | `alsa:` |
|
||||
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
|
||||
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
|
||||
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
|
||||
|
@@ -53,7 +53,7 @@ func ConfigToCodec(conf []byte) *core.Codec {
|
||||
codec.ClockRate = rd.ReadBits(24)
|
||||
}
|
||||
|
||||
codec.Channels = rd.ReadBits16(4)
|
||||
codec.Channels = rd.ReadBits8(4)
|
||||
|
||||
return codec
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ func ADTSToCodec(b []byte) *core.Codec {
|
||||
objType := rd.ReadBits8(2) + 1 // Profile, the MPEG-4 Audio Object Type minus 1
|
||||
sampleRateIdx := rd.ReadBits8(4) // MPEG-4 Sampling Frequency Index
|
||||
_ = rd.ReadBit() // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
|
||||
channels := rd.ReadBits16(3) // MPEG-4 Channel Configuration
|
||||
channels := rd.ReadBits8(3) // MPEG-4 Channel Configuration
|
||||
|
||||
//_ = rd.ReadBit() // Originality, set to 1 to signal originality of the audio and 0 otherwise
|
||||
//_ = rd.ReadBit() // Home, set to 1 to signal home usage of the audio and 0 otherwise
|
||||
@@ -43,7 +43,7 @@ func ADTSToCodec(b []byte) *core.Codec {
|
||||
wr := bits.NewWriter(nil)
|
||||
wr.WriteBits8(objType, 5)
|
||||
wr.WriteBits8(sampleRateIdx, 4)
|
||||
wr.WriteBits16(channels, 4)
|
||||
wr.WriteBits8(channels, 4)
|
||||
conf := wr.Bytes()
|
||||
|
||||
codec := &core.Codec{
|
||||
|
23
pkg/alsa/README.md
Normal file
23
pkg/alsa/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
## Build
|
||||
|
||||
```shell
|
||||
x86_64-linux-gnu-gcc -w -static asound_arch.c -o asound_amd64
|
||||
i686-linux-gnu-gcc -w -static asound_arch.c -o asound_i386
|
||||
aarch64-linux-gnu-gcc -w -static asound_arch.c -o asound_arm64
|
||||
arm-linux-gnueabihf-gcc -w -static asound_arch.c -o asound_arm
|
||||
mipsel-linux-gnu-gcc -w -static asound_arch.c -o asound_mipsle -D_TIME_BITS=32
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
|
||||
- https://github.com/yobert/alsa
|
||||
- https://github.com/Narsil/alsa-go
|
||||
- https://github.com/alsa-project/alsa-lib
|
||||
- https://github.com/anisse/alsa
|
||||
- https://github.com/tinyalsa/tinyalsa
|
||||
|
||||
**Broken pipe**
|
||||
|
||||
- https://stackoverflow.com/questions/26545139/alsa-cannot-recovery-from-underrun-prepare-failed-broken-pipe
|
||||
- https://klipspringer.avadeaux.net/alsa-broken-pipe-errors/
|
90
pkg/alsa/capture_linux.go
Normal file
90
pkg/alsa/capture_linux.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Capture struct {
|
||||
core.Connection
|
||||
dev *device.Device
|
||||
closed core.Waiter
|
||||
}
|
||||
|
||||
func newCapture(dev *device.Device) (*Capture, error) {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
}
|
||||
return &Capture{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "alsa",
|
||||
Medias: medias,
|
||||
Transport: dev,
|
||||
},
|
||||
dev: dev,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Capture) Start() error {
|
||||
dst := c.Medias[0].Codecs[0]
|
||||
src := &core.Codec{
|
||||
Name: dst.Name,
|
||||
ClockRate: c.dev.GetRateNear(dst.ClockRate),
|
||||
Channels: c.dev.GetChannelsNear(dst.Channels),
|
||||
}
|
||||
|
||||
if err := c.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, src.ClockRate, src.Channels); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcode := transcodeFunc(dst, src)
|
||||
frameBytes := int(pcm.BytesPerFrame(src))
|
||||
|
||||
var ts uint32
|
||||
|
||||
// readBufferSize for 20ms interval
|
||||
readBufferSize := 20 * frameBytes * int(src.ClockRate) / 1000
|
||||
b := make([]byte, readBufferSize)
|
||||
for {
|
||||
n, err := c.dev.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Recv += n
|
||||
|
||||
if len(c.Receivers) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
Timestamp: ts,
|
||||
},
|
||||
Payload: transcode(b[:n]),
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
|
||||
ts += uint32(n / frameBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func transcodeFunc(dst, src *core.Codec) func([]byte) []byte {
|
||||
if dst.ClockRate == src.ClockRate && dst.Channels == src.Channels {
|
||||
return func(b []byte) []byte {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return pcm.Transcode(dst, src)
|
||||
}
|
148
pkg/alsa/device/asound_32bit.go
Normal file
148
pkg/alsa/device/asound_32bit.go
Normal file
@@ -0,0 +1,148 @@
|
||||
//go:build 386 || arm
|
||||
|
||||
package device
|
||||
|
||||
type unsigned_char = byte
|
||||
type signed_int = int32
|
||||
type unsigned_int = uint32
|
||||
type signed_long = int64
|
||||
type unsigned_long = uint64
|
||||
type __u32 = uint32
|
||||
type void__user = uintptr
|
||||
|
||||
const (
|
||||
SNDRV_PCM_STREAM_PLAYBACK = 0
|
||||
SNDRV_PCM_STREAM_CAPTURE = 1
|
||||
|
||||
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
|
||||
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
|
||||
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
|
||||
|
||||
SNDRV_PCM_FORMAT_S8 = 0
|
||||
SNDRV_PCM_FORMAT_U8 = 1
|
||||
SNDRV_PCM_FORMAT_S16_LE = 2
|
||||
SNDRV_PCM_FORMAT_S16_BE = 3
|
||||
SNDRV_PCM_FORMAT_U16_LE = 4
|
||||
SNDRV_PCM_FORMAT_U16_BE = 5
|
||||
SNDRV_PCM_FORMAT_S24_LE = 6
|
||||
SNDRV_PCM_FORMAT_S24_BE = 7
|
||||
SNDRV_PCM_FORMAT_U24_LE = 8
|
||||
SNDRV_PCM_FORMAT_U24_BE = 9
|
||||
SNDRV_PCM_FORMAT_S32_LE = 10
|
||||
SNDRV_PCM_FORMAT_S32_BE = 11
|
||||
SNDRV_PCM_FORMAT_U32_LE = 12
|
||||
SNDRV_PCM_FORMAT_U32_BE = 13
|
||||
SNDRV_PCM_FORMAT_FLOAT_LE = 14
|
||||
SNDRV_PCM_FORMAT_FLOAT_BE = 15
|
||||
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
|
||||
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
|
||||
SNDRV_PCM_FORMAT_MU_LAW = 20
|
||||
SNDRV_PCM_FORMAT_A_LAW = 21
|
||||
SNDRV_PCM_FORMAT_MPEG = 23
|
||||
|
||||
SNDRV_PCM_IOCTL_PVERSION = 0x80044100
|
||||
SNDRV_PCM_IOCTL_INFO = 0x81204101
|
||||
SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110
|
||||
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111
|
||||
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113
|
||||
SNDRV_PCM_IOCTL_PREPARE = 0x00004140
|
||||
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x400c4150
|
||||
SNDRV_PCM_IOCTL_READI_FRAMES = 0x800c4151
|
||||
)
|
||||
|
||||
type snd_pcm_info struct { // size 288
|
||||
device unsigned_int // offset 0, size 4
|
||||
subdevice unsigned_int // offset 4, size 4
|
||||
stream signed_int // offset 8, size 4
|
||||
card signed_int // offset 12, size 4
|
||||
id [64]unsigned_char // offset 16, size 64
|
||||
name [80]unsigned_char // offset 80, size 80
|
||||
subname [32]unsigned_char // offset 160, size 32
|
||||
dev_class signed_int // offset 192, size 4
|
||||
dev_subclass signed_int // offset 196, size 4
|
||||
subdevices_count unsigned_int // offset 200, size 4
|
||||
subdevices_avail unsigned_int // offset 204, size 4
|
||||
pad1 [16]unsigned_char
|
||||
reserved [64]unsigned_char // offset 224, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_uframes_t = unsigned_long
|
||||
type snd_pcm_sframes_t = signed_long
|
||||
|
||||
type snd_xferi struct { // size 12
|
||||
result snd_pcm_sframes_t // offset 0, size 4
|
||||
buf void__user // offset 4, size 4
|
||||
frames snd_pcm_uframes_t // offset 8, size 4
|
||||
}
|
||||
|
||||
const (
|
||||
SNDRV_PCM_HW_PARAM_ACCESS = 0
|
||||
SNDRV_PCM_HW_PARAM_FORMAT = 1
|
||||
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
|
||||
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
|
||||
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
|
||||
|
||||
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
|
||||
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
|
||||
SNDRV_PCM_HW_PARAM_CHANNELS = 10
|
||||
SNDRV_PCM_HW_PARAM_RATE = 11
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
|
||||
SNDRV_PCM_HW_PARAM_PERIODS = 15
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
|
||||
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
|
||||
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
|
||||
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
|
||||
|
||||
SNDRV_MASK_MAX = 256
|
||||
|
||||
SNDRV_PCM_TSTAMP_NONE = 0
|
||||
SNDRV_PCM_TSTAMP_ENABLE = 1
|
||||
)
|
||||
|
||||
type snd_mask struct { // size 32
|
||||
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
|
||||
}
|
||||
|
||||
type snd_interval struct { // size 12
|
||||
min unsigned_int // offset 0, size 4
|
||||
max unsigned_int // offset 4, size 4
|
||||
bit unsigned_int
|
||||
}
|
||||
|
||||
type snd_pcm_hw_params struct { // size 604
|
||||
flags unsigned_int // offset 0, size 4
|
||||
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
|
||||
mres [5]snd_mask // offset 100, size 160
|
||||
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
|
||||
ires [9]snd_interval // offset 404, size 108
|
||||
rmask unsigned_int // offset 512, size 4
|
||||
cmask unsigned_int // offset 516, size 4
|
||||
info unsigned_int // offset 520, size 4
|
||||
msbits unsigned_int // offset 524, size 4
|
||||
rate_num unsigned_int // offset 528, size 4
|
||||
rate_den unsigned_int // offset 532, size 4
|
||||
fifo_size snd_pcm_uframes_t // offset 536, size 4
|
||||
reserved [64]unsigned_char // offset 540, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_sw_params struct { // size 104
|
||||
tstamp_mode signed_int // offset 0, size 4
|
||||
period_step unsigned_int // offset 4, size 4
|
||||
sleep_min unsigned_int // offset 8, size 4
|
||||
avail_min snd_pcm_uframes_t // offset 12, size 4
|
||||
xfer_align snd_pcm_uframes_t // offset 16, size 4
|
||||
start_threshold snd_pcm_uframes_t // offset 20, size 4
|
||||
stop_threshold snd_pcm_uframes_t // offset 24, size 4
|
||||
silence_threshold snd_pcm_uframes_t // offset 28, size 4
|
||||
silence_size snd_pcm_uframes_t // offset 32, size 4
|
||||
boundary snd_pcm_uframes_t // offset 36, size 4
|
||||
proto unsigned_int // offset 40, size 4
|
||||
tstamp_type unsigned_int // offset 44, size 4
|
||||
reserved [56]unsigned_char // offset 48, size 56
|
||||
}
|
148
pkg/alsa/device/asound_64bit.go
Normal file
148
pkg/alsa/device/asound_64bit.go
Normal file
@@ -0,0 +1,148 @@
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package device
|
||||
|
||||
type unsigned_char = byte
|
||||
type signed_int = int32
|
||||
type unsigned_int = uint32
|
||||
type signed_long = int64
|
||||
type unsigned_long = uint64
|
||||
type __u32 = uint32
|
||||
type void__user = uintptr
|
||||
|
||||
const (
|
||||
SNDRV_PCM_STREAM_PLAYBACK = 0
|
||||
SNDRV_PCM_STREAM_CAPTURE = 1
|
||||
|
||||
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
|
||||
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
|
||||
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
|
||||
|
||||
SNDRV_PCM_FORMAT_S8 = 0
|
||||
SNDRV_PCM_FORMAT_U8 = 1
|
||||
SNDRV_PCM_FORMAT_S16_LE = 2
|
||||
SNDRV_PCM_FORMAT_S16_BE = 3
|
||||
SNDRV_PCM_FORMAT_U16_LE = 4
|
||||
SNDRV_PCM_FORMAT_U16_BE = 5
|
||||
SNDRV_PCM_FORMAT_S24_LE = 6
|
||||
SNDRV_PCM_FORMAT_S24_BE = 7
|
||||
SNDRV_PCM_FORMAT_U24_LE = 8
|
||||
SNDRV_PCM_FORMAT_U24_BE = 9
|
||||
SNDRV_PCM_FORMAT_S32_LE = 10
|
||||
SNDRV_PCM_FORMAT_S32_BE = 11
|
||||
SNDRV_PCM_FORMAT_U32_LE = 12
|
||||
SNDRV_PCM_FORMAT_U32_BE = 13
|
||||
SNDRV_PCM_FORMAT_FLOAT_LE = 14
|
||||
SNDRV_PCM_FORMAT_FLOAT_BE = 15
|
||||
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
|
||||
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
|
||||
SNDRV_PCM_FORMAT_MU_LAW = 20
|
||||
SNDRV_PCM_FORMAT_A_LAW = 21
|
||||
SNDRV_PCM_FORMAT_MPEG = 23
|
||||
|
||||
SNDRV_PCM_IOCTL_PVERSION = 0x80044100
|
||||
SNDRV_PCM_IOCTL_INFO = 0x81204101
|
||||
SNDRV_PCM_IOCTL_HW_REFINE = 0xc2604110
|
||||
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc2604111
|
||||
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0884113
|
||||
SNDRV_PCM_IOCTL_PREPARE = 0x00004140
|
||||
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x40184150
|
||||
SNDRV_PCM_IOCTL_READI_FRAMES = 0x80184151
|
||||
)
|
||||
|
||||
type snd_pcm_info struct { // size 288
|
||||
device unsigned_int // offset 0, size 4
|
||||
subdevice unsigned_int // offset 4, size 4
|
||||
stream signed_int // offset 8, size 4
|
||||
card signed_int // offset 12, size 4
|
||||
id [64]unsigned_char // offset 16, size 64
|
||||
name [80]unsigned_char // offset 80, size 80
|
||||
subname [32]unsigned_char // offset 160, size 32
|
||||
dev_class signed_int // offset 192, size 4
|
||||
dev_subclass signed_int // offset 196, size 4
|
||||
subdevices_count unsigned_int // offset 200, size 4
|
||||
subdevices_avail unsigned_int // offset 204, size 4
|
||||
pad1 [16]unsigned_char
|
||||
reserved [64]unsigned_char // offset 224, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_uframes_t = unsigned_long
|
||||
type snd_pcm_sframes_t = signed_long
|
||||
|
||||
type snd_xferi struct { // size 24
|
||||
result snd_pcm_sframes_t // offset 0, size 8
|
||||
buf void__user // offset 8, size 8
|
||||
frames snd_pcm_uframes_t // offset 16, size 8
|
||||
}
|
||||
|
||||
const (
|
||||
SNDRV_PCM_HW_PARAM_ACCESS = 0
|
||||
SNDRV_PCM_HW_PARAM_FORMAT = 1
|
||||
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
|
||||
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
|
||||
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
|
||||
|
||||
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
|
||||
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
|
||||
SNDRV_PCM_HW_PARAM_CHANNELS = 10
|
||||
SNDRV_PCM_HW_PARAM_RATE = 11
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
|
||||
SNDRV_PCM_HW_PARAM_PERIODS = 15
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
|
||||
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
|
||||
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
|
||||
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
|
||||
|
||||
SNDRV_MASK_MAX = 256
|
||||
|
||||
SNDRV_PCM_TSTAMP_NONE = 0
|
||||
SNDRV_PCM_TSTAMP_ENABLE = 1
|
||||
)
|
||||
|
||||
type snd_mask struct { // size 32
|
||||
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
|
||||
}
|
||||
|
||||
type snd_interval struct { // size 12
|
||||
min unsigned_int // offset 0, size 4
|
||||
max unsigned_int // offset 4, size 4
|
||||
bit unsigned_int
|
||||
}
|
||||
|
||||
type snd_pcm_hw_params struct { // size 608
|
||||
flags unsigned_int // offset 0, size 4
|
||||
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
|
||||
mres [5]snd_mask // offset 100, size 160
|
||||
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
|
||||
ires [9]snd_interval // offset 404, size 108
|
||||
rmask unsigned_int // offset 512, size 4
|
||||
cmask unsigned_int // offset 516, size 4
|
||||
info unsigned_int // offset 520, size 4
|
||||
msbits unsigned_int // offset 524, size 4
|
||||
rate_num unsigned_int // offset 528, size 4
|
||||
rate_den unsigned_int // offset 532, size 4
|
||||
fifo_size snd_pcm_uframes_t // offset 536, size 8
|
||||
reserved [64]unsigned_char // offset 544, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_sw_params struct { // size 136
|
||||
tstamp_mode signed_int // offset 0, size 4
|
||||
period_step unsigned_int // offset 4, size 4
|
||||
sleep_min unsigned_int // offset 8, size 4
|
||||
avail_min snd_pcm_uframes_t // offset 16, size 8
|
||||
xfer_align snd_pcm_uframes_t // offset 24, size 8
|
||||
start_threshold snd_pcm_uframes_t // offset 32, size 8
|
||||
stop_threshold snd_pcm_uframes_t // offset 40, size 8
|
||||
silence_threshold snd_pcm_uframes_t // offset 48, size 8
|
||||
silence_size snd_pcm_uframes_t // offset 56, size 8
|
||||
boundary snd_pcm_uframes_t // offset 64, size 8
|
||||
proto unsigned_int // offset 72, size 4
|
||||
tstamp_type unsigned_int // offset 76, size 4
|
||||
reserved [56]unsigned_char // offset 80, size 56
|
||||
}
|
163
pkg/alsa/device/asound_arch.c
Normal file
163
pkg/alsa/device/asound_arch.c
Normal file
@@ -0,0 +1,163 @@
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sound/asound.h>
|
||||
|
||||
#define print_line(text) printf("%s\n", text)
|
||||
#define print_hex_const(name) printf("\t%s = 0x%08lx\n", #name, name)
|
||||
#define print_int_const(con) printf("\t%s = %d\n", #con, con)
|
||||
|
||||
#define print_struct_header(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str))
|
||||
#define print_struct_member(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem))
|
||||
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
|
||||
int main() {
|
||||
print_line("package device\n");
|
||||
|
||||
print_line("type unsigned_char = byte");
|
||||
print_line("type signed_int = int32");
|
||||
print_line("type unsigned_int = uint32");
|
||||
print_line("type signed_long = int64");
|
||||
print_line("type unsigned_long = uint64");
|
||||
print_line("type __u32 = uint32");
|
||||
print_line("type void__user = uintptr\n");
|
||||
|
||||
print_line("const (");
|
||||
print_int_const(SNDRV_PCM_STREAM_PLAYBACK);
|
||||
print_int_const(SNDRV_PCM_STREAM_CAPTURE);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
|
||||
print_int_const(SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED);
|
||||
print_int_const(SNDRV_PCM_ACCESS_MMAP_COMPLEX);
|
||||
print_int_const(SNDRV_PCM_ACCESS_RW_INTERLEAVED);
|
||||
print_int_const(SNDRV_PCM_ACCESS_RW_NONINTERLEAVED);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_FORMAT_S8);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U8);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S16_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S16_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U16_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U16_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S24_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S24_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U24_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U24_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S32_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S32_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U32_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U32_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT64_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT64_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_MU_LAW);
|
||||
print_int_const(SNDRV_PCM_FORMAT_A_LAW);
|
||||
print_int_const(SNDRV_PCM_FORMAT_MPEG);
|
||||
print_line("");
|
||||
print_hex_const(SNDRV_PCM_IOCTL_PVERSION); // A 0x00
|
||||
print_hex_const(SNDRV_PCM_IOCTL_INFO); // A 0x01
|
||||
print_hex_const(SNDRV_PCM_IOCTL_HW_REFINE); // A 0x10
|
||||
print_hex_const(SNDRV_PCM_IOCTL_HW_PARAMS); // A 0x11
|
||||
print_hex_const(SNDRV_PCM_IOCTL_SW_PARAMS); // A 0x13
|
||||
print_hex_const(SNDRV_PCM_IOCTL_PREPARE); // A 0x40
|
||||
print_hex_const(SNDRV_PCM_IOCTL_WRITEI_FRAMES); // A 0x50
|
||||
print_hex_const(SNDRV_PCM_IOCTL_READI_FRAMES); // A 0x51
|
||||
print_line(")\n");
|
||||
|
||||
print_struct_header(snd_pcm_info);
|
||||
print_struct_member(snd_pcm_info, device, "unsigned_int");
|
||||
print_struct_member(snd_pcm_info, subdevice, "unsigned_int");
|
||||
print_struct_member(snd_pcm_info, stream, "signed_int");
|
||||
print_struct_member(snd_pcm_info, card, "signed_int");
|
||||
print_struct_member(snd_pcm_info, id, "[64]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, name, "[80]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, subname, "[32]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, dev_class, "signed_int");
|
||||
print_struct_member(snd_pcm_info, dev_subclass, "signed_int");
|
||||
print_struct_member(snd_pcm_info, subdevices_count, "unsigned_int");
|
||||
print_struct_member(snd_pcm_info, subdevices_avail, "unsigned_int");
|
||||
print_line("\tpad1 [16]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, reserved, "[64]unsigned_char");
|
||||
print_line("}\n");
|
||||
|
||||
print_line("type snd_pcm_uframes_t = unsigned_long");
|
||||
print_line("type snd_pcm_sframes_t = signed_long\n");
|
||||
|
||||
print_struct_header(snd_xferi);
|
||||
print_struct_member(snd_xferi, result, "snd_pcm_sframes_t");
|
||||
print_struct_member(snd_xferi, buf, "void__user");
|
||||
print_struct_member(snd_xferi, frames, "snd_pcm_uframes_t");
|
||||
print_line("}\n");
|
||||
|
||||
print_line("const (");
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_ACCESS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FORMAT);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_SUBFORMAT);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FIRST_MASK);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_LAST_MASK);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_SAMPLE_BITS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FRAME_BITS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_CHANNELS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_RATE);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_TIME);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_BYTES);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIODS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_TIME);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_SIZE);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_BYTES);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_TICK_TIME);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FIRST_INTERVAL);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_LAST_INTERVAL);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_MASK_MAX);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_TSTAMP_NONE);
|
||||
print_int_const(SNDRV_PCM_TSTAMP_ENABLE);
|
||||
print_line(")\n");
|
||||
|
||||
print_struct_header(snd_mask);
|
||||
print_struct_member(snd_mask, bits, "[(SNDRV_MASK_MAX+31)/32]__u32");
|
||||
print_line("}\n");
|
||||
|
||||
print_struct_header(snd_interval);
|
||||
print_struct_member(snd_interval, min, "unsigned_int");
|
||||
print_struct_member(snd_interval, max, "unsigned_int");
|
||||
print_line("\tbit unsigned_int");
|
||||
print_line("}\n");
|
||||
|
||||
print_struct_header(snd_pcm_hw_params);
|
||||
print_struct_member(snd_pcm_hw_params, flags, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, masks, "[SNDRV_PCM_HW_PARAM_LAST_MASK-SNDRV_PCM_HW_PARAM_FIRST_MASK+1]snd_mask");
|
||||
print_struct_member(snd_pcm_hw_params, mres, "[5]snd_mask");
|
||||
print_struct_member(snd_pcm_hw_params, intervals, "[SNDRV_PCM_HW_PARAM_LAST_INTERVAL-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL+1]snd_interval");
|
||||
print_struct_member(snd_pcm_hw_params, ires, "[9]snd_interval");
|
||||
print_struct_member(snd_pcm_hw_params, rmask, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, cmask, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, info, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, msbits, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, rate_num, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, rate_den, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, fifo_size, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_hw_params, reserved, "[64]unsigned_char");
|
||||
print_line("}\n");
|
||||
|
||||
print_struct_header(snd_pcm_sw_params);
|
||||
print_struct_member(snd_pcm_sw_params, tstamp_mode, "signed_int");
|
||||
print_struct_member(snd_pcm_sw_params, period_step, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, sleep_min, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, avail_min, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, xfer_align, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, start_threshold, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, stop_threshold, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, silence_threshold, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, silence_size, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, boundary, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, proto, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, tstamp_type, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, reserved, "[56]unsigned_char");
|
||||
print_line("}\n");
|
||||
|
||||
return 0;
|
||||
}
|
146
pkg/alsa/device/asound_mipsle.go
Normal file
146
pkg/alsa/device/asound_mipsle.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package device
|
||||
|
||||
type unsigned_char = byte
|
||||
type signed_int = int32
|
||||
type unsigned_int = uint32
|
||||
type signed_long = int64
|
||||
type unsigned_long = uint64
|
||||
type __u32 = uint32
|
||||
type void__user = uintptr
|
||||
|
||||
const (
|
||||
SNDRV_PCM_STREAM_PLAYBACK = 0
|
||||
SNDRV_PCM_STREAM_CAPTURE = 1
|
||||
|
||||
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
|
||||
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
|
||||
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
|
||||
|
||||
SNDRV_PCM_FORMAT_S8 = 0
|
||||
SNDRV_PCM_FORMAT_U8 = 1
|
||||
SNDRV_PCM_FORMAT_S16_LE = 2
|
||||
SNDRV_PCM_FORMAT_S16_BE = 3
|
||||
SNDRV_PCM_FORMAT_U16_LE = 4
|
||||
SNDRV_PCM_FORMAT_U16_BE = 5
|
||||
SNDRV_PCM_FORMAT_S24_LE = 6
|
||||
SNDRV_PCM_FORMAT_S24_BE = 7
|
||||
SNDRV_PCM_FORMAT_U24_LE = 8
|
||||
SNDRV_PCM_FORMAT_U24_BE = 9
|
||||
SNDRV_PCM_FORMAT_S32_LE = 10
|
||||
SNDRV_PCM_FORMAT_S32_BE = 11
|
||||
SNDRV_PCM_FORMAT_U32_LE = 12
|
||||
SNDRV_PCM_FORMAT_U32_BE = 13
|
||||
SNDRV_PCM_FORMAT_FLOAT_LE = 14
|
||||
SNDRV_PCM_FORMAT_FLOAT_BE = 15
|
||||
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
|
||||
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
|
||||
SNDRV_PCM_FORMAT_MU_LAW = 20
|
||||
SNDRV_PCM_FORMAT_A_LAW = 21
|
||||
SNDRV_PCM_FORMAT_MPEG = 23
|
||||
|
||||
SNDRV_PCM_IOCTL_PVERSION = 0x40044100
|
||||
SNDRV_PCM_IOCTL_INFO = 0x41204101
|
||||
SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110
|
||||
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111
|
||||
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113
|
||||
SNDRV_PCM_IOCTL_PREPARE = 0x20004140
|
||||
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x800c4150
|
||||
SNDRV_PCM_IOCTL_READI_FRAMES = 0x400c4151
|
||||
)
|
||||
|
||||
type snd_pcm_info struct { // size 288
|
||||
device unsigned_int // offset 0, size 4
|
||||
subdevice unsigned_int // offset 4, size 4
|
||||
stream signed_int // offset 8, size 4
|
||||
card signed_int // offset 12, size 4
|
||||
id [64]unsigned_char // offset 16, size 64
|
||||
name [80]unsigned_char // offset 80, size 80
|
||||
subname [32]unsigned_char // offset 160, size 32
|
||||
dev_class signed_int // offset 192, size 4
|
||||
dev_subclass signed_int // offset 196, size 4
|
||||
subdevices_count unsigned_int // offset 200, size 4
|
||||
subdevices_avail unsigned_int // offset 204, size 4
|
||||
pad1 [16]unsigned_char
|
||||
reserved [64]unsigned_char // offset 224, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_uframes_t = unsigned_long
|
||||
type snd_pcm_sframes_t = signed_long
|
||||
|
||||
type snd_xferi struct { // size 12
|
||||
result snd_pcm_sframes_t // offset 0, size 4
|
||||
buf void__user // offset 4, size 4
|
||||
frames snd_pcm_uframes_t // offset 8, size 4
|
||||
}
|
||||
|
||||
const (
|
||||
SNDRV_PCM_HW_PARAM_ACCESS = 0
|
||||
SNDRV_PCM_HW_PARAM_FORMAT = 1
|
||||
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
|
||||
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
|
||||
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
|
||||
|
||||
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
|
||||
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
|
||||
SNDRV_PCM_HW_PARAM_CHANNELS = 10
|
||||
SNDRV_PCM_HW_PARAM_RATE = 11
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
|
||||
SNDRV_PCM_HW_PARAM_PERIODS = 15
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
|
||||
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
|
||||
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
|
||||
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
|
||||
|
||||
SNDRV_MASK_MAX = 256
|
||||
|
||||
SNDRV_PCM_TSTAMP_NONE = 0
|
||||
SNDRV_PCM_TSTAMP_ENABLE = 1
|
||||
)
|
||||
|
||||
type snd_mask struct { // size 32
|
||||
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
|
||||
}
|
||||
|
||||
type snd_interval struct { // size 12
|
||||
min unsigned_int // offset 0, size 4
|
||||
max unsigned_int // offset 4, size 4
|
||||
bit unsigned_int
|
||||
}
|
||||
|
||||
type snd_pcm_hw_params struct { // size 604
|
||||
flags unsigned_int // offset 0, size 4
|
||||
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
|
||||
mres [5]snd_mask // offset 100, size 160
|
||||
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
|
||||
ires [9]snd_interval // offset 404, size 108
|
||||
rmask unsigned_int // offset 512, size 4
|
||||
cmask unsigned_int // offset 516, size 4
|
||||
info unsigned_int // offset 520, size 4
|
||||
msbits unsigned_int // offset 524, size 4
|
||||
rate_num unsigned_int // offset 528, size 4
|
||||
rate_den unsigned_int // offset 532, size 4
|
||||
fifo_size snd_pcm_uframes_t // offset 536, size 4
|
||||
reserved [64]unsigned_char // offset 540, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_sw_params struct { // size 104
|
||||
tstamp_mode signed_int // offset 0, size 4
|
||||
period_step unsigned_int // offset 4, size 4
|
||||
sleep_min unsigned_int // offset 8, size 4
|
||||
avail_min snd_pcm_uframes_t // offset 12, size 4
|
||||
xfer_align snd_pcm_uframes_t // offset 16, size 4
|
||||
start_threshold snd_pcm_uframes_t // offset 20, size 4
|
||||
stop_threshold snd_pcm_uframes_t // offset 24, size 4
|
||||
silence_threshold snd_pcm_uframes_t // offset 28, size 4
|
||||
silence_size snd_pcm_uframes_t // offset 32, size 4
|
||||
boundary snd_pcm_uframes_t // offset 36, size 4
|
||||
proto unsigned_int // offset 40, size 4
|
||||
tstamp_type unsigned_int // offset 44, size 4
|
||||
reserved [56]unsigned_char // offset 48, size 56
|
||||
}
|
231
pkg/alsa/device/device_linux.go
Normal file
231
pkg/alsa/device/device_linux.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
fd uintptr
|
||||
path string
|
||||
|
||||
hwparams snd_pcm_hw_params
|
||||
frameBytes int // sample size * channels
|
||||
}
|
||||
|
||||
func Open(path string) (*Device, error) {
|
||||
// important to use nonblock because can get lock
|
||||
fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// important to remove nonblock because better to handle reads and writes
|
||||
if err = syscall.SetNonblock(fd, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := &Device{fd: uintptr(fd), path: path}
|
||||
d.init()
|
||||
|
||||
// load all supported formats, channels, rates, etc.
|
||||
if err = ioctl(d.fd, SNDRV_PCM_IOCTL_HW_REFINE, &d.hwparams); err != nil {
|
||||
_ = d.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.setMask(SNDRV_PCM_HW_PARAM_ACCESS, SNDRV_PCM_ACCESS_RW_INTERLEAVED)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *Device) Close() error {
|
||||
return syscall.Close(int(d.fd))
|
||||
}
|
||||
|
||||
func (d *Device) IsCapture() bool {
|
||||
// path: /dev/snd/pcmC0D0c, where p - playback, c - capture
|
||||
return d.path[len(d.path)-1] == 'c'
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Card int
|
||||
Device int
|
||||
SubDevice int
|
||||
Stream int
|
||||
ID string
|
||||
Name string
|
||||
SubName string
|
||||
}
|
||||
|
||||
func (d *Device) Info() (*Info, error) {
|
||||
var info snd_pcm_info
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_INFO, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Info{
|
||||
Card: int(info.card),
|
||||
Device: int(info.device),
|
||||
SubDevice: int(info.subdevice),
|
||||
Stream: int(info.stream),
|
||||
ID: str(info.id[:]),
|
||||
Name: str(info.name[:]),
|
||||
SubName: str(info.subname[:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Device) CheckFormat(format byte) bool {
|
||||
return d.checkMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))
|
||||
}
|
||||
|
||||
func (d *Device) ListFormats() (formats []byte) {
|
||||
for i := byte(0); i <= 28; i++ {
|
||||
if d.CheckFormat(i) {
|
||||
formats = append(formats, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) RangeRates() (uint32, uint32) {
|
||||
return d.getInterval(SNDRV_PCM_HW_PARAM_RATE)
|
||||
}
|
||||
|
||||
func (d *Device) RangeChannels() (byte, byte) {
|
||||
minCh, maxCh := d.getInterval(SNDRV_PCM_HW_PARAM_CHANNELS)
|
||||
return byte(minCh), byte(maxCh)
|
||||
}
|
||||
|
||||
func (d *Device) GetRateNear(rate uint32) uint32 {
|
||||
r1, r2 := d.RangeRates()
|
||||
if rate < r1 {
|
||||
return r1
|
||||
}
|
||||
if rate > r2 {
|
||||
return r2
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
func (d *Device) GetChannelsNear(channels byte) byte {
|
||||
c1, c2 := d.RangeChannels()
|
||||
if channels < c1 {
|
||||
return c1
|
||||
}
|
||||
if channels > c2 {
|
||||
return c2
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
const bufferSize = 4096
|
||||
|
||||
func (d *Device) SetHWParams(format byte, rate uint32, channels byte) error {
|
||||
d.setInterval(SNDRV_PCM_HW_PARAM_CHANNELS, uint32(channels))
|
||||
d.setInterval(SNDRV_PCM_HW_PARAM_RATE, rate)
|
||||
d.setMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))
|
||||
//d.setMask(SNDRV_PCM_HW_PARAM_SUBFORMAT, 0)
|
||||
|
||||
// important for smooth playback
|
||||
d.setInterval(SNDRV_PCM_HW_PARAM_BUFFER_SIZE, bufferSize)
|
||||
//d.setInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 2000)
|
||||
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_HW_PARAMS, &d.hwparams); err != nil {
|
||||
return fmt.Errorf("[alsa] set hw_params: %w", err)
|
||||
}
|
||||
|
||||
_, i := d.getInterval(SNDRV_PCM_HW_PARAM_FRAME_BITS)
|
||||
d.frameBytes = int(i / 8)
|
||||
|
||||
_, periods := d.getInterval(SNDRV_PCM_HW_PARAM_PERIODS)
|
||||
_, periodSize := d.getInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE)
|
||||
threshold := snd_pcm_uframes_t(periods * periodSize) // same as bufferSize
|
||||
|
||||
swparams := snd_pcm_sw_params{
|
||||
//tstamp_mode: SNDRV_PCM_TSTAMP_ENABLE,
|
||||
period_step: 1,
|
||||
avail_min: 1, // start as soon as possible
|
||||
stop_threshold: threshold,
|
||||
}
|
||||
|
||||
if d.IsCapture() {
|
||||
swparams.start_threshold = 1
|
||||
} else {
|
||||
swparams.start_threshold = threshold
|
||||
}
|
||||
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_SW_PARAMS, &swparams); err != nil {
|
||||
return fmt.Errorf("[alsa] set sw_params: %w", err)
|
||||
}
|
||||
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil); err != nil {
|
||||
return fmt.Errorf("[alsa] prepare: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) Write(b []byte) (n int, err error) {
|
||||
xfer := &snd_xferi{
|
||||
buf: uintptr(unsafe.Pointer(&b[0])),
|
||||
frames: snd_pcm_uframes_t(len(b) / d.frameBytes),
|
||||
}
|
||||
err = ioctl(d.fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, xfer)
|
||||
if err == syscall.EPIPE {
|
||||
// auto handle underrun state
|
||||
// https://stackoverflow.com/questions/59396728/how-to-properly-handle-xrun-in-alsa-programming-when-playing-audio-with-snd-pcm
|
||||
err = ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil)
|
||||
}
|
||||
n = int(xfer.result) * d.frameBytes
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) Read(b []byte) (n int, err error) {
|
||||
xfer := &snd_xferi{
|
||||
buf: uintptr(unsafe.Pointer(&b[0])),
|
||||
frames: snd_pcm_uframes_t(len(b) / d.frameBytes),
|
||||
}
|
||||
err = ioctl(d.fd, SNDRV_PCM_IOCTL_READI_FRAMES, xfer)
|
||||
n = int(xfer.result) * d.frameBytes
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) init() {
|
||||
for i := range d.hwparams.masks {
|
||||
d.hwparams.masks[i].bits[0] = 0xFFFFFFFF
|
||||
d.hwparams.masks[i].bits[1] = 0xFFFFFFFF
|
||||
}
|
||||
for i := range d.hwparams.intervals {
|
||||
d.hwparams.intervals[i].max = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
d.hwparams.rmask = 0xFFFFFFFF
|
||||
d.hwparams.cmask = 0
|
||||
d.hwparams.info = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
func (d *Device) setInterval(param, val uint32) {
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max = val
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].bit = 0b0100 // integer
|
||||
}
|
||||
|
||||
func (d *Device) setIntervalMin(param, val uint32) {
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val
|
||||
}
|
||||
|
||||
func (d *Device) getInterval(param uint32) (uint32, uint32) {
|
||||
return d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max
|
||||
}
|
||||
|
||||
func (d *Device) setMask(mask, val uint32) {
|
||||
d.hwparams.masks[mask].bits[0] = 0
|
||||
d.hwparams.masks[mask].bits[1] = 0
|
||||
d.hwparams.masks[mask].bits[val>>5] = 1 << (val & 0x1F)
|
||||
}
|
||||
|
||||
func (d *Device) checkMask(mask, val uint32) bool {
|
||||
return d.hwparams.masks[mask].bits[val>>5]&(1<<(val&0x1F)) > 0
|
||||
}
|
26
pkg/alsa/device/ioctl_linux.go
Normal file
26
pkg/alsa/device/ioctl_linux.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func ioctl(fd, req uintptr, arg any) error {
|
||||
var ptr uintptr
|
||||
if arg != nil {
|
||||
ptr = reflect.ValueOf(arg).Pointer()
|
||||
}
|
||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, req, ptr)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func str(b []byte) string {
|
||||
if i := bytes.IndexByte(b, 0); i >= 0 {
|
||||
return string(b[:i])
|
||||
}
|
||||
return string(b)
|
||||
}
|
44
pkg/alsa/open_linux.go
Normal file
44
pkg/alsa/open_linux.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Open(rawURL string) (core.Producer, error) {
|
||||
// Example (ffmpeg source compatible):
|
||||
// alsa:device?audio=/dev/snd/pcmC0D0p
|
||||
// TODO: ?audio=default
|
||||
// TODO: ?audio=hw:0,0
|
||||
// TODO: &sample_rate=48000&channels=2
|
||||
// TODO: &backchannel=1
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := u.Query().Get("audio")
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dev.CheckFormat(device.SNDRV_PCM_FORMAT_S16_LE) {
|
||||
_ = dev.Close()
|
||||
return nil, errors.New("alsa: format S16LE not supported")
|
||||
}
|
||||
|
||||
switch path[len(path)-1] {
|
||||
case 'p': // playback
|
||||
return newPlayback(dev)
|
||||
case 'c': // capture
|
||||
return newCapture(dev)
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
return nil, fmt.Errorf("alsa: unknown path: %s", path)
|
||||
}
|
84
pkg/alsa/playback_linux.go
Normal file
84
pkg/alsa/playback_linux.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Playback struct {
|
||||
core.Connection
|
||||
dev *device.Device
|
||||
closed core.Waiter
|
||||
}
|
||||
|
||||
func newPlayback(dev *device.Device) (*Playback, error) {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCML}, // support ffmpeg producer (auto transcode)
|
||||
{Name: core.CodecPCMA, ClockRate: 8000}, // support webrtc producer
|
||||
},
|
||||
},
|
||||
}
|
||||
return &Playback{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "alsa",
|
||||
Medias: medias,
|
||||
Transport: dev,
|
||||
},
|
||||
dev: dev,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Playback) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (p *Playback) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
src := track.Codec
|
||||
dst := &core.Codec{
|
||||
Name: core.CodecPCML,
|
||||
ClockRate: p.dev.GetRateNear(src.ClockRate),
|
||||
Channels: p.dev.GetChannelsNear(src.Channels),
|
||||
}
|
||||
sender := core.NewSender(media, dst)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
if n, err := p.dev.Write(pkt.Payload); err == nil {
|
||||
p.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if sender.Handler = pcm.TranscodeHandler(dst, src, sender.Handler); sender.Handler == nil {
|
||||
return fmt.Errorf("alsa: can't convert %s to %s", src, dst)
|
||||
}
|
||||
|
||||
// typical card support:
|
||||
// - Formats: S16_LE, S32_LE
|
||||
// - ClockRates: 8000 - 192000
|
||||
// - Channels: 2 - 10
|
||||
err := p.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, dst.ClockRate, byte(dst.Channels))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
p.Senders = append(p.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Playback) Start() (err error) {
|
||||
return p.closed.Wait()
|
||||
}
|
||||
|
||||
func (p *Playback) Stop() error {
|
||||
p.closed.Done(nil)
|
||||
return p.Connection.Stop()
|
||||
}
|
@@ -13,7 +13,7 @@ import (
|
||||
type Codec struct {
|
||||
Name string // H264, PCMU, PCMA, opus...
|
||||
ClockRate uint32 // 90000, 8000, 16000...
|
||||
Channels uint16 // 0, 1, 2
|
||||
Channels uint8 // 0, 1, 2
|
||||
FmtpLine string
|
||||
PayloadType uint8
|
||||
}
|
||||
@@ -249,3 +249,36 @@ func DecodeH264(fmtp string) (profile string, level byte) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseCodecString(s string) *Codec {
|
||||
var codec Codec
|
||||
|
||||
ss := strings.Split(s, "/")
|
||||
switch strings.ToLower(ss[0]) {
|
||||
case "pcm_s16be", "s16be", "pcm":
|
||||
codec.Name = CodecPCM
|
||||
case "pcm_s16le", "s16le", "pcml":
|
||||
codec.Name = CodecPCML
|
||||
case "pcm_alaw", "alaw", "pcma":
|
||||
codec.Name = CodecPCMA
|
||||
case "pcm_mulaw", "mulaw", "pcmu":
|
||||
codec.Name = CodecPCMU
|
||||
case "aac", "mpeg4-generic":
|
||||
codec.Name = CodecAAC
|
||||
case "opus":
|
||||
codec.Name = CodecOpus
|
||||
case "flac":
|
||||
codec.Name = CodecFLAC
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(ss) >= 2 {
|
||||
codec.ClockRate = uint32(Atoi(ss[1]))
|
||||
}
|
||||
if len(ss) >= 3 {
|
||||
codec.Channels = uint8(Atoi(ss[1]))
|
||||
}
|
||||
|
||||
return &codec
|
||||
}
|
||||
|
@@ -118,3 +118,17 @@ func TestName(t *testing.T) {
|
||||
// stage3
|
||||
_ = prod2.Stop()
|
||||
}
|
||||
|
||||
func TestStripUserinfo(t *testing.T) {
|
||||
s := `streams:
|
||||
test:
|
||||
- ffmpeg:rtsp://username:password@10.1.2.3:554/stream1
|
||||
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
|
||||
`
|
||||
s = StripUserinfo(s)
|
||||
require.Equal(t, `streams:
|
||||
test:
|
||||
- ffmpeg:rtsp://***@10.1.2.3:554/stream1
|
||||
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
|
||||
`, s)
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -77,3 +78,14 @@ func Caller() string {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
return file + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
const (
|
||||
unreserved = `A-Za-z0-9-._~`
|
||||
subdelims = `!$&'()*+,;=`
|
||||
userinfo = unreserved + subdelims + `%:`
|
||||
)
|
||||
|
||||
func StripUserinfo(s string) string {
|
||||
sanitizer := regexp.MustCompile(`://[` + userinfo + `]+@`)
|
||||
return sanitizer.ReplaceAllString(s, `://***@`)
|
||||
}
|
||||
|
@@ -139,7 +139,7 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, uint16(codec.Channels), codec.FmtpLine)
|
||||
|
||||
if media.Direction != "" {
|
||||
md.WithPropertyAttribute(media.Direction)
|
||||
|
@@ -97,8 +97,8 @@ func NewSender(media *Media, codec *Codec) *Sender {
|
||||
buf: buf,
|
||||
}
|
||||
s.Input = func(packet *Packet) {
|
||||
// writing to nil chan - OK, writing to closed chan - panic
|
||||
s.mu.Lock()
|
||||
// unblock write to nil chan - OK, write to closed chan - panic
|
||||
select {
|
||||
case s.buf <- packet:
|
||||
s.Bytes += len(packet.Payload)
|
||||
@@ -139,13 +139,13 @@ func (s *Sender) Start() {
|
||||
}
|
||||
s.done = make(chan struct{})
|
||||
|
||||
go func() {
|
||||
// for range on nil chan is OK
|
||||
for packet := range s.buf {
|
||||
// pass buf directly so that it's impossible for buf to be nil
|
||||
go func(buf chan *Packet) {
|
||||
for packet := range buf {
|
||||
s.Output(packet)
|
||||
}
|
||||
close(s.done)
|
||||
}()
|
||||
}(s.buf)
|
||||
}
|
||||
|
||||
func (s *Sender) Wait() {
|
||||
|
180
pkg/eseecloud/eseecloud.go
Normal file
180
pkg/eseecloud/eseecloud.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package eseecloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.Connection
|
||||
rd *core.ReadBuffer
|
||||
|
||||
videoPT, audioPT uint8
|
||||
}
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
rawURL, _ = strings.CutPrefix(rawURL, "eseecloud")
|
||||
res, err := http.Get("http" + rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod, err := Open(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info, ok := prod.(core.Info); ok {
|
||||
info.SetProtocol("http")
|
||||
info.SetURL(rawURL)
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func Open(r io.Reader) (core.Producer, error) {
|
||||
prod := &Producer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "eseecloud",
|
||||
Transport: r,
|
||||
},
|
||||
rd: core.NewReadBuffer(r),
|
||||
}
|
||||
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func (p *Producer) probe() error {
|
||||
b, err := p.rd.Peek(1024)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := bytes.Index(b, []byte("\r\n\r\n"))
|
||||
if i == -1 {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
b = make([]byte, i+4)
|
||||
_, _ = p.rd.Read(b)
|
||||
|
||||
re := regexp.MustCompile(`m=(video|audio) (\d+) (\w+)/(\d+)\S*`)
|
||||
for _, item := range re.FindAllStringSubmatch(string(b), 2) {
|
||||
p.SDP += item[0] + "\n"
|
||||
|
||||
switch item[3] {
|
||||
case "H264", "H265":
|
||||
p.Medias = append(p.Medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: item[3],
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
},
|
||||
},
|
||||
})
|
||||
p.videoPT = byte(core.Atoi(item[2]))
|
||||
|
||||
case "G711":
|
||||
p.Medias = append(p.Medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: core.CodecPCMA,
|
||||
ClockRate: 8000,
|
||||
},
|
||||
},
|
||||
})
|
||||
p.audioPT = byte(core.Atoi(item[2]))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Producer) Start() error {
|
||||
receivers := make(map[uint8]*core.Receiver)
|
||||
|
||||
for _, receiver := range p.Receivers {
|
||||
switch receiver.Codec.Kind() {
|
||||
case core.KindVideo:
|
||||
receivers[p.videoPT] = receiver
|
||||
case core.KindAudio:
|
||||
receivers[p.audioPT] = receiver
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
pkt, err := p.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if recv := receivers[pkt.PayloadType]; recv != nil {
|
||||
switch recv.Codec.Name {
|
||||
case core.CodecH264, core.CodecH265:
|
||||
// timestamp = seconds x 1000000
|
||||
pkt = &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Timestamp: uint32(uint64(pkt.Timestamp) * 90000 / 1000000),
|
||||
},
|
||||
Payload: annexb.EncodeToAVCC(pkt.Payload),
|
||||
}
|
||||
case core.CodecPCMA:
|
||||
pkt = &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
SequenceNumber: pkt.SequenceNumber,
|
||||
Timestamp: uint32(uint64(pkt.Timestamp) * 8000 / 1000000),
|
||||
},
|
||||
Payload: pkt.Payload,
|
||||
}
|
||||
}
|
||||
recv.WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Producer) readPacket() (*core.Packet, error) {
|
||||
b := make([]byte, 8)
|
||||
|
||||
if _, err := io.ReadFull(p.rd, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b[0] != '$' {
|
||||
return nil, errors.New("eseecloud: wrong start byte")
|
||||
}
|
||||
|
||||
size := binary.BigEndian.Uint32(b[4:])
|
||||
b = make([]byte, size)
|
||||
if _, err := io.ReadFull(p.rd, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkt := &core.Packet{}
|
||||
if err := pkt.Unmarshal(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.Recv += int(size)
|
||||
|
||||
return pkt, nil
|
||||
}
|
@@ -6,17 +6,24 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
)
|
||||
|
||||
func newRequest(method, url string, headers map[string]any) (*http.Request, error) {
|
||||
func newRequest(method, url string, headers map[string]any, body string) (*http.Request, error) {
|
||||
var rd io.Reader
|
||||
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
if body != "" {
|
||||
rd = strings.NewReader(body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
req, err := http.NewRequest(method, url, rd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -55,7 +62,8 @@ var Options = []expr.Option{
|
||||
options := params[1].(map[string]any)
|
||||
method, _ := options["method"].(string)
|
||||
headers, _ := options["headers"].(map[string]any)
|
||||
req, err = newRequest(method, url, headers)
|
||||
body, _ := options["body"].(string)
|
||||
req, err = newRequest(method, url, headers, body)
|
||||
} else {
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
}
|
||||
@@ -105,11 +113,19 @@ var Options = []expr.Option{
|
||||
),
|
||||
}
|
||||
|
||||
func Run(input string) (any, error) {
|
||||
program, err := expr.Compile(input, Options...)
|
||||
func Compile(input string) (*vm.Program, error) {
|
||||
return expr.Compile(input, Options...)
|
||||
}
|
||||
|
||||
func Eval(input string, env any) (any, error) {
|
||||
program, err := Compile(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return expr.Run(program, nil)
|
||||
return expr.Run(program, env)
|
||||
}
|
||||
|
||||
func Run(program *vm.Program, env any) (any, error) {
|
||||
return vm.Run(program, env)
|
||||
}
|
||||
|
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
func TestMatchHost(t *testing.T) {
|
||||
v, err := Run(`
|
||||
v, err := Eval(`
|
||||
let url = "rtsp://user:pass@192.168.1.123/cam/realmonitor?...";
|
||||
let host = match(url, "//[^/]+")[0][2:];
|
||||
host
|
||||
`)
|
||||
`, nil)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "user:pass@192.168.1.123", v)
|
||||
}
|
||||
|
@@ -16,6 +16,11 @@ func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
ps := JoinNALU(sps, pps)
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
// this can happen for FLV from FFmpeg
|
||||
if NALUType(packet.Payload) == NALUTypeSEI {
|
||||
size := int(binary.BigEndian.Uint32(packet.Payload)) + 4
|
||||
packet.Payload = packet.Payload[size:]
|
||||
}
|
||||
if NALUType(packet.Payload) == NALUTypeIFrame {
|
||||
packet.Payload = Join(ps, packet.Payload)
|
||||
}
|
||||
@@ -82,7 +87,15 @@ func AVCCToCodec(avcc []byte) *core.Codec {
|
||||
buf := bytes.NewBufferString("packetization-mode=1")
|
||||
|
||||
for {
|
||||
n := len(avcc)
|
||||
if n < 4 {
|
||||
break
|
||||
}
|
||||
|
||||
size := 4 + int(binary.BigEndian.Uint32(avcc))
|
||||
if n < size {
|
||||
break
|
||||
}
|
||||
|
||||
switch NALUType(avcc) {
|
||||
case NALUTypeSPS:
|
||||
@@ -95,11 +108,7 @@ func AVCCToCodec(avcc []byte) *core.Codec {
|
||||
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
|
||||
}
|
||||
|
||||
if size < len(avcc) {
|
||||
avcc = avcc[size:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
avcc = avcc[size:]
|
||||
}
|
||||
|
||||
return &core.Codec{
|
||||
|
@@ -101,3 +101,10 @@ func TestDecodeSPS2(t *testing.T) {
|
||||
require.Equal(t, uint16(640), sps.Width())
|
||||
require.Equal(t, uint16(360), sps.Height())
|
||||
}
|
||||
|
||||
func TestAVCCToCodec(t *testing.T) {
|
||||
s := "000000196764001fac2484014016ec0440000003004000000c23c60c920000000568ee32c8b0000000d365"
|
||||
b, _ := hex.DecodeString(s)
|
||||
codec := AVCCToCodec(b)
|
||||
require.Equal(t, "packetization-mode=1;profile-level-id=64001f;sprop-parameter-sets=Z2QAH6wkhAFAFuwEQAAAAwBAAAAMI8YMkg==,aO4yyLA=", codec.FmtpLine)
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ func NewAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {
|
||||
hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),
|
||||
ServiceCameraRTPStreamManagement(),
|
||||
//hap.ServiceHAPProtocolInformation(),
|
||||
//ServiceMicrophone(),
|
||||
ServiceMicrophone(),
|
||||
},
|
||||
}
|
||||
acc.InitIID()
|
||||
@@ -30,17 +30,17 @@ func ServiceMicrophone() *hap.Service {
|
||||
Perms: hap.EVPRPW,
|
||||
//Descr: "Mute",
|
||||
},
|
||||
{
|
||||
Type: "119",
|
||||
Format: hap.FormatUInt8,
|
||||
Value: 100,
|
||||
Perms: hap.EVPRPW,
|
||||
//Descr: "Volume",
|
||||
//Unit: hap.UnitPercentage,
|
||||
//MinValue: 0,
|
||||
//MaxValue: 100,
|
||||
//MinStep: 1,
|
||||
},
|
||||
//{
|
||||
// Type: "119",
|
||||
// Format: hap.FormatUInt8,
|
||||
// Value: 100,
|
||||
// Perms: hap.EVPRPW,
|
||||
// //Descr: "Volume",
|
||||
// //Unit: hap.UnitPercentage,
|
||||
// //MinValue: 0,
|
||||
// //MaxValue: 100,
|
||||
// //MinStep: 1,
|
||||
//},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
|
||||
VideoAttrs: []VideoAttrs{
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
||||
{Width: 320, Height: 240, Framerate: 15}, // apple watch
|
||||
{Width: 320, Height: 240, Framerate: 15}, // apple watch
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -71,11 +71,17 @@ type JSONCharacter struct {
|
||||
Event any `json:"ev,omitempty"`
|
||||
}
|
||||
|
||||
// 4.2.1.2 Invalid Setup Codes
|
||||
const insecurePINs = "00000000 11111111 22222222 33333333 44444444 55555555 66666666 77777777 88888888 99999999 12345678 87654321"
|
||||
|
||||
func SanitizePin(pin string) (string, error) {
|
||||
s := strings.ReplaceAll(pin, "-", "")
|
||||
if len(s) != 8 {
|
||||
return "", errors.New("hap: wrong PIN format: " + pin)
|
||||
}
|
||||
if strings.Contains(insecurePINs, s) {
|
||||
return "", errors.New("hap: insecure PIN: " + pin)
|
||||
}
|
||||
// 123-45-678
|
||||
return s[:3] + "-" + s[3:5] + "-" + s[5:], nil
|
||||
}
|
||||
|
@@ -46,6 +46,8 @@ func Marshal(v any) ([]byte, error) {
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Slice:
|
||||
return appendSlice(nil, value)
|
||||
case reflect.Struct:
|
||||
return appendStruct(nil, value)
|
||||
}
|
||||
@@ -53,6 +55,23 @@ func Marshal(v any) ([]byte, error) {
|
||||
return nil, errors.New("tlv8: not implemented: " + kind.String())
|
||||
}
|
||||
|
||||
// separator the most confusing meaning in the documentation.
|
||||
// It can have a value of 0x00 or 0xFF or even 0x05.
|
||||
const separator = 0xFF
|
||||
|
||||
func appendSlice(b []byte, value reflect.Value) ([]byte, error) {
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
if i > 0 {
|
||||
b = append(b, separator, 0)
|
||||
}
|
||||
var err error
|
||||
if b, err = appendStruct(b, value.Index(i)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func appendStruct(b []byte, value reflect.Value) ([]byte, error) {
|
||||
valueType := value.Type()
|
||||
|
||||
@@ -121,7 +140,7 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
|
||||
case reflect.Slice:
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
if i > 0 {
|
||||
b = append(b, 0, 0)
|
||||
b = append(b, separator, 0)
|
||||
}
|
||||
if b, err = appendValue(b, tag, value.Index(i)); err != nil {
|
||||
return nil, err
|
||||
@@ -179,64 +198,86 @@ func Unmarshal(data []byte, v any) error {
|
||||
kind = value.Kind()
|
||||
}
|
||||
|
||||
if kind != reflect.Struct {
|
||||
return errors.New("tlv8: not implemented: " + kind.String())
|
||||
switch kind {
|
||||
case reflect.Slice:
|
||||
return unmarshalSlice(data, value)
|
||||
case reflect.Struct:
|
||||
return unmarshalStruct(data, value)
|
||||
}
|
||||
|
||||
return unmarshalStruct(data, value)
|
||||
return errors.New("tlv8: not implemented: " + kind.String())
|
||||
}
|
||||
|
||||
func unmarshalStruct(b []byte, value reflect.Value) error {
|
||||
var waitSlice bool
|
||||
// unmarshalTLV can return two types of errors:
|
||||
// - critical and then the value of []byte will be nil
|
||||
// - not critical and then []byte will contain the value
|
||||
func unmarshalTLV(b []byte, value reflect.Value) ([]byte, error) {
|
||||
if len(b) < 2 {
|
||||
return nil, errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
|
||||
for len(b) >= 2 {
|
||||
t := b[0]
|
||||
l := int(b[1])
|
||||
t := b[0]
|
||||
l := int(b[1])
|
||||
|
||||
// array item divider
|
||||
if t == 0 && l == 0 {
|
||||
b = b[2:]
|
||||
waitSlice = true
|
||||
continue
|
||||
// array item divider (t == 0x00 || t == 0xFF)
|
||||
if l == 0 {
|
||||
return b[2:], errors.New("tlv8: zero item")
|
||||
}
|
||||
|
||||
var v []byte
|
||||
|
||||
for {
|
||||
if len(b) < 2+l {
|
||||
return nil, errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
|
||||
var v []byte
|
||||
v = append(v, b[2:2+l]...)
|
||||
b = b[2+l:]
|
||||
|
||||
for {
|
||||
if len(b) < 2+l {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
// if size == 255 and same tag - continue read big payload
|
||||
if l < 255 || len(b) < 2 || b[0] != t {
|
||||
break
|
||||
}
|
||||
|
||||
l = int(b[1])
|
||||
}
|
||||
|
||||
tag := strconv.Itoa(int(t))
|
||||
|
||||
valueField, ok := getStructField(value, tag)
|
||||
if !ok {
|
||||
return b, fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
|
||||
}
|
||||
|
||||
if err := unmarshalValue(v, valueField); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func unmarshalSlice(b []byte, value reflect.Value) error {
|
||||
valueIndex := value.Index(growSlice(value))
|
||||
for len(b) > 0 {
|
||||
var err error
|
||||
if b, err = unmarshalTLV(b, valueIndex); err != nil {
|
||||
if b != nil {
|
||||
valueIndex = value.Index(growSlice(value))
|
||||
continue
|
||||
}
|
||||
|
||||
v = append(v, b[2:2+l]...)
|
||||
b = b[2+l:]
|
||||
|
||||
// if size == 255 and same tag - continue read big payload
|
||||
if l < 255 || len(b) < 2 || b[0] != t {
|
||||
break
|
||||
}
|
||||
|
||||
l = int(b[1])
|
||||
}
|
||||
|
||||
tag := strconv.Itoa(int(t))
|
||||
|
||||
valueField, ok := getStructField(value, tag)
|
||||
if !ok {
|
||||
return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
|
||||
}
|
||||
|
||||
if waitSlice {
|
||||
if valueField.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("tlv8: should be slice T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
|
||||
}
|
||||
waitSlice = false
|
||||
}
|
||||
|
||||
if err := unmarshalValue(v, valueField); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalStruct(b []byte, value reflect.Value) error {
|
||||
for len(b) > 0 {
|
||||
var err error
|
||||
if b, err = unmarshalTLV(b, value); b == nil && err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package tlv8
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -107,3 +108,49 @@ func TestInterface(t *testing.T) {
|
||||
|
||||
require.Equal(t, src, dst)
|
||||
}
|
||||
|
||||
func TestSlice1(t *testing.T) {
|
||||
var v struct {
|
||||
VideoAttrs []struct {
|
||||
Width uint16 `tlv8:"1"`
|
||||
Height uint16 `tlv8:"2"`
|
||||
Framerate uint8 `tlv8:"3"`
|
||||
} `tlv8:"3"`
|
||||
}
|
||||
|
||||
s := `030b010280070202380403011e ff00 030b010200050202d00203011e`
|
||||
b1, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Unmarshal(b1, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, v.VideoAttrs, 2)
|
||||
|
||||
b2, err := Marshal(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, b1, b2)
|
||||
}
|
||||
|
||||
func TestSlice2(t *testing.T) {
|
||||
var v []struct {
|
||||
Width uint16 `tlv8:"1"`
|
||||
Height uint16 `tlv8:"2"`
|
||||
Framerate uint8 `tlv8:"3"`
|
||||
}
|
||||
|
||||
s := `010280070202380403011e ff00 010200050202d00203011e`
|
||||
b1, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Unmarshal(b1, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, v, 2)
|
||||
|
||||
b2, err := Marshal(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, b1, b2)
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
@@ -50,7 +50,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
|
||||
mediaCodec := &core.Codec{
|
||||
Name: audioCodecs[codec.CodecType],
|
||||
ClockRate: audioSampleRates[sampleRate],
|
||||
Channels: uint16(param.Channels),
|
||||
Channels: param.Channels,
|
||||
}
|
||||
|
||||
if mediaCodec.Name == core.CodecELD {
|
||||
|
3
pkg/ioctl/README.md
Normal file
3
pkg/ioctl/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# IOCTL
|
||||
|
||||
This is just an example how Linux IOCTL constants works.
|
28
pkg/ioctl/ioctl.go
Normal file
28
pkg/ioctl/ioctl.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ioctl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func Str(b []byte) string {
|
||||
if i := bytes.IndexByte(b, 0); i >= 0 {
|
||||
return string(b[:i])
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func io(mode byte, type_ byte, number byte, size uint16) uintptr {
|
||||
return uintptr(mode)<<30 | uintptr(size)<<16 | uintptr(type_)<<8 | uintptr(number)
|
||||
}
|
||||
|
||||
func IOR(type_ byte, number byte, size uint16) uintptr {
|
||||
return io(read, type_, number, size)
|
||||
}
|
||||
|
||||
func IOW(type_ byte, number byte, size uint16) uintptr {
|
||||
return io(write, type_, number, size)
|
||||
}
|
||||
|
||||
func IORW(type_ byte, number byte, size uint16) uintptr {
|
||||
return io(read|write, type_, number, size)
|
||||
}
|
8
pkg/ioctl/ioctl_be.go
Normal file
8
pkg/ioctl/ioctl_be.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build arm || arm64 || 386 || amd64
|
||||
|
||||
package ioctl
|
||||
|
||||
const (
|
||||
write = 1
|
||||
read = 2
|
||||
)
|
8
pkg/ioctl/ioctl_le.go
Normal file
8
pkg/ioctl/ioctl_le.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build mipsle
|
||||
|
||||
package ioctl
|
||||
|
||||
const (
|
||||
read = 1
|
||||
write = 2
|
||||
)
|
14
pkg/ioctl/ioctl_linux.go
Normal file
14
pkg/ioctl/ioctl_linux.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package ioctl
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func Ioctl(fd int, req uint, arg unsafe.Pointer) error {
|
||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
16
pkg/ioctl/ioctl_test.go
Normal file
16
pkg/ioctl/ioctl_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package ioctl
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIOR(t *testing.T) {
|
||||
// #define SNDRV_PCM_IOCTL_INFO _IOR('A', 0x01, struct snd_pcm_info)
|
||||
if runtime.GOARCH == "arm64" {
|
||||
c := IOR('A', 0x01, 288)
|
||||
require.Equal(t, uintptr(0x81204101), c)
|
||||
}
|
||||
}
|
@@ -89,12 +89,12 @@ func (m *Muxer) GetInit() ([]byte, error) {
|
||||
}
|
||||
|
||||
mv.WriteAudioTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
||||
uint32(i+1), codec.Name, codec.ClockRate, uint16(codec.Channels), b,
|
||||
)
|
||||
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecFLAC:
|
||||
mv.WriteAudioTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
||||
uint32(i+1), codec.Name, codec.ClockRate, uint16(codec.Channels), nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
144
pkg/nest/api.go
144
pkg/nest/api.go
@@ -166,42 +166,100 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {
|
||||
|
||||
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
|
||||
projectID + "/devices/" + deviceID + ":executeCommand"
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
|
||||
|
||||
maxRetries := 3
|
||||
retryDelay := time.Second * 30
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+a.Token)
|
||||
|
||||
client := &http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Handle 409 (Conflict), 429 (Too Many Requests), and 401 (Unauthorized)
|
||||
if res.StatusCode == 409 || res.StatusCode == 429 || res.StatusCode == 401 {
|
||||
res.Body.Close()
|
||||
if attempt < maxRetries-1 {
|
||||
// Get new token from Google
|
||||
if err := a.refreshToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
time.Sleep(retryDelay)
|
||||
retryDelay *= 2 // exponential backoff
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", errors.New("nest: wrong status: " + res.Status)
|
||||
}
|
||||
|
||||
var resv struct {
|
||||
Results struct {
|
||||
Answer string `json:"answerSdp"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
MediaSessionID string `json:"mediaSessionId"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
a.StreamProjectID = projectID
|
||||
a.StreamDeviceID = deviceID
|
||||
a.StreamSessionID = resv.Results.MediaSessionID
|
||||
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||
|
||||
return resv.Results.Answer, nil
|
||||
}
|
||||
|
||||
return "", errors.New("nest: max retries exceeded")
|
||||
}
|
||||
|
||||
func (a *API) refreshToken() error {
|
||||
// Get the cached API with matching token to get credentials
|
||||
var refreshKey string
|
||||
cacheMu.Lock()
|
||||
for key, api := range cache {
|
||||
if api.Token == a.Token {
|
||||
refreshKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
cacheMu.Unlock()
|
||||
|
||||
if refreshKey == "" {
|
||||
return errors.New("nest: unable to find cached credentials")
|
||||
}
|
||||
|
||||
// Parse credentials from cache key
|
||||
parts := strings.Split(refreshKey, ":")
|
||||
if len(parts) != 3 {
|
||||
return errors.New("nest: invalid cache key format")
|
||||
}
|
||||
clientID, clientSecret, refreshToken := parts[0], parts[1], parts[2]
|
||||
|
||||
// Get new API instance which will refresh the token
|
||||
newAPI, err := NewAPI(clientID, clientSecret, refreshToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+a.Token)
|
||||
|
||||
client := &http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", errors.New("nest: wrong status: " + res.Status)
|
||||
}
|
||||
|
||||
var resv struct {
|
||||
Results struct {
|
||||
Answer string `json:"answerSdp"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
MediaSessionID string `json:"mediaSessionId"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
a.StreamProjectID = projectID
|
||||
a.StreamDeviceID = deviceID
|
||||
a.StreamSessionID = resv.Results.MediaSessionID
|
||||
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||
|
||||
return resv.Results.Answer, nil
|
||||
// Update current API with new token
|
||||
a.Token = newAPI.Token
|
||||
a.ExpiresAt = newAPI.ExpiresAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) ExtendStream() error {
|
||||
@@ -407,20 +465,22 @@ type Device struct {
|
||||
}
|
||||
|
||||
func (a *API) StartExtendStreamTimer() {
|
||||
// Calculate the duration until 30 seconds before the stream expires
|
||||
duration := time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
|
||||
a.extendTimer = time.AfterFunc(duration, func() {
|
||||
if a.extendTimer != nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.extendTimer = time.NewTimer(time.Until(a.StreamExpiresAt) - time.Minute)
|
||||
go func() {
|
||||
<-a.extendTimer.C
|
||||
if err := a.ExtendStream(); err != nil {
|
||||
return
|
||||
}
|
||||
duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
|
||||
a.extendTimer.Reset(duration)
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *API) StopExtendStreamTimer() {
|
||||
if a.extendTimer == nil {
|
||||
return
|
||||
if a.extendTimer != nil {
|
||||
a.extendTimer.Stop()
|
||||
a.extendTimer = nil
|
||||
}
|
||||
a.extendTimer.Stop()
|
||||
}
|
||||
|
@@ -4,11 +4,12 @@ import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type WebRTCClient struct {
|
||||
@@ -38,9 +39,26 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
return nil, errors.New("nest: wrong query")
|
||||
}
|
||||
|
||||
nestAPI, err := NewAPI(cliendID, cliendSecret, refreshToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
maxRetries := 3
|
||||
retryDelay := time.Second * 30
|
||||
|
||||
var nestAPI *API
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
nestAPI, err = NewAPI(cliendID, cliendSecret, refreshToken)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
lastErr = err
|
||||
if attempt < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
retryDelay *= 2 // exponential backoff
|
||||
}
|
||||
}
|
||||
|
||||
if nestAPI == nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
protocols := strings.Split(query.Get("protocols"), ",")
|
||||
@@ -79,48 +97,62 @@ func (c *WebRTCClient) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) {
|
||||
rtcAPI, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
maxRetries := 3
|
||||
retryDelay := time.Second * 30
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
rtcAPI, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := pion.Configuration{}
|
||||
pc, err := rtcAPI.NewPeerConnection(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.FormatName = "nest/webrtc"
|
||||
conn.Mode = core.ModeActiveProducer
|
||||
conn.Protocol = "http"
|
||||
conn.URL = rawURL
|
||||
|
||||
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: "app"}, // important for Nest
|
||||
}
|
||||
|
||||
// 3. Create offer with candidates
|
||||
offer, err := conn.CreateCompleteOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Exchange SDP via Hass
|
||||
answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if attempt < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
retryDelay *= 2
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. Set answer with remote medias
|
||||
if err = conn.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WebRTCClient{conn: conn, api: nestAPI}, nil
|
||||
}
|
||||
|
||||
conf := pion.Configuration{}
|
||||
pc, err := rtcAPI.NewPeerConnection(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.FormatName = "nest/webrtc"
|
||||
conn.Mode = core.ModeActiveProducer
|
||||
conn.Protocol = "http"
|
||||
conn.URL = rawURL
|
||||
|
||||
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: "app"}, // important for Nest
|
||||
}
|
||||
|
||||
// 3. Create offer with candidates
|
||||
offer, err := conn.CreateCompleteOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Exchange SDP via Hass
|
||||
answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. Set answer with remote medias
|
||||
if err = conn.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WebRTCClient{conn: conn, api: nestAPI}, nil
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) {
|
||||
|
69
pkg/pcm/backchannel.go
Normal file
69
pkg/pcm/backchannel.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Backchannel struct {
|
||||
core.Connection
|
||||
cmd *shell.Command
|
||||
}
|
||||
|
||||
func NewBackchannel(cmd *shell.Command, audio string) (core.Producer, error) {
|
||||
var codec *core.Codec
|
||||
|
||||
if audio == "" {
|
||||
// default codec
|
||||
codec = &core.Codec{Name: core.CodecPCML, ClockRate: 16000}
|
||||
} else if codec = core.ParseCodecString(audio); codec == nil {
|
||||
return nil, errors.New("pcm: unsupported audio format: " + audio)
|
||||
}
|
||||
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
},
|
||||
}
|
||||
|
||||
return &Backchannel{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "pcm",
|
||||
Protocol: "pipe",
|
||||
Medias: medias,
|
||||
Transport: cmd,
|
||||
},
|
||||
cmd: cmd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
wr, err := c.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
if n, err := wr.Write(packet.Payload); err != nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Backchannel) Start() error {
|
||||
return c.cmd.Run()
|
||||
}
|
109
pkg/pcm/handlers.go
Normal file
109
pkg/pcm/handlers.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
// RepackG711 - Repack G.711 PCMA/PCMU into frames of size 1024
|
||||
// 1. Fixes WebRTC audio quality issue (monotonic timestamp)
|
||||
// 2. Fixes Reolink Doorbell backchannel issue (zero timestamp)
|
||||
// https://github.com/AlexxIT/go2rtc/issues/331
|
||||
func RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc {
|
||||
const PacketSize = 1024
|
||||
|
||||
var buf []byte
|
||||
var seq uint16
|
||||
var ts uint32
|
||||
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/432
|
||||
var mu sync.Mutex
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
mu.Lock()
|
||||
|
||||
buf = append(buf, packet.Payload...)
|
||||
if len(buf) < PacketSize {
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true, // should be true
|
||||
PayloadType: packet.PayloadType, // will be owerwriten
|
||||
SequenceNumber: seq,
|
||||
SSRC: packet.SSRC,
|
||||
},
|
||||
Payload: buf[:PacketSize],
|
||||
}
|
||||
|
||||
seq++
|
||||
|
||||
// don't know if zero TS important for Reolink Doorbell
|
||||
// don't have this strange devices for tests
|
||||
if !zeroTS {
|
||||
pkt.Timestamp = ts
|
||||
ts += PacketSize
|
||||
}
|
||||
|
||||
buf = buf[PacketSize:]
|
||||
|
||||
mu.Unlock()
|
||||
|
||||
handler(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
// LittleToBig - convert PCM little endian to PCM big endian
|
||||
func LittleToBig(handler core.HandlerFunc) core.HandlerFunc {
|
||||
return func(packet *rtp.Packet) {
|
||||
clone := *packet
|
||||
clone.Payload = FlipEndian(packet.Payload)
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func TranscodeHandler(dst, src *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
var ts uint32
|
||||
k := float32(BytesPerFrame(dst)) / float32(BytesPerFrame(src))
|
||||
f := Transcode(dst, src)
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
ts += uint32(k * float32(len(packet.Payload)))
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = f(packet.Payload)
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func BytesPerSample(codec *core.Codec) int {
|
||||
switch codec.Name {
|
||||
case core.CodecPCML, core.CodecPCM:
|
||||
return 2
|
||||
case core.CodecPCMU, core.CodecPCMA:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func BytesPerFrame(codec *core.Codec) int {
|
||||
if codec.Channels <= 1 {
|
||||
return BytesPerSample(codec)
|
||||
}
|
||||
return int(codec.Channels) * BytesPerSample(codec)
|
||||
}
|
||||
|
||||
func FramesPerDuration(codec *core.Codec, duration time.Duration) int {
|
||||
return int(time.Duration(codec.ClockRate) * duration / time.Second)
|
||||
}
|
||||
|
||||
func BytesPerDuration(codec *core.Codec, duration time.Duration) int {
|
||||
return BytesPerFrame(codec) * FramesPerDuration(codec, duration)
|
||||
}
|
342
pkg/pcm/pcm.go
342
pkg/pcm/pcm.go
@@ -1,200 +1,220 @@
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"math"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
// ResampleToG711 - convert PCMA/PCM/PCML to PCMA and PCMU to PCMU with decreasing sample rate
|
||||
func ResampleToG711(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc {
|
||||
n := float32(codec.ClockRate) / float32(sampleRate)
|
||||
|
||||
if codec.Channels == 2 {
|
||||
n *= 2 // hacky way for support two channels audio
|
||||
func ceil(x float32) int {
|
||||
d, fract := math.Modf(float64(x))
|
||||
if fract == 0.0 {
|
||||
return int(d)
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecPCMA:
|
||||
return DownsampleByte(PCMAtoPCM, PCMtoPCMA, n, handler)
|
||||
case core.CodecPCMU:
|
||||
return DownsampleByte(PCMUtoPCM, PCMtoPCMU, n, handler)
|
||||
case core.CodecPCM, core.CodecPCML:
|
||||
if n == 1 {
|
||||
handler = ResamplePCM(PCMtoPCMA, handler)
|
||||
} else {
|
||||
handler = DownsamplePCM(PCMtoPCMA, n, handler)
|
||||
}
|
||||
|
||||
if codec.Name == core.CodecPCML {
|
||||
return LittleToBig(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
panic(core.Caller())
|
||||
return int(d) + 1
|
||||
}
|
||||
|
||||
// DownsampleByte - convert PCMA/PCMU to PCMA/PCMU with decreasing sample rate (N times)
|
||||
func DownsampleByte(
|
||||
toPCM func(byte) int16, fromPCM func(int16) byte, n float32, handler core.HandlerFunc,
|
||||
) core.HandlerFunc {
|
||||
func Downsample(k float32) func([]int16) []int16 {
|
||||
var sampleN, sampleSum float32
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
samples := len(packet.Payload)
|
||||
newLen := uint32((float32(samples) + sampleN) / n)
|
||||
|
||||
oldSamples := packet.Payload
|
||||
newSamples := make([]byte, newLen)
|
||||
|
||||
return func(src []int16) (dst []int16) {
|
||||
var i int
|
||||
for _, sample := range oldSamples {
|
||||
sampleSum += float32(toPCM(sample))
|
||||
if sampleN++; sampleN >= n {
|
||||
newSamples[i] = fromPCM(int16(sampleSum / n))
|
||||
dst = make([]int16, ceil((float32(len(src))+sampleN)/k))
|
||||
for _, sample := range src {
|
||||
sampleSum += float32(sample)
|
||||
sampleN++
|
||||
if sampleN >= k {
|
||||
dst[i] = int16(sampleSum / k)
|
||||
i++
|
||||
|
||||
sampleSum = 0
|
||||
sampleN -= n
|
||||
sampleN -= k
|
||||
}
|
||||
}
|
||||
|
||||
ts += newLen
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = newSamples
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// LittleToBig - conver PCM little endian to PCM big endian
|
||||
func LittleToBig(handler core.HandlerFunc) core.HandlerFunc {
|
||||
return func(packet *rtp.Packet) {
|
||||
size := len(packet.Payload)
|
||||
b := make([]byte, size)
|
||||
for i := 0; i < size; i += 2 {
|
||||
b[i] = packet.Payload[i+1]
|
||||
b[i+1] = packet.Payload[i]
|
||||
}
|
||||
func Upsample(k float32) func([]int16) []int16 {
|
||||
var sampleN float32
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = b
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
return func(src []int16) (dst []int16) {
|
||||
var i int
|
||||
dst = make([]int16, ceil(k*float32(len(src))))
|
||||
for _, sample := range src {
|
||||
sampleN += k
|
||||
for sampleN > 0 {
|
||||
dst[i] = sample
|
||||
i++
|
||||
|
||||
// ResamplePCM - convert PCM to PCMA/PCMU with same sample rate
|
||||
func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.HandlerFunc {
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
len1 := len(packet.Payload)
|
||||
len2 := len1 / 2
|
||||
|
||||
oldSamples := packet.Payload
|
||||
newSamples := make([]byte, len2)
|
||||
|
||||
var i2 int
|
||||
for i1 := 0; i1 < len1; i1 += 2 {
|
||||
sample := int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1]))
|
||||
newSamples[i2] = fromPCM(sample)
|
||||
i2++
|
||||
}
|
||||
|
||||
ts += uint32(len2)
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = newSamples
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
// DownsamplePCM - convert PCM to PCMA/PCMU with decreasing sample rate (N times)
|
||||
func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc) core.HandlerFunc {
|
||||
var sampleN, sampleSum float32
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
samples := len(packet.Payload) / 2
|
||||
newLen := uint32((float32(samples) + sampleN) / n)
|
||||
|
||||
oldSamples := packet.Payload
|
||||
newSamples := make([]byte, newLen)
|
||||
|
||||
var i2 int
|
||||
for i1 := 0; i1 < len(packet.Payload); i1 += 2 {
|
||||
sampleSum += float32(int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1])))
|
||||
if sampleN++; sampleN >= n {
|
||||
newSamples[i2] = fromPCM(int16(sampleSum / n))
|
||||
i2++
|
||||
|
||||
sampleSum = 0
|
||||
sampleN -= n
|
||||
sampleN -= 1
|
||||
}
|
||||
}
|
||||
|
||||
ts += newLen
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = newSamples
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RepackG711 - Repack G.711 PCMA/PCMU into frames of size 1024
|
||||
// 1. Fixes WebRTC audio quality issue (monotonic timestamp)
|
||||
// 2. Fixes Reolink Doorbell backchannel issue (zero timestamp)
|
||||
// https://github.com/AlexxIT/go2rtc/issues/331
|
||||
func RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc {
|
||||
const PacketSize = 1024
|
||||
func FlipEndian(src []byte) (dst []byte) {
|
||||
var i, j int
|
||||
n := len(src)
|
||||
dst = make([]byte, n)
|
||||
for i < n {
|
||||
x := src[i]
|
||||
i++
|
||||
dst[j] = src[i]
|
||||
j++
|
||||
i++
|
||||
dst[j] = x
|
||||
j++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
var seq uint16
|
||||
var ts uint32
|
||||
func Transcode(dst, src *core.Codec) func([]byte) []byte {
|
||||
var reader func([]byte) []int16
|
||||
var writer func([]int16) []byte
|
||||
var filters []func([]int16) []int16
|
||||
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/432
|
||||
var mu sync.Mutex
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
mu.Lock()
|
||||
|
||||
buf = append(buf, packet.Payload...)
|
||||
if len(buf) < PacketSize {
|
||||
mu.Unlock()
|
||||
switch src.Name {
|
||||
case core.CodecPCML:
|
||||
reader = func(src []byte) (dst []int16) {
|
||||
var i, j int
|
||||
n := len(src)
|
||||
dst = make([]int16, n/2)
|
||||
for i < n {
|
||||
lo := src[i]
|
||||
i++
|
||||
hi := src[i]
|
||||
i++
|
||||
dst[j] = int16(hi)<<8 | int16(lo)
|
||||
j++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true, // should be true
|
||||
PayloadType: packet.PayloadType, // will be owerwriten
|
||||
SequenceNumber: seq,
|
||||
SSRC: packet.SSRC,
|
||||
},
|
||||
Payload: buf[:PacketSize],
|
||||
case core.CodecPCM:
|
||||
reader = func(src []byte) (dst []int16) {
|
||||
var i, j int
|
||||
n := len(src)
|
||||
dst = make([]int16, n/2)
|
||||
for i < n {
|
||||
hi := src[i]
|
||||
i++
|
||||
lo := src[i]
|
||||
i++
|
||||
dst[j] = int16(hi)<<8 | int16(lo)
|
||||
j++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
seq++
|
||||
|
||||
// don't know if zero TS important for Reolink Doorbell
|
||||
// don't have this strange devices for tests
|
||||
if !zeroTS {
|
||||
pkt.Timestamp = ts
|
||||
ts += PacketSize
|
||||
case core.CodecPCMU:
|
||||
reader = func(src []byte) (dst []int16) {
|
||||
var i int
|
||||
dst = make([]int16, len(src))
|
||||
for _, sample := range src {
|
||||
dst[i] = PCMUtoPCM(sample)
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
case core.CodecPCMA:
|
||||
reader = func(src []byte) (dst []int16) {
|
||||
var i int
|
||||
dst = make([]int16, len(src))
|
||||
for _, sample := range src {
|
||||
dst[i] = PCMAtoPCM(sample)
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[PacketSize:]
|
||||
if src.Channels > 1 {
|
||||
filters = append(filters, Downsample(float32(src.Channels)))
|
||||
}
|
||||
|
||||
mu.Unlock()
|
||||
if src.ClockRate > dst.ClockRate {
|
||||
filters = append(filters, Downsample(float32(src.ClockRate)/float32(dst.ClockRate)))
|
||||
} else if src.ClockRate < dst.ClockRate {
|
||||
filters = append(filters, Upsample(float32(dst.ClockRate)/float32(src.ClockRate)))
|
||||
}
|
||||
|
||||
handler(pkt)
|
||||
if dst.Channels > 1 {
|
||||
filters = append(filters, Upsample(float32(dst.Channels)))
|
||||
}
|
||||
|
||||
switch dst.Name {
|
||||
case core.CodecPCML:
|
||||
writer = func(src []int16) (dst []byte) {
|
||||
var i int
|
||||
dst = make([]byte, len(src)*2)
|
||||
for _, sample := range src {
|
||||
dst[i] = byte(sample)
|
||||
i++
|
||||
dst[i] = byte(sample >> 8)
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
case core.CodecPCM:
|
||||
writer = func(src []int16) (dst []byte) {
|
||||
var i int
|
||||
dst = make([]byte, len(src)*2)
|
||||
for _, sample := range src {
|
||||
dst[i] = byte(sample >> 8)
|
||||
i++
|
||||
dst[i] = byte(sample)
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
case core.CodecPCMU:
|
||||
writer = func(src []int16) (dst []byte) {
|
||||
var i int
|
||||
dst = make([]byte, len(src))
|
||||
for _, sample := range src {
|
||||
dst[i] = PCMtoPCMU(sample)
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
case core.CodecPCMA:
|
||||
writer = func(src []int16) (dst []byte) {
|
||||
var i int
|
||||
dst = make([]byte, len(src))
|
||||
for _, sample := range src {
|
||||
dst[i] = PCMtoPCMA(sample)
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return func(b []byte) []byte {
|
||||
samples := reader(b)
|
||||
for _, filter := range filters {
|
||||
samples = filter(samples)
|
||||
}
|
||||
return writer(samples)
|
||||
}
|
||||
}
|
||||
|
||||
func ConsumerCodecs() []*core.Codec {
|
||||
return []*core.Codec{
|
||||
{Name: core.CodecPCML},
|
||||
{Name: core.CodecPCM},
|
||||
{Name: core.CodecPCMA},
|
||||
{Name: core.CodecPCMU},
|
||||
}
|
||||
}
|
||||
|
||||
func ProducerCodecs() []*core.Codec {
|
||||
return []*core.Codec{
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
{Name: core.CodecPCM, ClockRate: 16000},
|
||||
{Name: core.CodecPCML, ClockRate: 8000},
|
||||
{Name: core.CodecPCM, ClockRate: 8000},
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
{Name: core.CodecPCML, ClockRate: 22050}, // wyoming-snd-external
|
||||
}
|
||||
}
|
||||
|
79
pkg/pcm/pcm_test.go
Normal file
79
pkg/pcm/pcm_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTranscode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src core.Codec
|
||||
dst core.Codec
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "s16be->s16be",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561",
|
||||
expect: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561",
|
||||
},
|
||||
{
|
||||
name: "s16be->s16le",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
dst: core.Codec{Name: core.CodecPCML, ClockRate: 8000, Channels: 1},
|
||||
source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561",
|
||||
expect: "CAFC1300430328061308510B9E0D760FDA101111EA13BD15F2168216D4156115",
|
||||
},
|
||||
{
|
||||
name: "s16be->mulaw",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
dst: core.Codec{Name: core.CodecPCMU, ClockRate: 8000, Channels: 1},
|
||||
source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561",
|
||||
expect: "52FDD1C5BEB8B3B0AEAEABA9A8A8A9AA",
|
||||
},
|
||||
{
|
||||
name: "s16be->alaw",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
dst: core.Codec{Name: core.CodecPCMA, ClockRate: 8000, Channels: 1},
|
||||
source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561",
|
||||
expect: "7CD4FFED95939E9B8584868083838080",
|
||||
},
|
||||
{
|
||||
name: "2ch->1ch",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 2},
|
||||
dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
source: "FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76",
|
||||
expect: "FCCA00130343062808130B510D9E0F76",
|
||||
},
|
||||
{
|
||||
name: "1ch->2ch",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 2},
|
||||
source: "FCCA00130343062808130B510D9E0F76",
|
||||
expect: "FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76",
|
||||
},
|
||||
{
|
||||
name: "16khz->8khz",
|
||||
src: core.Codec{Name: core.CodecPCM, ClockRate: 16000, Channels: 1},
|
||||
dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},
|
||||
source: "FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76",
|
||||
expect: "FCCA00130343062808130B510D9E0F76",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := Transcode(&test.dst, &test.src)
|
||||
b, _ := hex.DecodeString(test.source)
|
||||
b = f(b)
|
||||
s := fmt.Sprintf("%X", b)
|
||||
require.Equal(t, test.expect, s)
|
||||
})
|
||||
}
|
||||
}
|
96
pkg/pcm/producer_sync.go
Normal file
96
pkg/pcm/producer_sync.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type ProducerSync struct {
|
||||
core.Connection
|
||||
src *core.Codec
|
||||
rd io.Reader
|
||||
onClose func()
|
||||
}
|
||||
|
||||
func OpenSync(codec *core.Codec, rd io.Reader) *ProducerSync {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: ProducerCodecs(),
|
||||
},
|
||||
}
|
||||
|
||||
return &ProducerSync{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "pcm",
|
||||
Medias: medias,
|
||||
Transport: rd,
|
||||
},
|
||||
src: codec,
|
||||
rd: rd,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProducerSync) OnClose(f func()) {
|
||||
p.onClose = f
|
||||
}
|
||||
|
||||
func (p *ProducerSync) Start() error {
|
||||
if len(p.Receivers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pktSeq uint16
|
||||
var pktTS uint32 // time in frames
|
||||
var pktTime time.Duration // time in seconds
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
dst := p.Receivers[0].Codec
|
||||
transcode := Transcode(dst, p.src)
|
||||
|
||||
const chunkDuration = 20 * time.Millisecond
|
||||
chunkBytes := BytesPerDuration(p.src, chunkDuration)
|
||||
chunkFrames := uint32(FramesPerDuration(dst, chunkDuration))
|
||||
|
||||
for {
|
||||
buf := make([]byte, chunkBytes)
|
||||
n, _ := io.ReadFull(p.rd, buf)
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
pkt := &core.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: pktSeq,
|
||||
Timestamp: pktTS,
|
||||
},
|
||||
Payload: transcode(buf[:n]),
|
||||
}
|
||||
|
||||
if d := pktTime - time.Since(t0); d > 0 {
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
p.Receivers[0].WriteRTP(pkt)
|
||||
p.Recv += n
|
||||
|
||||
pktSeq++
|
||||
pktTS += chunkFrames
|
||||
pktTime += chunkDuration
|
||||
}
|
||||
|
||||
if p.onClose != nil {
|
||||
p.onClose()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
42
pkg/pcm/s16le/s16le.go
Normal file
42
pkg/pcm/s16le/s16le.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package s16le
|
||||
|
||||
func PeaksRMS(b []byte) int16 {
|
||||
// RMS of sine wave = peak / sqrt2
|
||||
// https://en.wikipedia.org/wiki/Root_mean_square
|
||||
// https://www.youtube.com/watch?v=MUDkL4KZi0I
|
||||
var peaks int32
|
||||
var peaksSum int32
|
||||
var prevSample int16
|
||||
var prevUp bool
|
||||
|
||||
var i int
|
||||
for n := len(b); i < n; {
|
||||
lo := b[i]
|
||||
i++
|
||||
hi := b[i]
|
||||
i++
|
||||
|
||||
sample := int16(hi)<<8 | int16(lo)
|
||||
up := sample >= prevSample
|
||||
|
||||
if i >= 4 {
|
||||
if up != prevUp {
|
||||
if prevSample >= 0 {
|
||||
peaksSum += int32(prevSample)
|
||||
} else {
|
||||
peaksSum -= int32(prevSample)
|
||||
}
|
||||
peaks++
|
||||
}
|
||||
}
|
||||
|
||||
prevSample = sample
|
||||
prevUp = up
|
||||
}
|
||||
|
||||
if peaks == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int16(peaksSum / peaks)
|
||||
}
|
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -536,6 +536,6 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
||||
return webrtcProd.MarshalJSON()
|
||||
}
|
||||
|
||||
|
||||
return json.Marshal(c.prod)
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// Deprecated: should be rewritten to core.Connection
|
||||
|
@@ -116,20 +116,39 @@ func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) strin
|
||||
// urlParse fix bugs:
|
||||
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
||||
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
||||
// 3. Content-Base: 192.168.253.220:1935/
|
||||
func urlParse(rawURL string) (*url.URL, error) {
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/830
|
||||
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
||||
rawURL = rawURL[7:]
|
||||
}
|
||||
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/1852
|
||||
if !strings.Contains(rawURL, "://") {
|
||||
rawURL = "rtsp://" + rawURL
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil && strings.HasSuffix(err.Error(), "after host") {
|
||||
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
|
||||
if i2 := strings.IndexByte(rawURL[i1+3:], '/'); i2 > 0 {
|
||||
return urlParse(rawURL[:i1+3+i2] + ":" + rawURL[i1+3+i2:])
|
||||
}
|
||||
if i := indexN(rawURL, '/', 3); i > 0 {
|
||||
return urlParse(rawURL[:i] + ":" + rawURL[i:])
|
||||
}
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
func indexN(s string, c byte, n int) int {
|
||||
var offset int
|
||||
for {
|
||||
i := strings.IndexByte(s[offset:], c)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
if n--; n == 0 {
|
||||
return offset + i
|
||||
}
|
||||
offset += i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
@@ -11,14 +11,20 @@ func TestURLParse(t *testing.T) {
|
||||
// https://github.com/AlexxIT/WebRTC/issues/395
|
||||
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
|
||||
u, err := urlParse(base)
|
||||
assert.Empty(t, err)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "::ffff:192.168.1.123:", u.Host)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/208
|
||||
base = "rtsp://rtsp://turret2-cam.lan:554/stream1/"
|
||||
u, err = urlParse(base)
|
||||
assert.Empty(t, err)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "turret2-cam.lan:554", u.Host)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1852
|
||||
base = "192.168.253.220:1935/"
|
||||
u, err = urlParse(base)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "192.168.253.220:1935", u.Host)
|
||||
}
|
||||
|
||||
func TestBugSDP1(t *testing.T) {
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/srtp/v2"
|
||||
"github.com/pion/srtp/v3"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
|
@@ -1,59 +0,0 @@
|
||||
package stdin
|
||||
|
||||
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 {
|
||||
stdin, err := c.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sender = core.NewSender(media, track.Codec)
|
||||
c.sender.Handler = func(packet *rtp.Packet) {
|
||||
_, _ = stdin.Write(packet.Payload)
|
||||
c.send += len(packet.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
c.sender.HandleRTP(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() (err error) {
|
||||
return c.cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() (err error) {
|
||||
if c.sender != nil {
|
||||
c.sender.Close()
|
||||
}
|
||||
return c.cmd.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Connection{
|
||||
ID: core.ID(c),
|
||||
FormatName: "exec",
|
||||
Protocol: "pipe",
|
||||
Medias: c.medias,
|
||||
Send: c.send,
|
||||
}
|
||||
if c.sender != nil {
|
||||
info.Senders = []*core.Sender{c.sender}
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package stdin
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
// Deprecated: should be rewritten to core.Connection
|
||||
type Client struct {
|
||||
cmd *shell.Command
|
||||
|
||||
medias []*core.Media
|
||||
sender *core.Sender
|
||||
send int
|
||||
}
|
||||
|
||||
func NewClient(cmd *shell.Command) (*Client, error) {
|
||||
c := &Client{
|
||||
cmd: cmd,
|
||||
medias: []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCM},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
67
pkg/wav/backchannel.go
Normal file
67
pkg/wav/backchannel.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Backchannel struct {
|
||||
core.Connection
|
||||
cmd *shell.Command
|
||||
}
|
||||
|
||||
func NewBackchannel(cmd *shell.Command) (core.Producer, error) {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
//{Name: core.CodecPCML},
|
||||
{Name: core.CodecPCMA},
|
||||
{Name: core.CodecPCMU},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &Backchannel{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "wav",
|
||||
Protocol: "pipe",
|
||||
Medias: medias,
|
||||
Transport: cmd,
|
||||
},
|
||||
cmd: cmd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
wr, err := c.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := Header(track.Codec)
|
||||
if _, err = wr.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
if n, err := wr.Write(packet.Payload); err != nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Backchannel) Start() error {
|
||||
return c.cmd.Run()
|
||||
}
|
@@ -2,7 +2,6 @@ package wav
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
@@ -17,39 +16,11 @@ func Open(r io.Reader) (*Producer, error) {
|
||||
// https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
|
||||
rd := bufio.NewReaderSize(r, core.BufferSize)
|
||||
|
||||
// skip Master RIFF chunk
|
||||
if _, err := rd.Discard(12); err != nil {
|
||||
codec, err := ReadHeader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codec := &core.Codec{}
|
||||
|
||||
for {
|
||||
chunkID, data, err := readChunk(rd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if chunkID == "data" {
|
||||
break
|
||||
}
|
||||
|
||||
if chunkID == "fmt " {
|
||||
// https://audiocoding.cc/articles/2008-05-22-wav-file-structure/wav_formats.txt
|
||||
switch data[0] {
|
||||
case 1:
|
||||
codec.Name = core.CodecPCML
|
||||
case 6:
|
||||
codec.Name = core.CodecPCMA
|
||||
case 7:
|
||||
codec.Name = core.CodecPCMU
|
||||
}
|
||||
|
||||
codec.Channels = uint16(data[2])
|
||||
codec.ClockRate = binary.LittleEndian.Uint32(data[4:])
|
||||
}
|
||||
}
|
||||
|
||||
if codec.Name == "" {
|
||||
return nil, errors.New("waw: unsupported codec")
|
||||
}
|
||||
@@ -110,18 +81,3 @@ func (c *Producer) Start() error {
|
||||
ts += PacketSize
|
||||
}
|
||||
}
|
||||
|
||||
func readChunk(r io.Reader) (chunkID string, data []byte, err error) {
|
||||
b := make([]byte, 8)
|
||||
if _, err = io.ReadFull(r, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if chunkID = string(b[:4]); chunkID != "data" {
|
||||
size := binary.LittleEndian.Uint32(b[4:])
|
||||
data = make([]byte, size)
|
||||
_, err = io.ReadFull(r, data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
103
pkg/wav/wav.go
Normal file
103
pkg/wav/wav.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Header(codec *core.Codec) []byte {
|
||||
var fmt, size, extra byte
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecPCML:
|
||||
fmt = 1
|
||||
size = 2
|
||||
case core.CodecPCMA:
|
||||
fmt = 6
|
||||
size = 1
|
||||
extra = 2
|
||||
case core.CodecPCMU:
|
||||
fmt = 7
|
||||
size = 1
|
||||
extra = 2
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
channels := byte(codec.Channels)
|
||||
if channels == 0 {
|
||||
channels = 1
|
||||
}
|
||||
|
||||
b := make([]byte, 0, 46) // cap with extra
|
||||
b = append(b, "RIFF\xFF\xFF\xFF\xFFWAVEfmt "...)
|
||||
|
||||
b = append(b, 0x10+extra, 0, 0, 0)
|
||||
b = append(b, fmt, 0)
|
||||
b = append(b, channels, 0)
|
||||
b = binary.LittleEndian.AppendUint32(b, codec.ClockRate)
|
||||
b = binary.LittleEndian.AppendUint32(b, uint32(size*channels)*codec.ClockRate)
|
||||
b = append(b, size*channels, 0)
|
||||
b = append(b, size*8, 0)
|
||||
if extra > 0 {
|
||||
b = append(b, 0, 0) // ExtraParamSize (if PCM, then doesn't exist)
|
||||
}
|
||||
|
||||
b = append(b, "data\xFF\xFF\xFF\xFF"...)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func ReadHeader(r io.Reader) (*core.Codec, error) {
|
||||
// skip Master RIFF chunk
|
||||
if _, err := io.ReadFull(r, make([]byte, 12)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var codec core.Codec
|
||||
|
||||
for {
|
||||
chunkID, data, err := readChunk(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if chunkID == "data" {
|
||||
break
|
||||
}
|
||||
|
||||
if chunkID == "fmt " {
|
||||
// https://audiocoding.cc/articles/2008-05-22-wav-file-structure/wav_formats.txt
|
||||
switch data[0] {
|
||||
case 1:
|
||||
codec.Name = core.CodecPCML
|
||||
case 6:
|
||||
codec.Name = core.CodecPCMA
|
||||
case 7:
|
||||
codec.Name = core.CodecPCMU
|
||||
}
|
||||
|
||||
codec.Channels = data[2]
|
||||
codec.ClockRate = binary.LittleEndian.Uint32(data[4:])
|
||||
}
|
||||
}
|
||||
|
||||
return &codec, nil
|
||||
}
|
||||
|
||||
func readChunk(r io.Reader) (chunkID string, data []byte, err error) {
|
||||
b := make([]byte, 8)
|
||||
if _, err = io.ReadFull(r, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if chunkID = string(b[:4]); chunkID != "data" {
|
||||
size := binary.LittleEndian.Uint32(b[4:])
|
||||
data = make([]byte, size)
|
||||
_, err = io.ReadFull(r, data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -5,9 +5,9 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
|
||||
@@ -125,13 +125,20 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error
|
||||
networks = append(networks, ice.NetworkType(ntype))
|
||||
}
|
||||
|
||||
udpMux, _ = ice.NewMultiUDPMuxFromPort(
|
||||
var err error
|
||||
if udpMux, err = ice.NewMultiUDPMuxFromPort(
|
||||
port,
|
||||
ice.UDPMuxFromPortWithInterfaceFilter(interfaceFilter),
|
||||
ice.UDPMuxFromPortWithIPFilter(ipFilter),
|
||||
ice.UDPMuxFromPortWithNetworks(networks...),
|
||||
)
|
||||
} else if ln, err := net.ListenPacket("udp", address); err == nil {
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ln, err := net.ListenPacket("udp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})
|
||||
}
|
||||
s.SetICEUDPMux(udpMux)
|
||||
|
@@ -3,7 +3,7 @@ package webrtc
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (c *Conn) CreateOffer(medias []*core.Media) (string, error) {
|
||||
|
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
|
@@ -73,7 +73,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
codec.Name = core.CodecPCMA
|
||||
}
|
||||
codec.ClockRate = 8000
|
||||
sender.Handler = pcm.ResampleToG711(track.Codec, 8000, sender.Handler)
|
||||
sender.Handler = pcm.TranscodeHandler(codec, track.Codec, sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,10 +11,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/stun"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/stun/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) {
|
||||
|
@@ -2,7 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
@@ -21,7 +21,7 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: MimeType(codec),
|
||||
ClockRate: codec.ClockRate,
|
||||
Channels: codec.Channels,
|
||||
Channels: uint16(codec.Channels),
|
||||
},
|
||||
PayloadType: 0, // don't know if this necessary
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package webrtc
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (c *Conn) SetOffer(offer string) (err error) {
|
||||
@@ -65,7 +65,8 @@ transeivers:
|
||||
|
||||
switch tr.Direction() {
|
||||
case webrtc.RTPTransceiverDirectionSendrecv:
|
||||
_ = tr.Sender().Stop()
|
||||
_ = tr.Sender().Stop() // don't know if necessary
|
||||
_ = tr.SetSender(tr.Sender(), nil) // set direction to recvonly
|
||||
case webrtc.RTPTransceiverDirectionSendonly:
|
||||
_ = tr.Stop()
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Track struct {
|
||||
|
@@ -3,7 +3,7 @@ package webrtc
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func NewClient(tracker, share, pwd string, pc *pion.PeerConnection) (*webrtc.Conn, error) {
|
||||
|
14
pkg/wyoming/README.md
Normal file
14
pkg/wyoming/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## Default wake words
|
||||
|
||||
- alexa_v0.1
|
||||
- hey_jarvis_v0.1
|
||||
- hey_mycroft_v0.1
|
||||
- hey_rhasspy_v0.1
|
||||
- ok_nabu_v0.1
|
||||
|
||||
## Useful Links
|
||||
|
||||
- https://github.com/rhasspy/wyoming-satellite
|
||||
- https://github.com/rhasspy/wyoming-openwakeword
|
||||
- https://github.com/fwartner/home-assistant-wakewords-collection
|
||||
- https://github.com/esphome/micro-wake-word-models/tree/main?tab=readme-ov-file
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user