Compare commits

..

35 Commits

Author SHA1 Message Date
Alexey Khit da92256910 Update version to 1.5.0 2023-05-04 15:04:06 +03:00
Alexey Khit 035b824645 Update readme 2023-05-04 15:03:03 +03:00
Alexey Khit 2a91c4625a Add ALSA support inside docker 2023-05-04 13:47:12 +03:00
Alexey Khit 23dd5b450c Add support H265 codec for MPEG-TS 2023-05-04 12:26:56 +03:00
Alexey Khit f617c148cd Fix FFmpeg template for H265 2023-05-04 11:57:32 +03:00
Alexey Khit b5f4c7f75b Rewrite exec pipe, TCP and HTTP sources 2023-05-04 11:56:56 +03:00
Alexey Khit d44efb84a0 Fix buffer size for mpegts 2023-05-04 11:49:38 +03:00
Alexey Khit 03968d2f2e Restore hadrware transcoding for MJPEG 2023-05-04 07:39:15 +03:00
Alexey Khit 3c371e7046 Change FFmpeg output for MJPEG to pipe 2023-05-04 07:38:49 +03:00
Alexey Khit 4656086985 Add auto transcoding to JPEG snapshot 2023-05-04 06:49:54 +03:00
Alexey Khit e78f9fa69d Add support pipe to exec source 2023-05-04 06:49:54 +03:00
Alexey Khit 2e8be342ef Rework FFmpeg hardware support 2023-05-04 01:24:37 +03:00
Alexey Khit 5387e88fe3 Rework FFmpeg devices support 2023-05-04 00:03:01 +03:00
Alexey Khit 1746f55eda Add pix_fmt to ffmpeg h264 transcoding 2023-05-03 23:57:39 +03:00
Alexey Khit 4d53889519 Improve support ONVIF client 2023-05-03 08:02:56 +03:00
Alexey Khit 6d9d89bbe3 Fix support 2 way audio for Reolink Doorbell #331 2023-05-03 08:01:33 +03:00
Alexey Khit c1923627c0 Fix panic on Producer GetMedias 2023-05-03 07:57:00 +03:00
Alexey Khit 95ca5f5fe1 Remove unnecessary run.sh file 2023-05-02 14:20:55 +03:00
Alexey Khit 4bbd3a1cd2 Fix ONVIF discovery for buggy camera 2023-05-02 11:25:00 +03:00
Alexey Khit 9c8a1d8b19 Add path to ONVIF requests 2023-05-02 11:07:12 +03:00
Alexey Khit 53967fc72a Update ONVIF discovery request #397 2023-05-02 09:36:38 +03:00
Alexey Khit 31f870e950 Update internal readme 2023-05-01 15:09:58 +03:00
Alexey Khit c7d228daff Remove mp4 pkg dependency from rtsp pkg 2023-05-01 14:33:03 +03:00
Alexey Khit 378f071e2c Add go2rtc_rtsp app 2023-05-01 12:55:32 +03:00
Alexey Khit 75f61b38ac Move cmd module to internal 2023-05-01 12:55:14 +03:00
Alexey Khit bc770f1a85 Remove FFmpeg buffer because have problems with MJPEG 2023-04-29 17:04:56 +03:00
Alexey Khit d276311fcf Add support insecure HTTPS client 2023-04-29 17:00:52 +03:00
Alexey Khit 1e14dc9ab2 Add ONVIF client and server support 2023-04-29 15:12:59 +03:00
Alexey Khit 8dbaa4ba93 Fix RTSP client Session processing 2023-04-29 13:48:17 +03:00
Alexey Khit f0893bd78b Fix bug in SDP from Annke CZ400 #384 2023-04-27 14:02:55 +03:00
Alexey Khit 6247746177 Change localhost to 127.0.0.1 2023-04-25 06:21:50 +03:00
Alexey Khit a20de73ab2 Add pkt_size option fort RTSP server 2023-04-24 06:40:11 +03:00
Alexey Khit 813c8b3b3d Make core atoi func public 2023-04-24 06:39:07 +03:00
Alexey Khit 63d9c6c2b7 Fix Chinese cameras with wrong Session header after v1.4.0 #382 2023-04-23 20:42:49 +03:00
Alexey Khit 2610f15eb6 Update readme about codecs 2023-04-23 08:08:52 +03:00
98 changed files with 2592 additions and 926 deletions
+2 -1
View File
@@ -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
+100 -24
View File
@@ -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.
-19
View File
@@ -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}"
-4
View File
@@ -1,4 +0,0 @@
**Project layout**
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro
-61
View File
@@ -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}
}
-50
View File
@@ -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}
}
-57
View File
@@ -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}
}
-91
View File
@@ -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)
}
-120
View File
@@ -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
}
-21
View File
@@ -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
}
-67
View File
@@ -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
}
-40
View File
@@ -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
View 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")
}
-65
View File
@@ -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)
}
-35
View File
@@ -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
}
+5 -1
View File
@@ -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
View 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
+1 -1
View File
@@ -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 -1
View File
@@ -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"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"gopkg.in/yaml.v3"
)
var Version = "1.4.0"
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 -2
View File
@@ -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"
+53 -22
View File
@@ -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
View 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
View 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
View 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
View 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
View 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,14 +50,16 @@ 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",
// 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",
@@ -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
View 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
}
@@ -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
}
@@ -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
}
@@ -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
View 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 -3
View File
@@ -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"
+23 -4
View File
@@ -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 -2
View File
@@ -2,8 +2,8 @@ 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"
@@ -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
View 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 -1
View File
@@ -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"
+23 -6
View File
@@ -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)))
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"strings"
"time"
"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/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
+2 -2
View File
@@ -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"
@@ -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
View 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"
+2 -2
View File
@@ -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"
+35 -5
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+27 -27
View File
@@ -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()
+2 -7
View File
@@ -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
@@ -120,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 {
+21
View File
@@ -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)
+80
View 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()
}
+13
View File
@@ -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
+4
View File
@@ -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
View 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
}
+4
View File
@@ -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
+214
View 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
View 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
View 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)
}
+10 -4
View File
@@ -64,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}
}
@@ -83,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}
}
+1 -1
View File
@@ -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() {
+14 -5
View File
@@ -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,
}
+8
View File
@@ -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
View 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
View 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
View 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>`
}
+35
View File
@@ -0,0 +1,35 @@
package pcm
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func RepackBackchannel(handler core.HandlerFunc) core.HandlerFunc {
var buf []byte
var seq uint16
return func(packet *rtp.Packet) {
buf = append(buf, packet.Payload...)
if len(buf) < 1024 {
return
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true, // should be true
PayloadType: packet.PayloadType, // will be owerwriten
SequenceNumber: seq,
Timestamp: 0, // should be always zero
SSRC: packet.SSRC,
},
Payload: buf[:1024],
}
handler(pkt)
buf = buf[1024:]
seq++
}
}
+8 -3
View File
@@ -244,11 +244,16 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) {
}
if c.session == "" {
// Session: 7116520596809429228
// Session: 216525287999;timeout=60
if s := res.Header.Get("Session"); s != "" {
c.session, s, _ = strings.Cut(s, ";timeout=")
if s != "" {
c.keepalive, _ = strconv.Atoi(s)
if i := strings.IndexByte(s, ';'); i > 0 {
c.session = s[:i]
if i = strings.Index(s, "timeout="); i > 0 {
c.keepalive, _ = strconv.Atoi(s[i+8:])
}
} else {
c.session = s
}
}
}
+1
View File
@@ -22,6 +22,7 @@ type Conn struct {
// public
Backchannel bool
PacketSize uint16
SessionName string
Medias []*core.Media
+18 -2
View File
@@ -6,6 +6,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/pion/rtp"
"time"
)
@@ -60,6 +61,12 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
sender := core.NewSender(media, codec)
// important to send original codec for valid IsRTP check
sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType)
// https://github.com/AlexxIT/go2rtc/issues/331
if c.mode == core.ModeActiveProducer && track.Codec.Name == core.CodecPCMA {
sender.Handler = pcm.RepackBackchannel(sender.Handler)
}
sender.HandleRTP(track)
c.senders = append(c.senders, sender)
@@ -104,14 +111,23 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.
if !codec.IsRTP() {
switch codec.Name {
case core.CodecH264:
handlerFunc = h264.RTPPay(1500, handlerFunc)
handlerFunc = h264.RTPPay(c.PacketSize, handlerFunc)
case core.CodecH265:
handlerFunc = h265.RTPPay(1500, handlerFunc)
handlerFunc = h265.RTPPay(c.PacketSize, handlerFunc)
case core.CodecAAC:
handlerFunc = aac.RTPPay(handlerFunc)
case core.CodecJPEG:
handlerFunc = mjpeg.RTPPay(handlerFunc)
}
} else if c.PacketSize != 0 {
switch codec.Name {
case core.CodecH264:
handlerFunc = h264.RTPPay(c.PacketSize, handlerFunc)
handlerFunc = h264.RTPDepay(codec, handlerFunc)
case core.CodecH265:
handlerFunc = h265.RTPPay(c.PacketSize, handlerFunc)
handlerFunc = h265.RTPDepay(codec, handlerFunc)
}
}
return handlerFunc
+23 -3
View File
@@ -12,7 +12,7 @@ import (
// Do - http.Client with support Digest Authorization
func Do(req *http.Request) (*http.Response, error) {
if client == nil {
if secureClient == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
dial := transport.DialContext
@@ -24,12 +24,32 @@ func Do(req *http.Request) (*http.Response, error) {
return conn, err
}
client = &http.Client{
secureClient = &http.Client{
Timeout: time.Second * 5000,
Transport: transport,
}
}
var client *http.Client
if req.URL.Scheme == "httpx" {
req.URL.Scheme = "https"
if insecureClient == nil {
transport := secureClient.Transport.(*http.Transport).Clone()
transport.TLSClientConfig.InsecureSkipVerify = true
insecureClient = &http.Client{
Timeout: secureClient.Timeout,
Transport: transport,
}
}
client = insecureClient
} else {
client = secureClient
}
user := req.URL.User
// Hikvision won't answer on Basic auth with any headers
@@ -92,7 +112,7 @@ func Do(req *http.Request) (*http.Response, error) {
return res, nil
}
var client *http.Client
var secureClient, insecureClient *http.Client
var connKey struct{}
func WithConn() (context.Context, *net.Conn) {
+1
View File
@@ -9,6 +9,7 @@ import (
)
// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
// https://ffmpeg.org/ffmpeg-all.html#Muxer
const ReceiveMTU = 1472
func NewAPI(address string) (*webrtc.API, error) {
+52 -13
View File
@@ -60,6 +60,7 @@
<script>
async function getStreams(url, tableID) {
const table = document.getElementById(tableID)
table.innerText = 'loading...'
const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url
if (!r.ok) {
@@ -170,6 +171,32 @@
</script>
<button id="devices">FFmpeg Devices (USB)</button>
<div class="module">
<table id="devices-table">
</table>
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/ffmpeg/devices', 'devices-table')
})
</script>
<button id="hardware">FFmpeg Hardware</button>
<div class="module">
<table id="hardware-table">
</table>
</div>
<script>
document.getElementById('hardware').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/ffmpeg/hardware', 'hardware-table')
})
</script>
<button id="hass">Home Assistant</button>
<div class="module">
<table id="hass-table"></table>
@@ -182,6 +209,31 @@
</script>
<button id="onvif">ONVIF</button>
<div class="module">
<form id="onvif-form" style="padding: 10px">
<input type="text" name="src" placeholder="onvif://user:pass@192.168.1.123:80" size="50">
<input type="submit" value="test">
</form>
<table id="onvif-table"></table>
</div>
<script>
document.getElementById('onvif').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/onvif', 'onvif-table')
})
document.getElementById('onvif-form').addEventListener('submit', async ev => {
ev.preventDefault()
const url = new URL('api/onvif', location.href)
url.searchParams.set('src', ev.target.elements['src'].value)
await getStreams(url.toString(), 'onvif-table')
})
</script>
<button id="roborock">Roborock</button>
<div class="module">
<form id="roborock-form" style="margin-bottom: 10px">
@@ -206,19 +258,6 @@
</script>
<button id="devices">USB Devices</button>
<div class="module">
<table id="devices-table">
</table>
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/devices', 'devices-table')
})
</script>
<button id="webtorrent">WebTorrent Shares</button>
<div class="module">
<table id="webtorrent-table"></table>