mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-05 00:12:48 +08:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
da92256910 | ||
![]() |
035b824645 | ||
![]() |
2a91c4625a | ||
![]() |
23dd5b450c | ||
![]() |
f617c148cd | ||
![]() |
b5f4c7f75b | ||
![]() |
d44efb84a0 | ||
![]() |
03968d2f2e | ||
![]() |
3c371e7046 | ||
![]() |
4656086985 | ||
![]() |
e78f9fa69d | ||
![]() |
2e8be342ef | ||
![]() |
5387e88fe3 | ||
![]() |
1746f55eda | ||
![]() |
4d53889519 | ||
![]() |
6d9d89bbe3 | ||
![]() |
c1923627c0 | ||
![]() |
95ca5f5fe1 | ||
![]() |
4bbd3a1cd2 | ||
![]() |
9c8a1d8b19 | ||
![]() |
53967fc72a | ||
![]() |
31f870e950 | ||
![]() |
c7d228daff | ||
![]() |
378f071e2c | ||
![]() |
75f61b38ac | ||
![]() |
bc770f1a85 | ||
![]() |
d276311fcf | ||
![]() |
1e14dc9ab2 | ||
![]() |
8dbaa4ba93 | ||
![]() |
f0893bd78b | ||
![]() |
6247746177 | ||
![]() |
a20de73ab2 | ||
![]() |
813c8b3b3d | ||
![]() |
63d9c6c2b7 | ||
![]() |
2610f15eb6 | ||
![]() |
9268acf1ca | ||
![]() |
55fdf1a647 | ||
![]() |
5fe07aeea0 | ||
![]() |
e8b22bca99 | ||
![]() |
5926c1deb9 | ||
![]() |
dd98edc48e | ||
![]() |
fb1cc7dfc2 | ||
![]() |
7626a09c1c | ||
![]() |
db85533e74 | ||
![]() |
5939c8acba | ||
![]() |
e985ad23a2 | ||
![]() |
7452eb5e05 | ||
![]() |
5f9788209d | ||
![]() |
c07ddb8309 |
@@ -40,7 +40,8 @@ FROM base
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
RUN apk add --no-cache tini ffmpeg bash curl jq
|
||||
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse
|
||||
|
||||
# Hardware Acceleration for Intel CPU (+50MB)
|
||||
ARG TARGETARCH
|
||||
|
124
README.md
124
README.md
@@ -42,6 +42,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Source: RTSP](#source-rtsp)
|
||||
* [Source: RTMP](#source-rtmp)
|
||||
* [Source: HTTP](#source-http)
|
||||
* [Source: ONVIF](#source-onvif)
|
||||
* [Source: FFmpeg](#source-ffmpeg)
|
||||
* [Source: FFmpeg Device](#source-ffmpeg-device)
|
||||
* [Source: Exec](#source-exec)
|
||||
@@ -156,10 +157,11 @@ Available source types:
|
||||
|
||||
- [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
|
||||
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
|
||||
- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
|
||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||
- [exec](#source-exec) - get media from external app output
|
||||
- [echo](#source-echo) - get stream link from bash or python
|
||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
|
||||
@@ -229,6 +231,8 @@ 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**.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# [HTTP-FLV] stream in video/x-flv format
|
||||
@@ -239,10 +243,26 @@ streams:
|
||||
|
||||
# [MJPEG] stream will be proxied without modification
|
||||
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
|
||||
|
||||
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
|
||||
tcp_magic: tcp://192.168.1.123:12345
|
||||
```
|
||||
|
||||
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
|
||||
|
||||
#### Source: ONVIF
|
||||
|
||||
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".
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua1: onvif://admin:password@192.168.1.123
|
||||
reolink1: onvif://admin:password@192.168.1.123:8000
|
||||
tapo1: onvif://admin:password@192.168.1.123:2020
|
||||
```
|
||||
|
||||
#### 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.
|
||||
@@ -273,7 +293,7 @@ streams:
|
||||
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||
```
|
||||
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/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.
|
||||
|
||||
@@ -301,25 +321,40 @@ Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2r
|
||||
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
|
||||
- `resolution` and `framerate` must be supported by your camera!
|
||||
- `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 important to set right framerate
|
||||
|
||||
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264
|
||||
linux_usbcam: ffmpeg:device?video=0&video_size=1280x720#video=h264
|
||||
windows_webcam: ffmpeg:device?video=0#video=h264
|
||||
macos_facetime: ffmpeg:device?video=0&audio=1&resolution=1280x720&framerate=30#video=h264#audio=pcma
|
||||
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
|
||||
```
|
||||
|
||||
#### Source: Exec
|
||||
|
||||
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
|
||||
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** 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.
|
||||
|
||||
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
|
||||
|
||||
The source can be used with:
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
|
||||
- any your own software
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||
picam_h264: exec:libcamera-vid -t 0 --inline -o -
|
||||
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
|
||||
```
|
||||
|
||||
#### Source: Echo
|
||||
@@ -803,8 +838,8 @@ Provides several features:
|
||||
|
||||
API examples:
|
||||
|
||||
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1`
|
||||
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1`
|
||||
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265)
|
||||
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC)
|
||||
|
||||
Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
@@ -895,7 +930,7 @@ But it cannot be done for [RTSP](#module-rtsp), [HTTP progressive streaming](#mo
|
||||
|
||||
Without filters:
|
||||
|
||||
- RTSP will provide only the first video and only the first audio
|
||||
- RTSP will provide only the first video and only the first audio (any codec)
|
||||
- MP4 will include only compatible codecs (H264, H265, AAC)
|
||||
- HLS will output in the legacy TS format (H264 without audio)
|
||||
|
||||
@@ -906,23 +941,25 @@ Some examples:
|
||||
- `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks
|
||||
- `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks
|
||||
- `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.mp4?src=camera1&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu` - MP4 file with non standard audio codecs, does not work in some players
|
||||
- `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
|
||||
|
||||
## 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.
|
||||
|
||||
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|
||||
|---------------------|-------------------------------|------------------------|-----------------------------------------|
|
||||
| *latency* | best | medium | bad |
|
||||
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
|
||||
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
|
||||
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
|
||||
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, OPUS | H264, AAC, OPUS |
|
||||
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, ?, AAC, OPUS, PCMU, PCMA, MP3 |
|
||||
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
|
||||
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
|
||||
| masOS Hass App | no | no | no |
|
||||
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|
||||
|---------------------|-------------------------------|-------------------------------|------------------------------------|
|
||||
| *latency* | best | medium | bad |
|
||||
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
|
||||
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
|
||||
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
|
||||
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS |
|
||||
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** |
|
||||
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** |
|
||||
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
|
||||
| masOS Hass App | no | no | no |
|
||||
|
||||
- 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/)
|
||||
@@ -931,9 +968,9 @@ Some examples:
|
||||
|
||||
**Audio**
|
||||
|
||||
- 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)
|
||||
- `PCMU` and `PCMA` inside **MP4** isn't a standard, but some players support them, for example Chromium browsers
|
||||
|
||||
**Apple devices**
|
||||
|
||||
@@ -941,6 +978,45 @@ Some examples:
|
||||
- iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
|
||||
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
|
||||
|
||||
**Codec names**
|
||||
|
||||
- H264 = H.264 = AVC (Advanced Video Coding)
|
||||
- H265 = H.265 = HEVC (High Efficiency Video Coding)
|
||||
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
|
||||
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
|
||||
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
|
||||
- AAC = MPEG4-GENERIC
|
||||
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
|
||||
|
||||
## Built-in transcoding
|
||||
|
||||
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.
|
||||
|
||||
**PCM for MSE/MP4/HLS**
|
||||
|
||||
Go2rtc can pack `PCMA`, `PCMU` and `PCM` codecs into an MP4 container so that they work in all browsers and all built-in players on modern devices. Including Apple QuickTime:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
PCM/xxx => PCMA/8000 => WebRTC
|
||||
PCMA/xxx => PCMA/8000 => WebRTC
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Starting go2rtc..." >&2
|
||||
|
||||
readonly config_path="/config"
|
||||
|
||||
if [[ -x "${config_path}/go2rtc" ]]; then
|
||||
readonly binary_path="${config_path}/go2rtc"
|
||||
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
|
||||
else
|
||||
readonly binary_path="/usr/local/bin/go2rtc"
|
||||
fi
|
||||
|
||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
||||
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
|
||||
|
||||
exec "${binary_path}"
|
@@ -1,4 +0,0 @@
|
||||
**Project layout**
|
||||
|
||||
- https://github.com/golang-standards/project-layout
|
||||
- https://github.com/micro/micro
|
@@ -1,61 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f avfoundation"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
audio := findMedia(core.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `"` + video.ID + `:` + audio.ID + `"`
|
||||
case video != nil:
|
||||
return `"` + video.ID + `"`
|
||||
case audio != nil:
|
||||
return `"` + audio.ID + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
var kind string
|
||||
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
process:
|
||||
for _, line := range lines {
|
||||
switch {
|
||||
case strings.HasSuffix(line, "video devices:"):
|
||||
kind = core.KindVideo
|
||||
continue
|
||||
case strings.HasSuffix(line, "audio devices:"):
|
||||
kind = core.KindAudio
|
||||
continue
|
||||
case strings.HasPrefix(line, "dummy"):
|
||||
break process
|
||||
}
|
||||
|
||||
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
|
||||
name := line[42:]
|
||||
media := loadMedia(kind, name)
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f v4l2"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
if video := findMedia(core.KindVideo, videoIdx); video != nil {
|
||||
return video.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
files, err := ioutil.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
log.Trace().Msg("[ffmpeg] " + file.Name())
|
||||
if strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
|
||||
if media != nil {
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
if !bytes.Contains(buf.Bytes(), []byte("Raw")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/DirectShow
|
||||
const deviceInputPrefix = "-f dshow"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
audio := findMedia(core.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `video="` + video.ID + `":audio=` + audio.ID + `"`
|
||||
case video != nil:
|
||||
return `video="` + video.ID + `"`
|
||||
case audio != nil:
|
||||
return `audio="` + audio.ID + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
lines := strings.Split(buf.String(), "\r\n")
|
||||
for _, line := range lines {
|
||||
var kind string
|
||||
if strings.HasSuffix(line, "(video)") {
|
||||
kind = core.KindVideo
|
||||
} else if strings.HasSuffix(line, "(audio)") {
|
||||
kind = core.KindAudio
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// hope we have constant prefix and suffix sizes
|
||||
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
|
||||
name := line[28 : len(line)-9]
|
||||
media := loadMedia(kind, name)
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
@@ -1,91 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
|
||||
api.HandleFunc("api/devices", handle)
|
||||
}
|
||||
|
||||
func GetInput(src string) (string, error) {
|
||||
if medias == nil {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
input := deviceInputPrefix
|
||||
|
||||
var videoIdx, audioIdx int
|
||||
if i := strings.IndexByte(src, '?'); i > 0 {
|
||||
query, err := url.ParseQuery(src[i+1:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for key, value := range query {
|
||||
switch key {
|
||||
case "video":
|
||||
videoIdx, _ = strconv.Atoi(value[0])
|
||||
case "audio":
|
||||
audioIdx, _ = strconv.Atoi(value[0])
|
||||
case "framerate":
|
||||
input += " -framerate " + value[0]
|
||||
case "resolution":
|
||||
input += " -video_size " + value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
var Bin string
|
||||
var log zerolog.Logger
|
||||
var medias []*core.Media
|
||||
|
||||
func findMedia(kind string, index int) *core.Media {
|
||||
for _, media := range medias {
|
||||
if media.Kind != kind {
|
||||
continue
|
||||
}
|
||||
if index == 0 {
|
||||
return media
|
||||
}
|
||||
index--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handle(w http.ResponseWriter, r *http.Request) {
|
||||
if medias == nil {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
var items []api.Stream
|
||||
var iv, ia int
|
||||
|
||||
for _, media := range medias {
|
||||
var source string
|
||||
switch media.Kind {
|
||||
case core.KindVideo:
|
||||
source = "ffmpeg:device?video=" + strconv.Itoa(iv)
|
||||
iv++
|
||||
case core.KindAudio:
|
||||
source = "ffmpeg:device?audio=" + strconv.Itoa(ia)
|
||||
ia++
|
||||
}
|
||||
items = append(items, api.Stream{Name: media.ID, URL: source})
|
||||
}
|
||||
|
||||
api.ResponseStreams(w, items)
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
EngineSoftware = "software"
|
||||
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
|
||||
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
|
||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||
EngineVideoToolbox = "videotoolbox" // macOS
|
||||
)
|
||||
|
||||
var cache = map[string]string{}
|
||||
|
||||
// MakeHardware converts software FFmpeg args to hardware args
|
||||
// empty engine for autoselect
|
||||
func MakeHardware(args *Args, engine string) {
|
||||
for i, codec := range args.codecs {
|
||||
if len(codec) < 12 {
|
||||
continue // skip short line (-c:v libx264...)
|
||||
}
|
||||
|
||||
// get current codec name
|
||||
name := cut(codec, ' ', 1)
|
||||
switch name {
|
||||
case "libx264":
|
||||
name = "h264"
|
||||
case "libx265":
|
||||
name = "h265"
|
||||
case "mjpeg":
|
||||
default:
|
||||
continue // skip unsupported codec
|
||||
}
|
||||
|
||||
// temporary disable probe for H265 and MJPEG
|
||||
if engine == "" && name == "h264" {
|
||||
if engine = cache[name]; engine == "" {
|
||||
engine = ProbeHardware(name)
|
||||
cache[name] = engine
|
||||
}
|
||||
}
|
||||
|
||||
switch engine {
|
||||
case EngineVAAPI:
|
||||
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_vaapi=" + filter[6:]
|
||||
}
|
||||
if strings.HasPrefix(filter, "transpose=") {
|
||||
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
|
||||
args.filters[i] = "transpose_vaapi=4" // reversal
|
||||
} else {
|
||||
args.filters[i] = "transpose_vaapi=" + filter[10:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix if input doesn't support hwaccel, do nothing when support
|
||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||
|
||||
case EngineCUDA:
|
||||
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_cuda=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
case EngineDXVA2:
|
||||
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_qsv=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
|
||||
|
||||
case EngineVideoToolbox:
|
||||
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
case EngineV4L2M2M:
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(arg ...string) bool {
|
||||
err := exec.Command(defaults["bin"], arg...).Run()
|
||||
log.Printf("%v %v", arg, err)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func cut(s string, sep byte, pos int) string {
|
||||
for n := 0; n < pos; n++ {
|
||||
if i := strings.IndexByte(s, sep); i > 0 {
|
||||
s = s[i+1:]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if i := strings.IndexByte(s, sep); i > 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
package ffmpeg
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_videotoolbox", "-f", "null", "-") {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_videotoolbox", "-f", "null", "-") {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_v4l2m2m", "-f", "null", "-") {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "h264":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "h264_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "hevc_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "mjpeg_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
package ffmpeg
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "mjpeg_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
23
cmd/go2rtc_rtsp/main.go
Normal file
23
cmd/go2rtc_rtsp/main.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Init()
|
||||
streams.Init()
|
||||
|
||||
rtsp.Init()
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigs
|
||||
|
||||
println("exit OK")
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("http", handle)
|
||||
streams.HandleFunc("https", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
// first we get the Content-Type to define supported producer
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
ct := res.Header.Get("Content-Type")
|
||||
if i := strings.IndexByte(ct, ';'); i > 0 {
|
||||
ct = ct[:i]
|
||||
}
|
||||
|
||||
switch ct {
|
||||
case "image/jpeg", "multipart/x-mixed-replace":
|
||||
return mjpeg.NewClient(res), nil
|
||||
|
||||
case "video/x-flv":
|
||||
var conn *rtmp.Client
|
||||
if conn, err = rtmp.Accept(res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
|
||||
case "video/mpeg":
|
||||
client := mpegts.NewClient(res)
|
||||
if err = client.Handle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("tcp", handle)
|
||||
}
|
||||
|
||||
func handle(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &http.Request{URL: u}
|
||||
res := &http.Response{Body: conn, Request: req}
|
||||
client := mpegts.NewClient(res)
|
||||
if err := client.Handle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
2
go.mod
2
go.mod
@@ -16,6 +16,8 @@ require (
|
||||
github.com/pion/stun v0.4.0
|
||||
github.com/pion/webrtc/v3 v3.1.58
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
4
go.sum
4
go.sum
@@ -106,6 +106,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/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=
|
||||
|
@@ -38,9 +38,13 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||
# libasound2-plugins for ALSA support
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
|
||||
apt-get -y update && apt-get -y install tini ffmpeg \
|
||||
python3 curl jq \
|
||||
intel-media-va-driver-non-free \
|
||||
libasound2-plugins
|
||||
|
||||
COPY --link --from=rootfs / /
|
||||
|
||||
|
11
internal/README.md
Normal file
11
internal/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## Go
|
||||
|
||||
```
|
||||
go mod why github.com/pion/rtcp
|
||||
go list -deps .\cmd\go2rtc_rtsp\
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://github.com/golang-standards/project-layout
|
||||
- https://github.com/micro/micro
|
@@ -2,7 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"net/http"
|
@@ -1,7 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"net/http"
|
@@ -16,7 +16,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var Version = "1.3.2"
|
||||
var Version = "1.5.0"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
@@ -1,8 +1,8 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
@@ -13,15 +13,15 @@ var stackSkip = [][]byte{
|
||||
[]byte("created by os/signal.Notify"),
|
||||
|
||||
// api/stack.go
|
||||
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
|
||||
[]byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"),
|
||||
|
||||
// api/api.go
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"),
|
||||
[]byte("created by net/http.(*connReader).startBackgroundRead"),
|
||||
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
||||
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
|
||||
|
||||
// webrtc/api.go
|
||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
@@ -1,7 +1,7 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||
)
|
@@ -2,8 +2,8 @@ package echo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"os/exec"
|
@@ -5,26 +5,21 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
// depends on RTSP server
|
||||
if rtsp.Port == "" {
|
||||
return
|
||||
}
|
||||
|
||||
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||
waitersMu.Lock()
|
||||
waiter := waiters[conn.URL.Path]
|
||||
@@ -43,30 +38,66 @@ func Init() {
|
||||
}
|
||||
})
|
||||
|
||||
streams.HandleFunc("exec", Handle)
|
||||
streams.HandleFunc("exec", execHandle)
|
||||
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
func Handle(url string) (core.Producer, error) {
|
||||
sum := md5.Sum([]byte(url))
|
||||
path := "/" + hex.EncodeToString(sum[:])
|
||||
func execHandle(url string) (core.Producer, error) {
|
||||
var path string
|
||||
|
||||
url = strings.Replace(
|
||||
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
|
||||
)
|
||||
args := shell.QuoteSplit(url[5:]) // remove `exec:`
|
||||
for i, arg := range args {
|
||||
if arg == "{output}" {
|
||||
if rtsp.Port == "" {
|
||||
return nil, errors.New("rtsp module disabled")
|
||||
}
|
||||
|
||||
// remove `exec:`
|
||||
args := shell.QuoteSplit(url[5:])
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
sum := md5.Sum([]byte(url))
|
||||
path = "/" + hex.EncodeToString(sum[:])
|
||||
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
if log.Debug().Enabled() {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return handlePipe(url, cmd)
|
||||
}
|
||||
|
||||
return handleRTSP(url, path, cmd)
|
||||
}
|
||||
|
||||
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
r, err := PipeCloser(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := magic.NewClient(r)
|
||||
if err = client.Probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.Desc = "exec active producer"
|
||||
client.URL = url
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
ch := make(chan core.Producer)
|
||||
|
||||
waitersMu.Lock()
|
26
internal/exec/pipe.go
Normal file
26
internal/exec/pipe.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
||||
func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pipeCloser{stdout, cmd}, nil
|
||||
}
|
||||
|
||||
type pipeCloser struct {
|
||||
io.ReadCloser
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (p pipeCloser) Close() error {
|
||||
return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
|
||||
}
|
68
internal/ffmpeg/device/device_darwin.go
Normal file
68
internal/ffmpeg/device/device_darwin.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f avfoundation"
|
||||
|
||||
func deviceInputSuffix(video, audio string) string {
|
||||
switch {
|
||||
case video != "" && audio != "":
|
||||
return `"` + video + `:` + audio + `"`
|
||||
case video != "":
|
||||
return `"` + video + `"`
|
||||
case audio != "":
|
||||
return `":` + audio + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func initDevices() {
|
||||
// [AVFoundation indev @ 0x147f04510] AVFoundation video devices:
|
||||
// [AVFoundation indev @ 0x147f04510] [0] FaceTime HD Camera
|
||||
// [AVFoundation indev @ 0x147f04510] [1] Capture screen 0
|
||||
// [AVFoundation indev @ 0x147f04510] AVFoundation audio devices:
|
||||
// [AVFoundation indev @ 0x147f04510] [0] MacBook Pro Microphone
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "",
|
||||
)
|
||||
b, _ := cmd.CombinedOutput()
|
||||
|
||||
re := regexp.MustCompile(`\[\d+] (.+)`)
|
||||
|
||||
var kind string
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
switch {
|
||||
case strings.HasSuffix(line, "video devices:"):
|
||||
kind = core.KindVideo
|
||||
continue
|
||||
case strings.HasSuffix(line, "audio devices:"):
|
||||
kind = core.KindAudio
|
||||
continue
|
||||
}
|
||||
|
||||
m := re.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := m[1]
|
||||
|
||||
switch kind {
|
||||
case core.KindVideo:
|
||||
videos = append(videos, name)
|
||||
case core.KindAudio:
|
||||
audios = append(audios, name)
|
||||
}
|
||||
|
||||
streams = append(streams, api.Stream{
|
||||
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||
})
|
||||
}
|
||||
}
|
60
internal/ffmpeg/device/device_linux.go
Normal file
60
internal/ffmpeg/device/device_linux.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f v4l2"
|
||||
|
||||
func deviceInputSuffix(video, audio string) string {
|
||||
if video != "" {
|
||||
return video
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func initDevices() {
|
||||
files, err := os.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := "/dev/" + file.Name()
|
||||
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
||||
)
|
||||
b, _ := cmd.CombinedOutput()
|
||||
|
||||
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: mjpeg : Motion-JPEG : 640x360 1280x720 1920x1080
|
||||
// [video4linux2,v4l2 @ 0x204e1c0] Raw : yuyv422 : YUYV 4:2:2 : 640x360 1280x720 1920x1080
|
||||
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: h264 : H.264 : 640x360 1280x720 1920x1080
|
||||
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
|
||||
m := re.FindAllStringSubmatch(string(b), -1)
|
||||
for _, i := range m {
|
||||
size, _, _ := strings.Cut(i[4], " ")
|
||||
stream := api.Stream{
|
||||
Name: i[3] + " | " + i[4],
|
||||
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
||||
}
|
||||
|
||||
if i[1] != "Compressed" {
|
||||
stream.URL += "#video=h264#hardware"
|
||||
}
|
||||
|
||||
videos = append(videos, name)
|
||||
streams = append(streams, stream)
|
||||
}
|
||||
}
|
||||
}
|
50
internal/ffmpeg/device/device_windows.go
Normal file
50
internal/ffmpeg/device/device_windows.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/DirectShow
|
||||
const deviceInputPrefix = "-f dshow"
|
||||
|
||||
func deviceInputSuffix(video, audio string) string {
|
||||
switch {
|
||||
case video != "" && audio != "":
|
||||
return `video="` + video + `":audio=` + audio + `"`
|
||||
case video != "":
|
||||
return `video="` + video + `"`
|
||||
case audio != "":
|
||||
return `audio="` + audio + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func initDevices() {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||
)
|
||||
b, _ := cmd.CombinedOutput()
|
||||
|
||||
re := regexp.MustCompile(`"([^"]+)" \((video|audio)\)`)
|
||||
for _, m := range re.FindAllStringSubmatch(string(b), -1) {
|
||||
name := m[1]
|
||||
kind := m[2]
|
||||
|
||||
stream := api.Stream{
|
||||
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case core.KindVideo:
|
||||
videos = append(videos, name)
|
||||
stream.URL += "#video=h264#hardware"
|
||||
case core.KindAudio:
|
||||
audios = append(audios, name)
|
||||
}
|
||||
|
||||
streams = append(streams, stream)
|
||||
}
|
||||
}
|
70
internal/ffmpeg/device/devices.go
Normal file
70
internal/ffmpeg/device/devices.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Init(bin string) {
|
||||
Bin = bin
|
||||
|
||||
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
||||
}
|
||||
|
||||
func GetInput(src string) (string, error) {
|
||||
runonce.Do(initDevices)
|
||||
|
||||
input := deviceInputPrefix
|
||||
|
||||
var video, audio string
|
||||
|
||||
if i := strings.IndexByte(src, '?'); i > 0 {
|
||||
query, err := url.ParseQuery(src[i+1:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for key, value := range query {
|
||||
switch key {
|
||||
case "video":
|
||||
video = value[0]
|
||||
case "audio":
|
||||
audio = value[0]
|
||||
case "resolution":
|
||||
input += " -video_size " + value[0]
|
||||
default: // "input_format", "framerate", "video_size"
|
||||
input += " -" + key + " " + value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if video != "" {
|
||||
if i, err := strconv.Atoi(video); err == nil && i < len(videos) {
|
||||
video = videos[i]
|
||||
}
|
||||
}
|
||||
if audio != "" {
|
||||
if i, err := strconv.Atoi(audio); err == nil && i < len(audios) {
|
||||
audio = audios[i]
|
||||
}
|
||||
}
|
||||
|
||||
input += " -i " + deviceInputSuffix(video, audio)
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
var Bin string
|
||||
|
||||
var videos, audios []string
|
||||
var streams []api.Stream
|
||||
var runonce sync.Once
|
||||
|
||||
func apiDevices(w http.ResponseWriter, r *http.Request) {
|
||||
runonce.Do(initDevices)
|
||||
|
||||
api.ResponseStreams(w, streams)
|
||||
}
|
@@ -1,16 +1,15 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -32,11 +31,11 @@ func Init() {
|
||||
if args == nil {
|
||||
return nil, errors.New("can't generate ffmpeg command")
|
||||
}
|
||||
return exec.Handle("exec:" + args.String())
|
||||
return streams.GetProducer("exec:" + args.String())
|
||||
})
|
||||
|
||||
device.Bin = defaults["bin"]
|
||||
device.Init()
|
||||
device.Init(defaults["bin"])
|
||||
hardware.Init(defaults["bin"])
|
||||
}
|
||||
|
||||
var defaults = map[string]string{
|
||||
@@ -51,16 +50,19 @@ var defaults = map[string]string{
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -bufsize 8192k -f rtsp {output}",
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||
"output/mjpeg": "-f mjpeg -",
|
||||
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
|
||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||
@@ -70,8 +72,7 @@ var defaults = map[string]string{
|
||||
"aac": "-c:a aac", // keep sample rate and channels
|
||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||
"mp3": "-c:a libmp3lame -q:a 8",
|
||||
"pcm": "-c:a pcm_s16be",
|
||||
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||
|
||||
@@ -116,19 +117,19 @@ func inputTemplate(name, s string, query url.Values) string {
|
||||
return strings.Replace(template, "{input}", s, 1)
|
||||
}
|
||||
|
||||
func parseArgs(s string) *Args {
|
||||
func parseArgs(s string) *ffmpeg.Args {
|
||||
// init FFmpeg arguments
|
||||
args := &Args{
|
||||
bin: defaults["bin"],
|
||||
global: defaults["global"],
|
||||
output: defaults["output"],
|
||||
args := &ffmpeg.Args{
|
||||
Bin: defaults["bin"],
|
||||
Global: defaults["global"],
|
||||
Output: defaults["output"],
|
||||
}
|
||||
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||
query = parseQuery(s[i+1:])
|
||||
args.video = len(query["video"])
|
||||
args.audio = len(query["audio"])
|
||||
args.Video = len(query["video"])
|
||||
args.Audio = len(query["audio"])
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
@@ -139,46 +140,46 @@ func parseArgs(s string) *Args {
|
||||
if i := strings.Index(s, "://"); i > 0 {
|
||||
switch s[:i] {
|
||||
case "http", "https", "rtmp":
|
||||
args.input = inputTemplate("http", s, query)
|
||||
args.Input = inputTemplate("http", s, query)
|
||||
case "rtsp", "rtsps":
|
||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
||||
// skip unnecessary input tracks
|
||||
switch {
|
||||
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
|
||||
args.input = "-allowed_media_types video+audio "
|
||||
case args.video > 0:
|
||||
args.input = "-allowed_media_types video "
|
||||
case args.audio > 0:
|
||||
args.input = "-allowed_media_types audio "
|
||||
case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0):
|
||||
args.Input = "-allowed_media_types video+audio "
|
||||
case args.Video > 0:
|
||||
args.Input = "-allowed_media_types video "
|
||||
case args.Audio > 0:
|
||||
args.Input = "-allowed_media_types audio "
|
||||
}
|
||||
|
||||
args.input += inputTemplate("rtsp", s, query)
|
||||
args.Input += inputTemplate("rtsp", s, query)
|
||||
default:
|
||||
args.input = "-i " + s
|
||||
args.Input = "-i " + s
|
||||
}
|
||||
} else if streams.Get(s) != nil {
|
||||
s = "rtsp://localhost:" + rtsp.Port + "/" + s
|
||||
s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
|
||||
switch {
|
||||
case args.video > 0 && args.audio == 0:
|
||||
case args.Video > 0 && args.Audio == 0:
|
||||
s += "?video"
|
||||
case args.audio > 0 && args.video == 0:
|
||||
case args.Audio > 0 && args.Video == 0:
|
||||
s += "?audio"
|
||||
default:
|
||||
s += "?video&audio"
|
||||
}
|
||||
args.input = inputTemplate("rtsp", s, query)
|
||||
args.Input = inputTemplate("rtsp", s, query)
|
||||
} else if strings.HasPrefix(s, "device?") {
|
||||
var err error
|
||||
args.input, err = device.GetInput(s)
|
||||
args.Input, err = device.GetInput(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
args.input = inputTemplate("file", s, query)
|
||||
args.Input = inputTemplate("file", s, query)
|
||||
}
|
||||
|
||||
if query["async"] != nil {
|
||||
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
|
||||
args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
|
||||
}
|
||||
|
||||
// Parse query params:
|
||||
@@ -226,7 +227,7 @@ func parseArgs(s string) *Args {
|
||||
}
|
||||
|
||||
// 3. Process video codecs
|
||||
if args.video > 0 {
|
||||
if args.Video > 0 {
|
||||
for _, video := range query["video"] {
|
||||
if video != "copy" {
|
||||
if codec := defaults[video]; codec != "" {
|
||||
@@ -243,7 +244,7 @@ func parseArgs(s string) *Args {
|
||||
}
|
||||
|
||||
// 4. Process audio codecs
|
||||
if args.audio > 0 {
|
||||
if args.Audio > 0 {
|
||||
for _, audio := range query["audio"] {
|
||||
if audio != "copy" {
|
||||
if codec := defaults[audio]; codec != "" {
|
||||
@@ -260,14 +261,21 @@ func parseArgs(s string) *Args {
|
||||
}
|
||||
|
||||
if query["hardware"] != nil {
|
||||
MakeHardware(args, query["hardware"][0])
|
||||
hardware.MakeHardware(args, query["hardware"][0], defaults)
|
||||
}
|
||||
}
|
||||
|
||||
if args.codecs == nil {
|
||||
if args.Codecs == nil {
|
||||
args.AddCodec("-c copy")
|
||||
}
|
||||
|
||||
// transcoding to only mjpeg
|
||||
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
|
||||
// no transcoding from mjpeg input
|
||||
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
|
||||
args.Output = defaults["output/mjpeg"]
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -283,76 +291,3 @@ func parseQuery(s string) map[string][]string {
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
bin string // ffmpeg
|
||||
global string // -hide_banner -v error
|
||||
input string // -re -stream_loop -1 -i /media/bunny.mp4
|
||||
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
||||
filters []string // scale=1920:1080
|
||||
output string // -f rtsp {output}
|
||||
|
||||
video, audio int // count of video and audio params
|
||||
}
|
||||
|
||||
func (a *Args) AddCodec(codec string) {
|
||||
a.codecs = append(a.codecs, codec)
|
||||
}
|
||||
|
||||
func (a *Args) AddFilter(filter string) {
|
||||
a.filters = append(a.filters, filter)
|
||||
}
|
||||
|
||||
func (a *Args) InsertFilter(filter string) {
|
||||
a.filters = append([]string{filter}, a.filters...)
|
||||
}
|
||||
|
||||
func (a *Args) String() string {
|
||||
b := bytes.NewBuffer(make([]byte, 0, 512))
|
||||
|
||||
b.WriteString(a.bin)
|
||||
|
||||
if a.global != "" {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.global)
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.input)
|
||||
|
||||
multimode := a.video > 1 || a.audio > 1
|
||||
var iv, ia int
|
||||
|
||||
for _, codec := range a.codecs {
|
||||
// support multiple video and/or audio codecs
|
||||
if multimode && len(codec) >= 5 {
|
||||
switch codec[:5] {
|
||||
case "-c:v ":
|
||||
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
|
||||
iv++
|
||||
case "-c:a ":
|
||||
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
|
||||
ia++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(codec)
|
||||
}
|
||||
|
||||
if a.filters != nil {
|
||||
for i, filter := range a.filters {
|
||||
if i == 0 {
|
||||
b.WriteString(" -vf ")
|
||||
} else {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(filter)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.output)
|
||||
|
||||
return b.String()
|
||||
}
|
@@ -7,8 +7,17 @@ import (
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
|
||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
|
||||
args = parseArgs("/media/bbb.mp4#video=mjpeg")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -", args.String())
|
||||
|
||||
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf format=vaapi|nv12,hwupload -f mjpeg -", args.String())
|
||||
|
||||
args = parseArgs("device?video=0&input_format=mjpeg&video_size=1920x1080")
|
||||
assert.Equal(t, `ffmpeg -hide_banner -f dshow -input_format mjpeg -video_size 1920x1080 -i video="0" -c copy -f mjpeg -`, args.String())
|
||||
}
|
137
internal/ffmpeg/hardware/hardware.go
Normal file
137
internal/ffmpeg/hardware/hardware.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
EngineSoftware = "software"
|
||||
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
|
||||
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
|
||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||
EngineVideoToolbox = "videotoolbox" // macOS
|
||||
)
|
||||
|
||||
func Init(bin string) {
|
||||
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
|
||||
api.ResponseStreams(w, ProbeAll(bin))
|
||||
})
|
||||
}
|
||||
|
||||
// MakeHardware converts software FFmpeg args to hardware args
|
||||
// empty engine for autoselect
|
||||
func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) {
|
||||
for i, codec := range args.Codecs {
|
||||
if len(codec) < 10 {
|
||||
continue // skip short line (-c:v mjpeg...)
|
||||
}
|
||||
|
||||
// get current codec name
|
||||
name := cut(codec, ' ', 1)
|
||||
switch name {
|
||||
case "libx264":
|
||||
name = "h264"
|
||||
case "libx265":
|
||||
name = "h265"
|
||||
case "mjpeg":
|
||||
default:
|
||||
continue // skip unsupported codec
|
||||
}
|
||||
|
||||
// temporary disable probe for H265
|
||||
if engine == "" && name != "h265" {
|
||||
if engine = cache[name]; engine == "" {
|
||||
engine = ProbeHardware(args.Bin, name)
|
||||
cache[name] = engine
|
||||
}
|
||||
}
|
||||
|
||||
switch engine {
|
||||
case EngineVAAPI:
|
||||
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters[i] = "scale_vaapi=" + filter[6:]
|
||||
}
|
||||
if strings.HasPrefix(filter, "transpose=") {
|
||||
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
|
||||
args.Filters[i] = "transpose_vaapi=4" // reversal
|
||||
} else {
|
||||
args.Filters[i] = "transpose_vaapi=" + filter[10:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix if input doesn't support hwaccel, do nothing when support
|
||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||
|
||||
case EngineCUDA:
|
||||
args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters[i] = "scale_cuda=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
case EngineDXVA2:
|
||||
args.Input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.Input
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters[i] = "scale_qsv=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
|
||||
|
||||
case EngineVideoToolbox:
|
||||
args.Input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.Input
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
case EngineV4L2M2M:
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cache = map[string]string{}
|
||||
|
||||
func run(bin string, args string) bool {
|
||||
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
||||
log.Printf("%v %v", args, err)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func runToString(bin string, args string) string {
|
||||
if run(bin, args) {
|
||||
return "OK"
|
||||
} else {
|
||||
return "ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
func cut(s string, sep byte, pos int) string {
|
||||
for n := 0; n < pos; n++ {
|
||||
if i := strings.IndexByte(s, sep); i > 0 {
|
||||
s = s[i+1:]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if i := strings.IndexByte(s, sep); i > 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
37
internal/ffmpeg/hardware/hardware_darwin.go
Normal file
37
internal/ffmpeg/hardware/hardware_darwin.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
)
|
||||
|
||||
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -"
|
||||
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -"
|
||||
|
||||
func ProbeAll(bin string) []api.Stream {
|
||||
return []api.Stream{
|
||||
{
|
||||
Name: runToString(bin, ProbeVideoToolboxH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeVideoToolboxH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ProbeHardware(bin, name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(bin, ProbeVideoToolboxH264) {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(bin, ProbeVideoToolboxH265) {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
94
internal/ffmpeg/hardware/hardware_linux.go
Normal file
94
internal/ffmpeg/hardware/hardware_linux.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
||||
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
||||
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||
|
||||
func ProbeAll(bin string) []api.Stream {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
return []api.Stream{
|
||||
{
|
||||
Name: runToString(bin, ProbeV4L2M2MH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeV4L2M2MH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []api.Stream{
|
||||
{
|
||||
Name: runToString(bin, ProbeVAAPIH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeVAAPIH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineVAAPI,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeVAAPIJPEG),
|
||||
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineVAAPI,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeCUDAH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeCUDAH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ProbeHardware(bin, name string) string {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(bin, ProbeV4L2M2MH264) {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
case "h265":
|
||||
if run(bin, ProbeV4L2M2MH265) {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(bin, ProbeCUDAH264) {
|
||||
return EngineCUDA
|
||||
}
|
||||
if run(bin, ProbeVAAPIH264) {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(bin, ProbeCUDAH265) {
|
||||
return EngineCUDA
|
||||
}
|
||||
if run(bin, ProbeVAAPIH265) {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run(bin, ProbeVAAPIJPEG) {
|
||||
return EngineVAAPI
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
61
internal/ffmpeg/hardware/hardware_windows.go
Normal file
61
internal/ffmpeg/hardware/hardware_windows.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package hardware
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/internal/api"
|
||||
|
||||
const ProbeDXVA2H264 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -"
|
||||
const ProbeDXVA2H265 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -"
|
||||
const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -"
|
||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||
|
||||
func ProbeAll(bin string) []api.Stream {
|
||||
return []api.Stream{
|
||||
{
|
||||
Name: runToString(bin, ProbeDXVA2H264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeDXVA2H265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineDXVA2,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeDXVA2JPEG),
|
||||
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineDXVA2,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeCUDAH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeCUDAH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ProbeHardware(bin, name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(bin, ProbeCUDAH264) {
|
||||
return EngineCUDA
|
||||
}
|
||||
if run(bin, ProbeDXVA2H264) {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(bin, ProbeCUDAH265) {
|
||||
return EngineCUDA
|
||||
}
|
||||
if run(bin, ProbeDXVA2H265) {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run(bin, ProbeDXVA2JPEG) {
|
||||
return EngineDXVA2
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
12
internal/ffmpeg/helpers.go
Normal file
12
internal/ffmpeg/helpers.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func TranscodeToJPEG(b []byte) ([]byte, error) {
|
||||
cmd := exec.Command("ffmpeg", "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
|
||||
cmd.Stdin = bytes.NewBuffer(b)
|
||||
return cmd.Output()
|
||||
}
|
@@ -3,9 +3,9 @@ package hass
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
@@ -4,10 +4,10 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
@@ -131,6 +131,25 @@ func importEntries(config string) map[string]string {
|
||||
case "roborock":
|
||||
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
|
||||
|
||||
case "onvif":
|
||||
var data struct {
|
||||
Host string `json:"host" json:"host"`
|
||||
Port uint16 `json:"port" json:"port"`
|
||||
Username string `json:"username" json:"username"`
|
||||
Password string `json:"password" json:"password"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Data, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if data.Username != "" && data.Password != "" {
|
||||
urls[entrie.Title] = fmt.Sprintf(
|
||||
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
|
||||
)
|
||||
} else {
|
||||
urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
|
||||
}
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
@@ -2,14 +2,15 @@ package hls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -48,6 +49,9 @@ const keepalive = 5 * time.Second
|
||||
|
||||
var sessions = map[string]*Session{}
|
||||
|
||||
// once I saw 404 on MP4 segment, so better to use mutex
|
||||
var sessionsMu sync.RWMutex
|
||||
|
||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
// CORS important for Chromecast
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
@@ -128,11 +132,16 @@ segment.ts?id=` + sid + `&n=%d
|
||||
segment.ts?id=` + sid + `&n=%d`
|
||||
}
|
||||
|
||||
sessionsMu.Lock()
|
||||
sessions[sid] = session
|
||||
sessionsMu.Unlock()
|
||||
|
||||
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
|
||||
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
|
||||
|
||||
// bandwidth important for Safari, codecs useful for smooth playback
|
||||
data := []byte(`#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
|
||||
hls/playlist.m3u8?id=` + sid)
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
@@ -150,7 +159,9 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -173,7 +184,9 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -212,7 +225,9 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -233,7 +248,9 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
@@ -3,8 +3,8 @@ package homekit
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
||||
"net/http"
|
@@ -1,10 +1,10 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/rs/zerolog"
|
95
internal/http/http.go
Normal file
95
internal/http/http.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("http", handleHTTP)
|
||||
streams.HandleFunc("https", handleHTTP)
|
||||
streams.HandleFunc("httpx", handleHTTP)
|
||||
|
||||
streams.HandleFunc("tcp", handleTCP)
|
||||
}
|
||||
|
||||
func handleHTTP(url string) (core.Producer, error) {
|
||||
// first we get the Content-Type to define supported producer
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
ct := res.Header.Get("Content-Type")
|
||||
if i := strings.IndexByte(ct, ';'); i > 0 {
|
||||
ct = ct[:i]
|
||||
}
|
||||
|
||||
switch ct {
|
||||
case "image/jpeg", "multipart/x-mixed-replace":
|
||||
return mjpeg.NewClient(res), nil
|
||||
|
||||
case "video/x-flv":
|
||||
var conn *rtmp.Client
|
||||
if conn, err = rtmp.Accept(res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
|
||||
default: // "video/mpeg":
|
||||
}
|
||||
|
||||
client := magic.NewClient(res.Body)
|
||||
if err = client.Probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.Desc = "HTTP active producer"
|
||||
client.URL = url
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func handleTCP(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := magic.NewClient(conn)
|
||||
if err = client.Probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.Desc = "TCP active producer"
|
||||
client.URL = rawURL
|
||||
|
||||
return client, nil
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
package isapi
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/isapi"
|
||||
)
|
@@ -1,7 +1,7 @@
|
||||
package ivideon
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
"strings"
|
@@ -2,14 +2,18 @@ package mjpeg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -29,14 +33,16 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
exit := make(chan []byte)
|
||||
|
||||
cons := &mjpeg.Consumer{
|
||||
cons := &magic.Keyframe{
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
exit <- msg
|
||||
if b, ok := msg.([]byte); ok {
|
||||
select {
|
||||
case exit <- b:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,6 +55,17 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
switch cons.CodecName() {
|
||||
case core.CodecH264, core.CodecH265:
|
||||
ts := time.Now()
|
||||
var err error
|
||||
if data, err = ffmpeg.TranscodeToJPEG(data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "image/jpeg")
|
||||
h.Set("Content-Length", strconv.Itoa(len(data)))
|
@@ -4,13 +4,11 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -61,6 +59,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,17 +105,15 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
cons := &mp4.Consumer{
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Medias: core.ParseQuery(r.URL.Query()),
|
||||
Medias: mp4.ParseQuery(r.URL.Query()),
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
cons.Listen(func(msg any) {
|
||||
if exit == nil {
|
||||
return
|
||||
}
|
||||
if data, ok := msg.([]byte); ok {
|
||||
mu.Lock()
|
||||
_, err := w.Write(data)
|
||||
mu.Unlock()
|
||||
|
||||
if err != nil && exit != nil {
|
||||
if _, err := w.Write(data); err != nil {
|
||||
select {
|
||||
case exit <- err:
|
||||
default:
|
||||
@@ -128,6 +125,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,11 +136,13 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -153,7 +153,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
if i, _ := strconv.Atoi(s); i > 0 {
|
||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||
if exit != nil {
|
||||
exit <- nil
|
||||
select {
|
||||
case exit <- nil:
|
||||
default:
|
||||
}
|
||||
exit = nil
|
||||
}
|
||||
})
|
||||
@@ -161,6 +164,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
err = <-exit
|
||||
exit = nil
|
||||
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
|
@@ -2,8 +2,8 @@ package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
@@ -110,6 +110,12 @@ func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
|
||||
case mp4.MimeAAC:
|
||||
codec := &core.Codec{Name: core.CodecAAC}
|
||||
audios = append(audios, codec)
|
||||
case mp4.MimeFlac:
|
||||
audios = append(audios,
|
||||
&core.Codec{Name: core.CodecPCMA},
|
||||
&core.Codec{Name: core.CodecPCMU},
|
||||
&core.Codec{Name: core.CodecPCM},
|
||||
)
|
||||
case mp4.MimeOpus:
|
||||
codec := &core.Codec{Name: core.CodecOpus}
|
||||
audios = append(audios, codec)
|
@@ -1,8 +1,8 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"net/http"
|
||||
)
|
@@ -2,8 +2,8 @@ package ngrok
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ngrok"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
194
internal/onvif/init.go
Normal file
194
internal/onvif/init.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/onvif"
|
||||
"github.com/rs/zerolog"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log = app.GetLogger("onvif")
|
||||
|
||||
streams.HandleFunc("onvif", streamOnvif)
|
||||
|
||||
// ONVIF server on all suburls
|
||||
api.HandleFunc("/onvif/", onvifDeviceService)
|
||||
|
||||
// ONVIF client autodiscovery
|
||||
api.HandleFunc("api/onvif", apiOnvif)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamOnvif(rawURL string) (core.Producer, error) {
|
||||
client, err := onvif.NewClient(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, err := client.GetURI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[onvif] new uri=%s", uri)
|
||||
|
||||
return streams.GetProducer(uri)
|
||||
}
|
||||
|
||||
func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
action := onvif.GetRequestAction(b)
|
||||
if action == "" {
|
||||
http.Error(w, "malformed request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] %s", action)
|
||||
|
||||
var res string
|
||||
|
||||
switch action {
|
||||
case onvif.ActionGetCapabilities:
|
||||
// important for Hass: Media section
|
||||
res = onvif.GetCapabilitiesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetSystemDateAndTime:
|
||||
// important for Hass
|
||||
res = onvif.GetSystemDateAndTimeResponse()
|
||||
|
||||
case onvif.ActionGetNetworkInterfaces:
|
||||
// important for Hass: none
|
||||
res = onvif.GetNetworkInterfacesResponse()
|
||||
|
||||
case onvif.ActionGetDeviceInformation:
|
||||
// important for Hass: SerialNumber (unique server ID)
|
||||
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
|
||||
case onvif.ActionGetServiceCapabilities:
|
||||
// important for Hass
|
||||
res = onvif.GetServiceCapabilitiesResponse()
|
||||
|
||||
case onvif.ActionSystemReboot:
|
||||
res = onvif.SystemRebootResponse()
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
case onvif.ActionGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
res = onvif.GetProfilesResponse(streams.GetAll())
|
||||
|
||||
case onvif.ActionGetStreamUri:
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
|
||||
res = onvif.GetStreamUriResponse(uri)
|
||||
|
||||
default:
|
||||
http.Error(w, "unsupported action", http.StatusBadRequest)
|
||||
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
||||
if _, err = w.Write([]byte(res)); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
|
||||
func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
|
||||
var items []api.Stream
|
||||
|
||||
if src == "" {
|
||||
urls, err := onvif.DiscoveryStreamingURLs()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, rawURL := range urls {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Warn().Str("url", rawURL).Msg("[onvif] broken")
|
||||
continue
|
||||
}
|
||||
|
||||
if u.Scheme != "http" {
|
||||
log.Warn().Str("url", rawURL).Msg("[onvif] unsupported")
|
||||
continue
|
||||
}
|
||||
|
||||
u.Scheme = "onvif"
|
||||
u.User = url.UserPassword("user", "pass")
|
||||
|
||||
if u.Path == onvif.PathDevice {
|
||||
u.Path = ""
|
||||
}
|
||||
|
||||
items = append(items, api.Stream{Name: u.Host, URL: u.String()})
|
||||
}
|
||||
} else {
|
||||
client, err := onvif.NewClient(src)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if l := log.Trace(); l.Enabled() {
|
||||
b, _ := client.GetProfiles()
|
||||
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
|
||||
}
|
||||
|
||||
name, err := client.GetName()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := client.GetProfilesTokens()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for i, token := range tokens {
|
||||
items = append(items, api.Stream{
|
||||
Name: name + " stream" + strconv.Itoa(i),
|
||||
URL: src + "?subtype=" + token,
|
||||
})
|
||||
}
|
||||
|
||||
if len(tokens) > 0 && client.HasSnapshots() {
|
||||
items = append(items, api.Stream{
|
||||
Name: name + " snapshot",
|
||||
URL: src + "?subtype=" + tokens[0] + "&snapshot",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
api.ResponseStreams(w, items)
|
||||
}
|
@@ -2,8 +2,8 @@ package roborock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock"
|
||||
"net/http"
|
@@ -1,8 +1,8 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/rs/zerolog/log"
|
@@ -6,10 +6,9 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -22,6 +21,7 @@ func Init() {
|
||||
Username string `yaml:"username" json:"-"`
|
||||
Password string `yaml:"password" json:"-"`
|
||||
DefaultQuery string `yaml:"default_query" json:"default_query"`
|
||||
PacketSize uint16 `yaml:"pkt_size"`
|
||||
} `yaml:"rtsp"`
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func Init() {
|
||||
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
||||
|
||||
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
|
||||
defaultMedias = mp4.ParseQuery(query)
|
||||
defaultMedias = ParseQuery(query)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -67,6 +67,7 @@ func Init() {
|
||||
}
|
||||
|
||||
c := rtsp.NewServer(conn)
|
||||
c.PacketSize = conf.Mod.PacketSize
|
||||
// skip check auth for localhost
|
||||
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
|
||||
c.Auth(conf.Mod.Username, conf.Mod.Password)
|
||||
@@ -174,13 +175,18 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
|
||||
conn.SessionName = app.UserAgent
|
||||
|
||||
conn.Medias = mp4.ParseQuery(conn.URL.Query())
|
||||
query := conn.URL.Query()
|
||||
conn.Medias = ParseQuery(query)
|
||||
if conn.Medias == nil {
|
||||
for _, media := range defaultMedias {
|
||||
conn.Medias = append(conn.Medias, media.Clone())
|
||||
}
|
||||
}
|
||||
|
||||
if s := query.Get("pkt_size"); s != "" {
|
||||
conn.PacketSize = uint16(core.Atoi(s))
|
||||
}
|
||||
|
||||
if err := stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
return
|
||||
@@ -242,3 +248,27 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
func ParseQuery(query map[string][]string) []*core.Media {
|
||||
if v := query["mp4"]; v != nil {
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return core.ParseQuery(query)
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
package srtp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"net"
|
||||
)
|
@@ -2,9 +2,9 @@ package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -68,6 +68,13 @@ func GetOrNew(src string) *Stream {
|
||||
return New(src, src)
|
||||
}
|
||||
|
||||
func GetAll() (names []string) {
|
||||
for name := range streams {
|
||||
names = append(names, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
@@ -56,6 +56,10 @@ func (p *Producer) GetMedias() []*core.Media {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.conn.GetMedias()
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package tapo
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tapo"
|
||||
)
|
@@ -1,7 +1,7 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
@@ -2,7 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
@@ -2,9 +2,9 @@ package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
@@ -2,7 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
@@ -3,10 +3,10 @@ package webtorrent
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
||||
"github.com/rs/zerolog"
|
54
main.go
54
main.go
@@ -1,32 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/debug"
|
||||
"github.com/AlexxIT/go2rtc/cmd/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/cmd/echo"
|
||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||
"github.com/AlexxIT/go2rtc/cmd/hls"
|
||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||
"github.com/AlexxIT/go2rtc/cmd/http"
|
||||
"github.com/AlexxIT/go2rtc/cmd/isapi"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/tapo"
|
||||
"github.com/AlexxIT/go2rtc/cmd/tcp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webtorrent"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/debug"
|
||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/hass"
|
||||
"github.com/AlexxIT/go2rtc/internal/hls"
|
||||
"github.com/AlexxIT/go2rtc/internal/homekit"
|
||||
"github.com/AlexxIT/go2rtc/internal/http"
|
||||
"github.com/AlexxIT/go2rtc/internal/isapi"
|
||||
"github.com/AlexxIT/go2rtc/internal/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/internal/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/mp4"
|
||||
"github.com/AlexxIT/go2rtc/internal/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/internal/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/internal/onvif"
|
||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/tapo"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/webtorrent"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -36,6 +36,7 @@ func main() {
|
||||
app.Init() // init config and logs
|
||||
api.Init() // init HTTP API server
|
||||
streams.Init() // load streams list
|
||||
onvif.Init()
|
||||
|
||||
rtsp.Init() // add support RTSP client and RTSP server
|
||||
rtmp.Init() // add support RTMP client
|
||||
@@ -50,7 +51,6 @@ func main() {
|
||||
isapi.Init()
|
||||
mpegts.Init()
|
||||
roborock.Init()
|
||||
tcp.Init()
|
||||
|
||||
srtp.Init()
|
||||
homekit.Init()
|
||||
|
@@ -68,7 +68,7 @@ func (c *Codec) Match(remote *Codec) bool {
|
||||
}
|
||||
|
||||
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
c := &Codec{PayloadType: byte(atoi(payloadType))}
|
||||
c := &Codec{PayloadType: byte(Atoi(payloadType))}
|
||||
|
||||
for _, attr := range md.Attributes {
|
||||
switch {
|
||||
@@ -78,7 +78,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
|
||||
c.Name = strings.ToUpper(ss[0])
|
||||
// fix tailing space: `a=rtpmap:96 H264/90000 `
|
||||
c.ClockRate = uint32(atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
|
||||
c.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
|
||||
|
||||
if len(ss) == 3 && ss[2] == "2" {
|
||||
c.Channels = 2
|
||||
@@ -99,9 +99,16 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
case "8":
|
||||
c.Name = CodecPCMA
|
||||
c.ClockRate = 8000
|
||||
case "10":
|
||||
c.Name = CodecPCM
|
||||
c.ClockRate = 44100
|
||||
c.Channels = 2
|
||||
case "11":
|
||||
c.Name = CodecPCM
|
||||
c.ClockRate = 44100
|
||||
case "14":
|
||||
c.Name = CodecMP3
|
||||
c.ClockRate = 44100
|
||||
c.ClockRate = 90000 // it's not real sample rate
|
||||
case "26":
|
||||
c.Name = CodecJPEG
|
||||
c.ClockRate = 90000
|
||||
@@ -113,11 +120,6 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
return c
|
||||
}
|
||||
|
||||
func atoi(s string) (i int) {
|
||||
i, _ = strconv.Atoi(s)
|
||||
return
|
||||
}
|
||||
|
||||
func DecodeH264(fmtp string) string {
|
||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||
|
@@ -27,7 +27,8 @@ const (
|
||||
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
|
||||
CodecPCM = "L16" // Linear PCM
|
||||
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
CodecFLAC = "FLAC"
|
||||
|
||||
CodecAll = "ALL"
|
||||
CodecAny = "ANY"
|
||||
|
@@ -6,8 +6,15 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
|
||||
// same as: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
|
||||
func Now90000() uint32 {
|
||||
return uint32(time.Duration(time.Now().UnixMilli()) * 90)
|
||||
}
|
||||
|
||||
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
||||
|
||||
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
|
||||
@@ -22,6 +29,15 @@ func RandString(size, base byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Any(errs ...error) error {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
@@ -41,6 +57,11 @@ func Between(s, sub1, sub2 string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func Atoi(s string) (i int) {
|
||||
i, _ = strconv.Atoi(s)
|
||||
return
|
||||
}
|
||||
|
||||
func Assert(ok bool) {
|
||||
if !ok {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
|
@@ -93,7 +93,7 @@ func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC:
|
||||
return KindAudio
|
||||
}
|
||||
return ""
|
||||
@@ -136,6 +136,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
if media.ID != "" {
|
||||
md.WithValueAttribute("control", media.ID)
|
||||
}
|
||||
|
||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||
}
|
||||
|
||||
|
80
pkg/ffmpeg/ffmpeg.go
Normal file
80
pkg/ffmpeg/ffmpeg.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
Bin string // ffmpeg
|
||||
Global string // -hide_banner -v error
|
||||
Input string // -re -stream_loop -1 -i /media/bunny.mp4
|
||||
Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
||||
Filters []string // scale=1920:1080
|
||||
Output string // -f rtsp {output}
|
||||
|
||||
Video, Audio int // count of Video and Audio params
|
||||
}
|
||||
|
||||
func (a *Args) AddCodec(codec string) {
|
||||
a.Codecs = append(a.Codecs, codec)
|
||||
}
|
||||
|
||||
func (a *Args) AddFilter(filter string) {
|
||||
a.Filters = append(a.Filters, filter)
|
||||
}
|
||||
|
||||
func (a *Args) InsertFilter(filter string) {
|
||||
a.Filters = append([]string{filter}, a.Filters...)
|
||||
}
|
||||
|
||||
func (a *Args) String() string {
|
||||
b := bytes.NewBuffer(make([]byte, 0, 512))
|
||||
|
||||
b.WriteString(a.Bin)
|
||||
|
||||
if a.Global != "" {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.Global)
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.Input)
|
||||
|
||||
multimode := a.Video > 1 || a.Audio > 1
|
||||
var iv, ia int
|
||||
|
||||
for _, codec := range a.Codecs {
|
||||
// support multiple video and/or audio codecs
|
||||
if multimode && len(codec) >= 5 {
|
||||
switch codec[:5] {
|
||||
case "-c:v ":
|
||||
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
|
||||
iv++
|
||||
case "-c:a ":
|
||||
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
|
||||
ia++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(codec)
|
||||
}
|
||||
|
||||
if a.Filters != nil {
|
||||
for i, filter := range a.Filters {
|
||||
if i == 0 {
|
||||
b.WriteString(" -vf ")
|
||||
} else {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(filter)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.Output)
|
||||
|
||||
return b.String()
|
||||
}
|
@@ -26,6 +26,19 @@ func AnnexB2AVC(b []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func AVCtoAnnexB(b []byte) []byte {
|
||||
b = bytes.Clone(b)
|
||||
for i := 0; i < len(b); {
|
||||
size := int(binary.BigEndian.Uint32(b[i:]))
|
||||
b[i] = 0
|
||||
b[i+1] = 0
|
||||
b[i+2] = 0
|
||||
b[i+3] = 1
|
||||
i += 4 + size
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
const forbiddenZeroBit = 0x80
|
||||
const nalUnitType = 0x1F
|
||||
|
||||
|
@@ -94,6 +94,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||
if mtu == 0 {
|
||||
mtu = 1472
|
||||
}
|
||||
|
||||
payloader := &Payloader{IsAVC: true}
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
mtu -= 12 // rtp.Header size
|
||||
|
54
pkg/h265/avc.go
Normal file
54
pkg/h265/avc.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package h265
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
|
||||
const forbiddenZeroBit = 0x80
|
||||
const nalUnitType = 0x3F
|
||||
|
||||
// DecodeStream - find and return first AU in AVC format
|
||||
// useful for processing live streams with unknown separator size
|
||||
func DecodeStream(annexb []byte) ([]byte, int) {
|
||||
startPos := -1
|
||||
|
||||
i := 0
|
||||
for {
|
||||
// search next separator
|
||||
if i = h264.IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// move i to next AU
|
||||
if i += 3; i >= len(annexb) {
|
||||
break
|
||||
}
|
||||
|
||||
// check if AU type valid
|
||||
octet := annexb[i]
|
||||
if octet&forbiddenZeroBit != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
nalType := (octet >> 1) & nalUnitType
|
||||
if startPos >= 0 {
|
||||
switch nalType {
|
||||
case NALUTypeVPS, NALUTypePFrame:
|
||||
if annexb[i-4] == 0 {
|
||||
return h264.DecodeAnnexB(annexb[startPos : i-4]), i - 4
|
||||
} else {
|
||||
return h264.DecodeAnnexB(annexb[startPos : i-3]), i - 3
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch nalType {
|
||||
case NALUTypeVPS, NALUTypePFrame:
|
||||
if i >= 4 && annexb[i-4] == 0 {
|
||||
startPos = i - 4
|
||||
} else {
|
||||
startPos = i - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, 0
|
||||
}
|
@@ -76,6 +76,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||
if mtu == 0 {
|
||||
mtu = 1472
|
||||
}
|
||||
|
||||
payloader := &Payloader{}
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
mtu -= 12 // rtp.Header size
|
||||
|
@@ -32,6 +32,16 @@ const (
|
||||
Mdat = "mdat"
|
||||
)
|
||||
|
||||
const (
|
||||
sampleIsNonSync = 0x10000
|
||||
sampleDependsOn1 = 0x1000000
|
||||
sampleDependsOn2 = 0x2000000
|
||||
|
||||
SampleVideoIFrame = sampleDependsOn2
|
||||
SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync
|
||||
SampleAudio = sampleIsNonSync
|
||||
)
|
||||
|
||||
func (m *Movie) WriteFileType() {
|
||||
m.StartAtom(Ftyp)
|
||||
m.WriteString("iso5")
|
||||
@@ -250,7 +260,7 @@ func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, chann
|
||||
m.EndAtom() // TRAK
|
||||
}
|
||||
|
||||
func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) {
|
||||
func (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, time uint64) {
|
||||
m.StartAtom(Moof)
|
||||
|
||||
m.StartAtom(MoofMfhd)
|
||||
@@ -276,10 +286,10 @@ func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64)
|
||||
TfhdDefaultSampleFlags |
|
||||
TfhdDefaultBaseIsMoof,
|
||||
)
|
||||
m.WriteUint32(tid) // track id
|
||||
m.WriteUint32(duration) // default sample duration
|
||||
m.WriteUint32(size) // default sample size
|
||||
m.WriteUint32(0x2000000) // default sample flags
|
||||
m.WriteUint32(tid) // track id
|
||||
m.WriteUint32(duration) // default sample duration
|
||||
m.WriteUint32(size) // default sample size
|
||||
m.WriteUint32(flags) // default sample flags
|
||||
m.EndAtom()
|
||||
|
||||
m.StartAtom(MoofTrafTfdt)
|
||||
@@ -314,5 +324,4 @@ func (m *Movie) WriteData(b []byte) {
|
||||
m.StartAtom(Mdat)
|
||||
m.Write(b)
|
||||
m.EndAtom()
|
||||
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package iso
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
)
|
||||
|
||||
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
@@ -46,9 +47,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
|
||||
switch codec {
|
||||
case core.CodecAAC, core.CodecMP3:
|
||||
m.StartAtom("mp4a")
|
||||
m.StartAtom("mp4a") // supported in all players and browsers
|
||||
case core.CodecFLAC:
|
||||
m.StartAtom("fLaC") // supported in all players and browsers
|
||||
case core.CodecOpus:
|
||||
m.StartAtom("Opus")
|
||||
m.StartAtom("Opus") // supported in Chrome and Firefox
|
||||
case core.CodecPCMU:
|
||||
m.StartAtom("ulaw")
|
||||
case core.CodecPCMA:
|
||||
@@ -56,6 +59,11 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
||||
default:
|
||||
panic("unsupported iso audio: " + codec)
|
||||
}
|
||||
|
||||
if channels == 0 {
|
||||
channels = 1
|
||||
}
|
||||
|
||||
m.Skip(6)
|
||||
m.WriteUint16(1) // data_reference_index
|
||||
m.Skip(2) // version
|
||||
@@ -72,6 +80,10 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
||||
m.WriteEsdsAAC(conf)
|
||||
case core.CodecMP3:
|
||||
m.WriteEsdsMP3()
|
||||
case core.CodecFLAC:
|
||||
m.StartAtom("dfLa")
|
||||
m.Write(pcm.FLACHeader(false, sampleRate))
|
||||
m.EndAtom()
|
||||
case core.CodecOpus:
|
||||
// don't know what means this magic
|
||||
m.StartAtom("dOps")
|
||||
@@ -106,6 +118,7 @@ func (m *Movie) WriteEsdsAAC(conf []byte) {
|
||||
m.Skip(2) // es id
|
||||
m.Skip(1) // es flags
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#aac-audio
|
||||
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
|
||||
m.WriteBytes(0x40) // object id
|
||||
m.WriteBytes(0x15) // stream type
|
||||
@@ -139,6 +152,7 @@ func (m *Movie) WriteEsdsMP3() {
|
||||
m.Skip(2) // es id
|
||||
m.Skip(1) // es flags
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#mp3-audio
|
||||
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
|
||||
m.WriteBytes(0x6B) // object id
|
||||
m.WriteBytes(0x15) // stream type
|
||||
|
214
pkg/magic/client.go
Normal file
214
pkg/magic/client.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Client - can read unknown bytestream and autodetect format
|
||||
type Client struct {
|
||||
Desc string
|
||||
URL string
|
||||
|
||||
Handle func() error
|
||||
|
||||
r io.ReadCloser
|
||||
sniff []byte
|
||||
|
||||
medias []*core.Media
|
||||
receiver *core.Receiver
|
||||
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(r io.ReadCloser) *Client {
|
||||
return &Client{r: r}
|
||||
}
|
||||
|
||||
func (c *Client) Probe() (err error) {
|
||||
c.sniff = make([]byte, mpegts.PacketSize*3) // MPEG-TS: SDT+PAT+PMT
|
||||
c.recv, err = io.ReadFull(c.r, c.sniff)
|
||||
if err != nil {
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var codec *core.Codec
|
||||
|
||||
if bytes.HasPrefix(c.sniff, []byte{0, 0, 0, 1}) {
|
||||
switch {
|
||||
case h264.NALUType(c.sniff) == h264.NALUTypeSPS:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
c.Handle = c.ReadBitstreams
|
||||
|
||||
case h265.NALUType(c.sniff) == h265.NALUTypeVPS:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
c.Handle = c.ReadBitstreams
|
||||
}
|
||||
|
||||
} else if bytes.HasPrefix(c.sniff, []byte{0xFF, 0xD8}) {
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecJPEG,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
c.Handle = c.ReadMJPEG
|
||||
|
||||
} else if c.sniff[0] == mpegts.SyncByte {
|
||||
ts := mpegts.NewReader()
|
||||
ts.AppendBuffer(c.sniff)
|
||||
_ = ts.GetPacket()
|
||||
for _, streamType := range ts.GetStreamTypes() {
|
||||
switch streamType {
|
||||
case mpegts.StreamTypeH264:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
c.Handle = c.ReadMPEGTS
|
||||
|
||||
case mpegts.StreamTypeH265:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
c.Handle = c.ReadMPEGTS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if codec == nil {
|
||||
_ = c.Close()
|
||||
return errors.New("unknown format: " + hex.EncodeToString(c.sniff[:8]))
|
||||
}
|
||||
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) ReadBitstreams() error {
|
||||
buf := c.sniff // total bufer
|
||||
b := make([]byte, 1024*1024) // reading buffer
|
||||
|
||||
var decodeStream func([]byte) ([]byte, int)
|
||||
switch c.receiver.Codec.Name {
|
||||
case core.CodecH264:
|
||||
decodeStream = h264.DecodeStream
|
||||
case core.CodecH265:
|
||||
decodeStream = h265.DecodeStream
|
||||
}
|
||||
|
||||
for {
|
||||
payload, n := decodeStream(buf)
|
||||
if payload == nil {
|
||||
n, err := c.r.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf = append(buf, b[:n]...)
|
||||
c.recv += n
|
||||
continue
|
||||
}
|
||||
|
||||
buf = buf[n:]
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d", h264.Types(payload), len(payload))
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: payload,
|
||||
}
|
||||
c.receiver.WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ReadMJPEG() error {
|
||||
buf := c.sniff // total bufer
|
||||
b := make([]byte, 1024*1024) // reading buffer
|
||||
|
||||
for {
|
||||
// one JPEG end and next start
|
||||
i := bytes.Index(buf, []byte{0xFF, 0xD9, 0xFF, 0xD8})
|
||||
if i < 0 {
|
||||
n, err := c.r.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf = append(buf, b[:n]...)
|
||||
c.recv += n
|
||||
|
||||
// if we receive frame
|
||||
if n >= 2 && b[n-2] == 0xFF && b[n-1] == 0xD9 {
|
||||
i = len(buf)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
i += 2
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: buf[:i],
|
||||
}
|
||||
c.receiver.WriteRTP(pkt)
|
||||
|
||||
buf = buf[i:]
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ReadMPEGTS() error {
|
||||
b := make([]byte, 1024*1024) // reading buffer
|
||||
|
||||
ts := mpegts.NewReader()
|
||||
ts.AppendBuffer(c.sniff)
|
||||
|
||||
for {
|
||||
packet := ts.GetPacket()
|
||||
if packet == nil {
|
||||
n, err := c.r.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts.AppendBuffer(b[:n])
|
||||
c.recv += n
|
||||
continue
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h264.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
|
||||
|
||||
switch packet.PayloadType {
|
||||
case mpegts.StreamTypeH264, mpegts.StreamTypeH265:
|
||||
c.receiver.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.r.Close()
|
||||
}
|
91
pkg/magic/keyframe.go
Normal file
91
pkg/magic/keyframe.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Keyframe struct {
|
||||
core.Listener
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
medias []*core.Media
|
||||
sender *core.Sender
|
||||
}
|
||||
|
||||
func (k *Keyframe) GetMedias() []*core.Media {
|
||||
if k.medias == nil {
|
||||
k.medias = append(k.medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
{Name: core.CodecJPEG},
|
||||
},
|
||||
})
|
||||
}
|
||||
return k.medias
|
||||
}
|
||||
|
||||
func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
var handler core.HandlerFunc
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
handler = func(packet *rtp.Packet) {
|
||||
if !h264.IsKeyframe(packet.Payload) {
|
||||
return
|
||||
}
|
||||
b := h264.AVCtoAnnexB(packet.Payload)
|
||||
k.Fire(b)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler = h264.RTPDepay(track.Codec, handler)
|
||||
}
|
||||
case core.CodecH265:
|
||||
handler = func(packet *rtp.Packet) {
|
||||
if !h265.IsKeyframe(packet.Payload) {
|
||||
return
|
||||
}
|
||||
k.Fire(packet.Payload)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler = h265.RTPDepay(track.Codec, handler)
|
||||
}
|
||||
case core.CodecJPEG:
|
||||
handler = func(packet *rtp.Packet) {
|
||||
k.Fire(packet.Payload)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler = mjpeg.RTPDepay(handler)
|
||||
}
|
||||
}
|
||||
|
||||
k.sender = core.NewSender(media, track.Codec)
|
||||
k.sender.Handler = handler
|
||||
k.sender.HandleRTP(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Keyframe) CodecName() string {
|
||||
if k.sender != nil {
|
||||
return k.sender.Codec.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (k *Keyframe) Stop() error {
|
||||
if k.sender != nil {
|
||||
k.sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
41
pkg/magic/producer.go
Normal file
41
pkg/magic/producer.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
if c.receiver == nil {
|
||||
c.receiver = core.NewReceiver(media, codec)
|
||||
}
|
||||
return c.receiver, nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.Handle()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() (err error) {
|
||||
if c.receiver != nil {
|
||||
c.receiver.Close()
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: c.Desc,
|
||||
URL: c.URL,
|
||||
Medias: c.medias,
|
||||
Recv: c.recv,
|
||||
}
|
||||
if c.receiver != nil {
|
||||
info.Receivers = append(info.Receivers, c.receiver)
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
@@ -6,7 +6,9 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
@@ -19,6 +21,7 @@ type Consumer struct {
|
||||
senders []*core.Sender
|
||||
|
||||
muxer *Muxer
|
||||
mu sync.Mutex
|
||||
wait byte
|
||||
|
||||
send int
|
||||
@@ -52,7 +55,8 @@ func (c *Consumer) GetMedias() []*core.Media {
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
trackID := byte(len(c.senders))
|
||||
|
||||
handler := core.NewSender(media, track.Codec)
|
||||
codec := track.Codec.Clone()
|
||||
handler := core.NewSender(media, codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
@@ -70,10 +74,12 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
// important to use Mutex because right fragment order
|
||||
c.mu.Lock()
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
@@ -97,46 +103,48 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||
}
|
||||
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unsupported codec")
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
c.send += len(buf)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecAAC:
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||
}
|
||||
case core.CodecOpus, core.CodecMP3: // no changes
|
||||
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
|
||||
handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler)
|
||||
codec.Name = core.CodecFLAC
|
||||
|
||||
default:
|
||||
handler.Handler = nil
|
||||
}
|
||||
}
|
||||
|
||||
if handler.Handler == nil {
|
||||
println("ERROR: MP4 unsupported codec: " + track.Codec.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
handler.HandleRTP(track)
|
||||
|
@@ -4,9 +4,45 @@ import "github.com/AlexxIT/go2rtc/pkg/core"
|
||||
|
||||
// ParseQuery - like usual parse, but with mp4 param handler
|
||||
func ParseQuery(query map[string][]string) []*core.Media {
|
||||
if query["mp4"] != nil {
|
||||
cons := Consumer{}
|
||||
return cons.GetMedias()
|
||||
if v := query["mp4"]; len(v) != 0 {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if v[0] == "" {
|
||||
return medias // legacy
|
||||
}
|
||||
|
||||
medias[1].Codecs = append(medias[1].Codecs,
|
||||
&core.Codec{Name: core.CodecPCMA},
|
||||
&core.Codec{Name: core.CodecPCMU},
|
||||
&core.Codec{Name: core.CodecPCM},
|
||||
)
|
||||
|
||||
if v[0] == "flac" {
|
||||
return medias // modern browsers
|
||||
}
|
||||
|
||||
medias[1].Codecs = append(medias[1].Codecs,
|
||||
&core.Codec{Name: core.CodecOpus},
|
||||
&core.Codec{Name: core.CodecMP3},
|
||||
)
|
||||
|
||||
return medias // Chrome, FFmpeg, VLC
|
||||
}
|
||||
|
||||
return core.ParseQuery(query)
|
||||
|
@@ -15,12 +15,14 @@ type Muxer struct {
|
||||
fragIndex uint32
|
||||
dts []uint64
|
||||
pts []uint32
|
||||
codecs []*core.Codec
|
||||
}
|
||||
|
||||
const (
|
||||
MimeH264 = "avc1.640029"
|
||||
MimeH265 = "hvc1.1.6.L153.B0"
|
||||
MimeAAC = "mp4a.40.2"
|
||||
MimeFlac = "flac"
|
||||
MimeOpus = "opus"
|
||||
)
|
||||
|
||||
@@ -43,6 +45,8 @@ func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
|
||||
s += MimeAAC
|
||||
case core.CodecOpus:
|
||||
s += MimeOpus
|
||||
case core.CodecFLAC:
|
||||
s += MimeFlac
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +64,11 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
// some dummy SPS and PPS not a problem
|
||||
if len(sps) == 0 {
|
||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
}
|
||||
if len(pps) == 0 {
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
@@ -79,10 +85,14 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
||||
|
||||
case core.CodecH265:
|
||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
// some dummy SPS and PPS not a problem
|
||||
if len(vps) == 0 {
|
||||
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
|
||||
}
|
||||
if len(sps) == 0 {
|
||||
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
|
||||
}
|
||||
if len(pps) == 0 {
|
||||
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
|
||||
}
|
||||
|
||||
@@ -108,14 +118,15 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
||||
)
|
||||
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC:
|
||||
mv.WriteAudioTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
||||
)
|
||||
}
|
||||
|
||||
m.pts = append(m.pts, 0)
|
||||
m.dts = append(m.dts, 0)
|
||||
m.pts = append(m.pts, 0)
|
||||
m.codecs = append(m.codecs, codec)
|
||||
}
|
||||
|
||||
mv.StartAtom(iso.MoovMvex)
|
||||
@@ -138,28 +149,49 @@ func (m *Muxer) Reset() {
|
||||
}
|
||||
|
||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||
// important before increment
|
||||
time := m.dts[trackID]
|
||||
codec := m.codecs[trackID]
|
||||
|
||||
duration := packet.Timestamp - m.pts[trackID]
|
||||
m.pts[trackID] = packet.Timestamp
|
||||
|
||||
// minumum duration important for MSE in Apple Safari
|
||||
if duration == 0 || duration > codec.ClockRate {
|
||||
duration = codec.ClockRate/1000 + 1
|
||||
m.pts[trackID] += duration
|
||||
}
|
||||
|
||||
size := len(packet.Payload)
|
||||
|
||||
// flags important for Apple Finder video preview
|
||||
var flags uint32
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
flags = iso.SampleVideoIFrame
|
||||
} else {
|
||||
flags = iso.SampleVideoNonIFrame
|
||||
}
|
||||
case core.CodecH265:
|
||||
if h265.IsKeyframe(packet.Payload) {
|
||||
flags = iso.SampleVideoIFrame
|
||||
} else {
|
||||
flags = iso.SampleVideoNonIFrame
|
||||
}
|
||||
default:
|
||||
flags = iso.SampleAudio // not important
|
||||
}
|
||||
|
||||
m.fragIndex++
|
||||
|
||||
var duration uint32
|
||||
newTime := packet.Timestamp
|
||||
if m.pts[trackID] > 0 {
|
||||
duration = newTime - m.pts[trackID]
|
||||
m.dts[trackID] += uint64(duration)
|
||||
} else {
|
||||
// important, or Safari will fail with first frame
|
||||
duration = 1
|
||||
}
|
||||
m.pts[trackID] = newTime
|
||||
|
||||
mv := iso.NewMovie(1024 + len(packet.Payload))
|
||||
mv := iso.NewMovie(1024 + size)
|
||||
mv.WriteMovieFragment(
|
||||
m.fragIndex, uint32(trackID+1), duration,
|
||||
uint32(len(packet.Payload)), time,
|
||||
m.fragIndex, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID],
|
||||
)
|
||||
mv.WriteData(packet.Payload)
|
||||
|
||||
//log.Printf("[MP4] track=%d ts=%6d dur=%5d idx=%3d len=%d", trackID+1, m.dts[trackID], duration, m.fragIndex, len(packet.Payload))
|
||||
|
||||
m.dts[trackID] += uint64(duration)
|
||||
|
||||
return mv.Bytes()
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ func NewClient(res *http.Response) *Client {
|
||||
func (c *Client) Handle() error {
|
||||
reader := NewReader()
|
||||
|
||||
b := make([]byte, 1024*1024*256) // 256K
|
||||
b := make([]byte, 1024*256) // 256K
|
||||
|
||||
probe := core.NewProbe(c.medias == nil)
|
||||
for probe == nil || probe.Active() {
|
||||
|
@@ -3,6 +3,7 @@ package mpegts
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
@@ -16,6 +17,7 @@ const (
|
||||
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
|
||||
StreamTypeAAC = 0x0F
|
||||
StreamTypeH264 = 0x1B
|
||||
StreamTypeH265 = 0x24
|
||||
StreamTypePCMATapo = 0x90
|
||||
)
|
||||
|
||||
@@ -36,6 +38,8 @@ type PES struct {
|
||||
|
||||
Sequence uint16
|
||||
Timestamp uint32
|
||||
|
||||
decodeStream func([]byte) ([]byte, int)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -52,9 +56,14 @@ func (p *PES) SetBuffer(size uint16, b []byte) {
|
||||
optSize := b[2] // optional fields
|
||||
b = b[minHeaderSize+optSize:]
|
||||
|
||||
if p.StreamType == StreamTypeH264 {
|
||||
switch p.StreamType {
|
||||
case StreamTypeH264:
|
||||
p.Mode = ModeStream
|
||||
} else {
|
||||
p.decodeStream = h264.DecodeStream
|
||||
case StreamTypeH265:
|
||||
p.Mode = ModeStream
|
||||
p.decodeStream = h265.DecodeStream
|
||||
default:
|
||||
println("WARNING: mpegts: unknown zero-size stream")
|
||||
}
|
||||
} else {
|
||||
@@ -91,7 +100,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
|
||||
payload := p.Payload[minHeaderSize+optSize:]
|
||||
|
||||
switch p.StreamType {
|
||||
case StreamTypeH264:
|
||||
case StreamTypeH264, StreamTypeH265:
|
||||
var ts uint32
|
||||
|
||||
const hasPTS = 0b1000_0000
|
||||
@@ -125,7 +134,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
|
||||
p.Payload = nil
|
||||
|
||||
case ModeStream:
|
||||
payload, i := h264.DecodeStream(p.Payload)
|
||||
payload, i := p.decodeStream(p.Payload)
|
||||
if payload == nil {
|
||||
return
|
||||
}
|
||||
@@ -137,7 +146,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
|
||||
pkt = &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
PayloadType: p.StreamType,
|
||||
Timestamp: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second),
|
||||
Timestamp: core.Now90000(),
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
@@ -129,6 +129,14 @@ func (r *Reader) GetPacket() *rtp.Packet {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) GetStreamTypes() []byte {
|
||||
types := make([]byte, 0, len(r.pes))
|
||||
for _, pes := range r.pes {
|
||||
types = append(types, pes.StreamType)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
// Sync - search sync byte
|
||||
func (r *Reader) Sync() bool {
|
||||
// drop previous readed packet
|
||||
|
255
pkg/onvif/client.go
Normal file
255
pkg/onvif/client.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
url *url.URL
|
||||
|
||||
deviceURL string
|
||||
mediaURL string
|
||||
imaginURL string
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseURL := "http://" + u.Host
|
||||
|
||||
client := &Client{url: u}
|
||||
if u.Path == "" {
|
||||
client.deviceURL = baseURL + PathDevice
|
||||
} else {
|
||||
client.deviceURL = baseURL + u.Path
|
||||
}
|
||||
|
||||
b, err := client.GetCapabilities()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.mediaURL = FindTagValue(b, "Media.+?XAddr")
|
||||
client.imaginURL = FindTagValue(b, "Imaging.+?XAddr")
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetURI() (string, error) {
|
||||
query := c.url.Query()
|
||||
|
||||
token := query.Get("subtype")
|
||||
|
||||
// support empty
|
||||
if i := atoi(token); i >= 0 {
|
||||
tokens, err := c.GetProfilesTokens()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if i >= len(tokens) {
|
||||
return "", errors.New("onvif: wrong subtype")
|
||||
}
|
||||
token = tokens[i]
|
||||
}
|
||||
|
||||
getUri := c.GetStreamUri
|
||||
if query.Has("snapshot") {
|
||||
getUri = c.GetSnapshotUri
|
||||
}
|
||||
|
||||
b, err := getUri(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := FindTagValue(b, "Uri")
|
||||
uri = html.UnescapeString(uri)
|
||||
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if u.User == nil && c.url.User != nil {
|
||||
u.User = c.url.User
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (c *Client) GetName() (string, error) {
|
||||
b, err := c.GetDeviceInformation()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return FindTagValue(b, "Manufacturer") + " " + FindTagValue(b, "Model"), nil
|
||||
}
|
||||
|
||||
func (c *Client) GetProfilesTokens() ([]string, error) {
|
||||
b, err := c.GetProfiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tokens []string
|
||||
|
||||
re := regexp.MustCompile(`Profiles.+?token="([^"]+)`)
|
||||
for _, s := range re.FindAllStringSubmatch(string(b), 10) {
|
||||
tokens = append(tokens, s[1])
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (c *Client) HasSnapshots() bool {
|
||||
b, err := c.GetServiceCapabilities()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(b), `SnapshotUri="true"`)
|
||||
}
|
||||
|
||||
func (c *Client) GetCapabilities() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL,
|
||||
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:Category>All</tds:Category>
|
||||
</tds:GetCapabilities>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetNetworkInterfaces() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetDeviceInformation() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetProfiles() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.mediaURL, `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetStreamUri(token string) ([]byte, error) {
|
||||
return c.Request(
|
||||
c.mediaURL,
|
||||
`<trt:GetStreamUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<trt:StreamSetup>
|
||||
<tt:Stream>RTP-Unicast</tt:Stream>
|
||||
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>
|
||||
</trt:StreamSetup>
|
||||
<trt:ProfileToken>`+token+`</trt:ProfileToken>
|
||||
</trt:GetStreamUri>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
|
||||
return c.Request(
|
||||
c.imaginURL,
|
||||
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<trt:ProfileToken>`+token+`</trt:ProfileToken>
|
||||
</trt:GetSnapshotUri>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL, `<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetServiceCapabilities() ([]byte, error) {
|
||||
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
|
||||
return c.Request(
|
||||
c.mediaURL, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) SystemReboot() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetServices() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:IncludeCapability>true</tds:IncludeCapability>
|
||||
</tds:GetServices>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetScopes() ([]byte, error) {
|
||||
return c.Request(
|
||||
c.deviceURL, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) Request(url, body string) ([]byte, error) {
|
||||
if url == "" {
|
||||
return nil, errors.New("onvif: unsupported service")
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.WriteString(
|
||||
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
|
||||
)
|
||||
|
||||
if user := c.url.User; user != nil {
|
||||
nonce := core.RandString(16, 36)
|
||||
created := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
pass, _ := user.Password()
|
||||
|
||||
h := sha1.New()
|
||||
h.Write([]byte(nonce + created + pass))
|
||||
|
||||
buf.WriteString(`<s:Header>
|
||||
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
|
||||
<wsse:UsernameToken>
|
||||
<wsse:Username>` + user.Username() + `</wsse:Username>
|
||||
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `</wsse:Password>
|
||||
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
|
||||
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` + created + `</wsu:Created>
|
||||
</wsse:UsernameToken>
|
||||
</wsse:Security>
|
||||
</s:Header>`)
|
||||
}
|
||||
|
||||
buf.WriteString(`<s:Body>` + body + `</s:Body></s:Envelope>`)
|
||||
|
||||
client := &http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// need to close body with eny response status
|
||||
b, err := io.ReadAll(res.Body)
|
||||
|
||||
if err == nil && res.StatusCode != http.StatusOK {
|
||||
err = errors.New("onvif: " + res.Status + " for " + url)
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
109
pkg/onvif/helpers.go
Normal file
109
pkg/onvif/helpers.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PathDevice = "/onvif/device_service"
|
||||
)
|
||||
|
||||
func FindTagValue(b []byte, tag string) string {
|
||||
re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`)
|
||||
m := re.FindSubmatch(b)
|
||||
if len(m) != 2 {
|
||||
return ""
|
||||
}
|
||||
return string(m[1])
|
||||
}
|
||||
|
||||
// UUID - generate something like 44302cbf-0d18-4feb-79b3-33b575263da3
|
||||
func UUID() string {
|
||||
s := core.RandString(32, 16)
|
||||
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
|
||||
}
|
||||
|
||||
func DiscoveryStreamingURLs() ([]string, error) {
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf
|
||||
// 5.3 Discovery Procedure:
|
||||
msg := `<?xml version="1.0" ?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Header xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
|
||||
<a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>
|
||||
<a:MessageID>urn:uuid:` + UUID() + `</a:MessageID>
|
||||
<a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<d:Probe xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery">
|
||||
<d:Types />
|
||||
<d:Scopes />
|
||||
</d:Probe>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.IP{239, 255, 255, 250},
|
||||
Port: 3702,
|
||||
}
|
||||
|
||||
if _, err = conn.WriteTo([]byte(msg), addr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = conn.SetReadDeadline(time.Now().Add(time.Second * 3)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var urls []string
|
||||
|
||||
b := make([]byte, 8192)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUDP(b)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
//log.Printf("[onvif] discovery response addr=%s:\n%s", addr, b[:n])
|
||||
|
||||
// ignore printers, etc
|
||||
if !strings.Contains(string(b[:n]), "onvif") {
|
||||
continue
|
||||
}
|
||||
|
||||
url := FindTagValue(b[:n], "XAddrs")
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// fix some buggy cameras
|
||||
// <wsdd:XAddrs>http://0.0.0.0:8080/onvif/device_service</wsdd:XAddrs>
|
||||
if strings.HasPrefix(url, "http://0.0.0.0") {
|
||||
url = "http://" + addr.IP.String() + url[14:]
|
||||
}
|
||||
|
||||
urls = append(urls, url)
|
||||
}
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
|
||||
func atoi(s string) int {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
204
pkg/onvif/server.go
Normal file
204
pkg/onvif/server.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ActionGetCapabilities = "GetCapabilities"
|
||||
ActionGetSystemDateAndTime = "GetSystemDateAndTime"
|
||||
ActionGetNetworkInterfaces = "GetNetworkInterfaces"
|
||||
ActionGetDeviceInformation = "GetDeviceInformation"
|
||||
ActionGetServiceCapabilities = "GetServiceCapabilities"
|
||||
ActionGetProfiles = "GetProfiles"
|
||||
ActionGetStreamUri = "GetStreamUri"
|
||||
ActionSystemReboot = "SystemReboot"
|
||||
|
||||
ActionGetServices = "GetServices"
|
||||
ActionGetScopes = "GetScopes"
|
||||
ActionGetVideoSources = "GetVideoSources"
|
||||
ActionGetAudioSources = "GetAudioSources"
|
||||
ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
|
||||
ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
|
||||
ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
|
||||
ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
|
||||
)
|
||||
|
||||
func GetRequestAction(b []byte) string {
|
||||
// <soap-env:Body><ns0:GetCapabilities xmlns:ns0="http://www.onvif.org/ver10/device/wsdl">
|
||||
// <v:Body><GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl" /></v:Body>
|
||||
re := regexp.MustCompile(`Body[^<]+<([^ />]+)`)
|
||||
m := re.FindSubmatch(b)
|
||||
if len(m) != 2 {
|
||||
return ""
|
||||
}
|
||||
if i := bytes.IndexByte(m[1], ':'); i > 0 {
|
||||
return string(m[1][i+1:])
|
||||
}
|
||||
return string(m[1])
|
||||
}
|
||||
|
||||
func GetCapabilitiesResponse(host string) string {
|
||||
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:Capabilities xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:Device>
|
||||
<tt:XAddr>http://` + host + `/onvif/device_service</tt:XAddr>
|
||||
</tt:Device>
|
||||
<tt:Media>
|
||||
<tt:XAddr>http://` + host + `/onvif/media_service</tt:XAddr>
|
||||
<tt:StreamingCapabilities>
|
||||
<tt:RTPMulticast>false</tt:RTPMulticast>
|
||||
<tt:RTP_TCP>false</tt:RTP_TCP>
|
||||
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
|
||||
</tt:StreamingCapabilities>
|
||||
</tt:Media>
|
||||
</tds:Capabilities>
|
||||
</tds:GetCapabilitiesResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
||||
|
||||
func GetSystemDateAndTimeResponse() string {
|
||||
loc := time.Now()
|
||||
utc := loc.UTC()
|
||||
|
||||
return fmt.Sprintf(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<tds:GetSystemDateAndTimeResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:SystemDateAndTime xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:DateTimeType>NTP</tt:DateTimeType>
|
||||
<tt:DaylightSavings>false</tt:DaylightSavings>
|
||||
<tt:TimeZone>
|
||||
<tt:TZ>GMT%s</tt:TZ>
|
||||
</tt:TimeZone>
|
||||
<tt:UTCDateTime>
|
||||
<tt:Time>
|
||||
<tt:Hour>%d</tt:Hour>
|
||||
<tt:Minute>%d</tt:Minute>
|
||||
<tt:Second>%d</tt:Second>
|
||||
</tt:Time>
|
||||
<tt:Date>
|
||||
<tt:Year>%d</tt:Year>
|
||||
<tt:Month>%d</tt:Month>
|
||||
<tt:Day>%d</tt:Day>
|
||||
</tt:Date>
|
||||
</tt:UTCDateTime>
|
||||
<tt:LocalDateTime>
|
||||
<tt:Time>
|
||||
<tt:Hour>%d</tt:Hour>
|
||||
<tt:Minute>%d</tt:Minute>
|
||||
<tt:Second>%d</tt:Second>
|
||||
</tt:Time>
|
||||
<tt:Date>
|
||||
<tt:Year>%d</tt:Year>
|
||||
<tt:Month>%d</tt:Month>
|
||||
<tt:Day>%d</tt:Day>
|
||||
</tt:Date>
|
||||
</tt:LocalDateTime>
|
||||
</tds:SystemDateAndTime>
|
||||
</tds:GetSystemDateAndTimeResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
loc.Format("-07:00"),
|
||||
utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(),
|
||||
loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(),
|
||||
)
|
||||
}
|
||||
|
||||
func GetNetworkInterfacesResponse() string {
|
||||
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<tds:GetNetworkInterfacesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
||||
|
||||
func GetDeviceInformationResponse(manuf, model, firmware, serial string) string {
|
||||
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:Manufacturer>` + manuf + `</tds:Manufacturer>
|
||||
<tds:Model>` + model + `</tds:Model>
|
||||
<tds:FirmwareVersion>` + firmware + `</tds:FirmwareVersion>
|
||||
<tds:SerialNumber>` + serial + `</tds:SerialNumber>
|
||||
<tds:HardwareId>1.00</tds:HardwareId>
|
||||
</tds:GetDeviceInformationResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
||||
|
||||
func GetServiceCapabilitiesResponse() string {
|
||||
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<trt:GetServiceCapabilitiesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<trt:Capabilities SnapshotUri="false" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
|
||||
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
|
||||
</trt:Capabilities>
|
||||
</trt:GetServiceCapabilitiesResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
||||
|
||||
func SystemRebootResponse() string {
|
||||
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<tds:SystemRebootResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:Message>system reboot in 1 second...</tds:Message>
|
||||
</tds:SystemRebootResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
||||
|
||||
func GetProfilesResponse(names []string) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.WriteString(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">`)
|
||||
|
||||
for i, name := range names {
|
||||
buf.WriteString(`
|
||||
<trt:Profiles token="` + name + `" fixed="true">
|
||||
<tt:Name>` + name + `</tt:Name>
|
||||
<tt:VideoEncoderConfiguration token="` + strconv.Itoa(i) + `">
|
||||
<tt:Encoding>H264</tt:Encoding>
|
||||
<tt:Resolution>
|
||||
<tt:Width>1920</tt:Width>
|
||||
<tt:Height>1080</tt:Height>
|
||||
</tt:Resolution>
|
||||
</tt:VideoEncoderConfiguration>
|
||||
</trt:Profiles>`)
|
||||
}
|
||||
|
||||
buf.WriteString(`
|
||||
</trt:GetProfilesResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func GetStreamUriResponse(uri string) string {
|
||||
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<trt:MediaUri>
|
||||
<tt:Uri xmlns:tt="http://www.onvif.org/ver10/schema">` + uri + `</tt:Uri>
|
||||
</trt:MediaUri>
|
||||
</trt:GetStreamUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
146
pkg/pcm/flac.go
Normal file
146
pkg/pcm/flac.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Package pcm - support raw (verbatim) PCM 16 bit in the FLAC container:
|
||||
// - only 1 channel
|
||||
// - only 16 bit per sample
|
||||
// - only 8000, 16000, 24000, 48000 sample rate
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/sigurn/crc16"
|
||||
"github.com/sigurn/crc8"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func FLACHeader(magic bool, sampleRate uint32) []byte {
|
||||
b := make([]byte, 42)
|
||||
|
||||
if magic {
|
||||
copy(b, "fLaC") // [0..3]
|
||||
}
|
||||
|
||||
// https://xiph.org/flac/format.html#metadata_block_header
|
||||
b[4] = 0x80 // [4] lastMetadata=1 (1 bit), blockType=0 - STREAMINFO (7 bit)
|
||||
b[7] = 0x22 // [5..7] blockLength=34 (24 bit)
|
||||
|
||||
// Important for Apple QuickTime player:
|
||||
// 1. Both values should be same
|
||||
// 2. Maximum value = 32768
|
||||
binary.BigEndian.PutUint16(b[8:], 32768) // [8..9] info.BlockSizeMin=16 (16 bit)
|
||||
binary.BigEndian.PutUint16(b[10:], 32768) // [10..11] info.BlockSizeMin=65535 (16 bit)
|
||||
|
||||
// [12..14] info.FrameSizeMin=0 (24 bit)
|
||||
// [15..17] info.FrameSizeMax=0 (24 bit)
|
||||
|
||||
b[18] = byte(sampleRate >> 12)
|
||||
b[19] = byte(sampleRate >> 4)
|
||||
b[20] = byte(sampleRate << 4) // [18..20] info.SampleRate=8000 (20 bit), info.NChannels=1-1 (3 bit)
|
||||
|
||||
b[21] = 0xF0 // [21..25] info.BitsPerSample=16-1 (5 bit), info.NSamples (36 bit)
|
||||
|
||||
// [26..41] MD5sum (16 bytes)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
var table8 *crc8.Table
|
||||
var table16 *crc16.Table
|
||||
|
||||
func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
if codec.Channels >= 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sr byte
|
||||
switch codec.ClockRate {
|
||||
case 8000:
|
||||
sr = 0b0100
|
||||
case 16000:
|
||||
sr = 0b0101
|
||||
case 22050:
|
||||
sr = 0b0110
|
||||
case 24000:
|
||||
sr = 0b0111
|
||||
case 32000:
|
||||
sr = 0b1000
|
||||
case 44100:
|
||||
sr = 0b1001
|
||||
case 48000:
|
||||
sr = 0b1010
|
||||
case 96000:
|
||||
sr = 0b1011
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if table8 == nil {
|
||||
table8 = crc8.MakeTable(crc8.CRC8)
|
||||
}
|
||||
if table16 == nil {
|
||||
table16 = crc16.MakeTable(crc16.CRC16_BUYPASS)
|
||||
}
|
||||
|
||||
var sampleNumber int32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
samples := uint16(len(packet.Payload))
|
||||
|
||||
if codec.Name == core.CodecPCM {
|
||||
samples /= 2
|
||||
}
|
||||
|
||||
// https://xiph.org/flac/format.html#frame_header
|
||||
buf := make([]byte, samples*2+30)
|
||||
|
||||
// 1. Frame header
|
||||
buf[0] = 0xFF
|
||||
buf[1] = 0xF9 // [0..1] syncCode=0xFFF8 - reserved (15 bit), blockStrategy=1 - variable-blocksize (1 bit)
|
||||
buf[2] = 0x70 | sr // blockSizeType=7 (4 bit), sampleRate=4 - 8000 (4 bit)
|
||||
buf[3] = 0x08 // channels=1-1 (4 bit), sampleSize=4 - 16 (3 bit), reserved=0 (1 bit)
|
||||
|
||||
n := 4 + utf8.EncodeRune(buf[4:], sampleNumber) // 4 bytes max
|
||||
sampleNumber += int32(samples)
|
||||
|
||||
// this is wrong but very simple frame block size value
|
||||
binary.BigEndian.PutUint16(buf[n:], samples-1)
|
||||
n += 2
|
||||
|
||||
buf[n] = crc8.Checksum(buf[:n], table8)
|
||||
n += 1
|
||||
|
||||
// 2. Subframe header
|
||||
buf[n] = 0x02 // padding=0 (1 bit), subframeType=1 - verbatim (6 bit), wastedFlag=0 (1 bit)
|
||||
n += 1
|
||||
|
||||
// 3. Subframe
|
||||
switch codec.Name {
|
||||
case core.CodecPCMA:
|
||||
for _, b := range packet.Payload {
|
||||
s16 := PCMAtoPCM(b)
|
||||
buf[n] = byte(s16 >> 8)
|
||||
buf[n+1] = byte(s16)
|
||||
n += 2
|
||||
}
|
||||
case core.CodecPCMU:
|
||||
for _, b := range packet.Payload {
|
||||
s16 := PCMUtoPCM(b)
|
||||
buf[n] = byte(s16 >> 8)
|
||||
buf[n+1] = byte(s16)
|
||||
n += 2
|
||||
}
|
||||
case core.CodecPCM:
|
||||
n += copy(buf[n:], packet.Payload)
|
||||
}
|
||||
|
||||
// 4. Frame footer
|
||||
crc := crc16.Checksum(buf[:n], table16)
|
||||
binary.BigEndian.PutUint16(buf[n:], crc)
|
||||
n += 2
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = buf[:n]
|
||||
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user