├── assets
├── logo.gif
├── logo.png
└── go2rtc.png
├── pkg
├── ioctl
│ ├── README.md
│ ├── ioctl_le.go
│ ├── ioctl_be.go
│ ├── ioctl_linux.go
│ ├── ioctl_test.go
│ └── ioctl.go
├── rtsp
│ └── README.md
├── hap
│ ├── camera
│ │ ├── README.md
│ │ ├── ch120_streaming_status.go
│ │ ├── ch130_data_stream_transport.go
│ │ ├── ch116_supported_rtp.go
│ │ ├── ch209.go
│ │ ├── ch131_data_stream.go
│ │ ├── ch205.go
│ │ ├── ch207.go
│ │ ├── ch206.go
│ │ ├── ch117_selected_stream.go
│ │ ├── ch118_setup_endpoints.go
│ │ ├── ch114_supported_video.go
│ │ └── ch115_supported_audio.go
│ ├── hkdf
│ │ └── hkdf.go
│ ├── setup
│ │ ├── setup_test.go
│ │ └── setup.go
│ ├── curve25519
│ │ └── curve25519.go
│ ├── ed25519
│ │ └── ed25519.go
│ ├── hds
│ │ └── hds_test.go
│ └── chacha20poly1305
│ │ └── chacha20poly1305.go
├── mdns
│ ├── README.md
│ ├── syscall_windows.go
│ ├── mdns_test.go
│ ├── syscall.go
│ └── syscall_bsd.go
├── shell
│ ├── procattr.go
│ ├── procattr_linux.go
│ ├── shell_test.go
│ ├── shell.go
│ └── command.go
├── mjpeg
│ ├── README.md
│ ├── mjpeg_test.go
│ ├── jpeg.go
│ ├── writer.go
│ └── consumer.go
├── creds
│ ├── README.md
│ ├── secrets_test.go
│ ├── secrets.go
│ └── creds.go
├── ascii
│ └── README.md
├── opus
│ └── README.md
├── core
│ ├── listener.go
│ ├── worker.go
│ ├── slices.go
│ ├── track_test.go
│ ├── README.md
│ ├── waiter.go
│ ├── readbuffer_test.go
│ ├── media_test.go
│ └── node.go
├── expr
│ └── expr_test.go
├── h265
│ ├── README.md
│ ├── h265_test.go
│ ├── avc.go
│ ├── avcc.go
│ └── helper.go
├── webrtc
│ ├── README.md
│ ├── producer.go
│ └── track.go
├── wyoming
│ ├── README.md
│ ├── wyoming.go
│ ├── mic.go
│ ├── snd.go
│ ├── producer.go
│ └── backchannel.go
├── hls
│ └── producer.go
├── tuya
│ ├── README.md
│ └── helper.go
├── alsa
│ ├── device
│ │ └── ioctl_linux.go
│ ├── README.md
│ └── open_linux.go
├── tcp
│ ├── textproto_test.go
│ ├── dial.go
│ └── websocket
│ │ └── dial.go
├── flv
│ └── flv_test.go
├── v4l2
│ └── device
│ │ ├── README.md
│ │ └── formats.go
├── pcm
│ ├── s16le
│ │ └── s16le.go
│ ├── pcmu.go
│ ├── v1
│ │ └── pcm_test.go
│ ├── pcma.go
│ ├── producer.go
│ └── backchannel.go
├── y4m
│ ├── README.md
│ ├── consumer.go
│ └── producer.go
├── aac
│ ├── README.md
│ ├── consumer.go
│ ├── aac_test.go
│ ├── rtp_test.go
│ └── producer.go
├── gopro
│ └── discovery.go
├── mpegts
│ ├── README.md
│ └── opus.go
├── mp4
│ ├── mime.go
│ └── README.md
├── dvrip
│ ├── dvrip.go
│ └── backchannel.go
├── roborock
│ └── producer.go
├── homekit
│ └── log
│ │ └── debug.go
├── h264
│ └── README.md
├── probe
│ └── consumer.go
├── mpjpeg
│ ├── multipart.go
│ └── producer.go
├── debug
│ └── debug.go
├── ring
│ └── snapshot.go
├── onvif
│ └── README.md
├── rtmp
│ └── README.md
├── xnet
│ ├── net.go
│ └── tls
│ │ └── tls.go
├── ngrok
│ └── ngrok.go
├── isapi
│ └── backchannel.go
├── wav
│ ├── backchannel.go
│ └── producer.go
├── webtorrent
│ └── crypto.go
├── magic
│ ├── producer.go
│ └── mjpeg
│ │ └── producer.go
├── tapo
│ └── backchannel.go
├── bubble
│ └── producer.go
├── bits
│ └── writer.go
└── image
│ └── producer.go
├── internal
├── hls
│ ├── README.md
│ └── ws.go
├── alsa
│ ├── alsa.go
│ └── alsa_linux.go
├── v4l2
│ ├── v4l2.go
│ └── README.md
├── api
│ ├── README.md
│ └── static.go
├── debug
│ ├── debug.go
│ └── stack.go
├── ivideon
│ └── ivideon.go
├── eseecloud
│ └── eseecloud.go
├── flussonic
│ └── flussonic.go
├── isapi
│ └── init.go
├── bubble
│ └── bubble.go
├── streams
│ ├── helpers.go
│ ├── publish.go
│ ├── stream_test.go
│ └── preload.go
├── exec
│ └── README.md
├── ffmpeg
│ ├── virtual
│ │ ├── virtual_test.go
│ │ └── virtual.go
│ ├── jpeg_test.go
│ ├── device
│ │ └── devices.go
│ ├── version.go
│ ├── hardware
│ │ ├── hardware_darwin.go
│ │ ├── hardware_bsd.go
│ │ └── hardware_windows.go
│ └── api.go
├── tapo
│ └── tapo.go
├── srtp
│ └── srtp.go
├── expr
│ └── expr.go
├── onvif
│ └── README.md
├── gopro
│ ├── gopro.go
│ └── README.md
├── mpegts
│ ├── aac.go
│ └── mpegts.go
├── yandex
│ ├── README.md
│ └── yandex.go
├── doorbird
│ └── doorbird.go
├── echo
│ └── echo.go
├── webrtc
│ └── switchbot.go
├── app
│ └── storage.go
├── pinggy
│ ├── pinggy.go
│ └── README.md
├── nest
│ └── init.go
├── xiaomi
│ └── README.md
├── rtmp
│ └── README.md
├── mjpeg
│ └── README.md
├── mp4
│ └── ws.go
└── ngrok
│ └── ngrok.go
├── website
├── icons
│ ├── favicon.ico
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ └── apple-touch-icon-180x180.png
├── manifest.json
└── api
│ └── index.html
├── www
├── static.go
└── hls.html
├── examples
├── onvif_client
│ └── README.md
├── mod_pinggy
│ ├── go.mod
│ └── main.go
├── go2rtc_rtsp
│ └── main.go
├── go2rtc_hass
│ └── main.go
├── go2rtc_mjpeg
│ └── main.go
├── mdns
│ └── main.go
└── rtsp_client
│ └── main.go
├── .gitignore
├── package.json
├── .github
└── workflows
│ └── gh-pages.yml
├── LICENSE
├── docker
├── Dockerfile
├── rockchip.Dockerfile
└── README.md
├── scripts
└── build.sh
└── go.mod
/assets/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/assets/logo.gif
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/go2rtc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/assets/go2rtc.png
--------------------------------------------------------------------------------
/pkg/ioctl/README.md:
--------------------------------------------------------------------------------
1 | # IOCTL
2 |
3 | This is just an example how Linux IOCTL constants works.
4 |
--------------------------------------------------------------------------------
/pkg/rtsp/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - https://www.kurento.org/blog/rtp-i-intro-rtp-and-sdp
--------------------------------------------------------------------------------
/internal/hls/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - https://walterebert.com/playground/video/hls/
4 |
--------------------------------------------------------------------------------
/website/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/website/icons/favicon.ico
--------------------------------------------------------------------------------
/pkg/hap/camera/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - https://github.com/bauer-andreas/secure-video-specification
4 |
--------------------------------------------------------------------------------
/pkg/ioctl/ioctl_le.go:
--------------------------------------------------------------------------------
1 | //go:build mipsle
2 |
3 | package ioctl
4 |
5 | const (
6 | read = 1
7 | write = 2
8 | )
9 |
--------------------------------------------------------------------------------
/www/static.go:
--------------------------------------------------------------------------------
1 | package www
2 |
3 | import "embed"
4 |
5 | //go:embed *.html
6 | //go:embed *.js
7 | var Static embed.FS
8 |
--------------------------------------------------------------------------------
/pkg/mdns/README.md:
--------------------------------------------------------------------------------
1 | # Useful links
2 |
3 | - https://grouper.ieee.org/groups/1722/contributions/2009/Bonjour%20Device%20Discovery.pdf
--------------------------------------------------------------------------------
/website/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/website/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/website/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/website/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/pkg/shell/procattr.go:
--------------------------------------------------------------------------------
1 | //go:build !linux
2 |
3 | package shell
4 |
5 | import "syscall"
6 |
7 | var procAttr *syscall.SysProcAttr
8 |
--------------------------------------------------------------------------------
/website/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexxIT/go2rtc/HEAD/website/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/pkg/ioctl/ioctl_be.go:
--------------------------------------------------------------------------------
1 | //go:build arm || arm64 || 386 || amd64
2 |
3 | package ioctl
4 |
5 | const (
6 | write = 1
7 | read = 2
8 | )
9 |
--------------------------------------------------------------------------------
/internal/alsa/alsa.go:
--------------------------------------------------------------------------------
1 | //go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))
2 |
3 | package alsa
4 |
5 | func Init() {
6 | // not supported
7 | }
8 |
--------------------------------------------------------------------------------
/internal/v4l2/v4l2.go:
--------------------------------------------------------------------------------
1 | //go:build !(linux && (386 || arm || mipsle || amd64 || arm64))
2 |
3 | package v4l2
4 |
5 | func Init() {
6 | // not supported
7 | }
8 |
--------------------------------------------------------------------------------
/examples/onvif_client/README.md:
--------------------------------------------------------------------------------
1 | ## Example
2 |
3 | ```shell
4 | go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations
5 | ```
--------------------------------------------------------------------------------
/internal/api/README.md:
--------------------------------------------------------------------------------
1 | ## Exit codes
2 |
3 | - https://tldp.org/LDP/abs/html/exitcodes.html
4 | - https://komodor.com/learn/exit-codes-in-containers-and-kubernetes-the-complete-guide/
5 |
--------------------------------------------------------------------------------
/internal/debug/debug.go:
--------------------------------------------------------------------------------
1 | package debug
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/api"
5 | )
6 |
7 | func Init() {
8 | api.HandleFunc("api/stack", stackHandler)
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/shell/procattr_linux.go:
--------------------------------------------------------------------------------
1 | package shell
2 |
3 | import "syscall"
4 |
5 | // will stop child if parent died (even with SIGKILL)
6 | var procAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM}
7 |
--------------------------------------------------------------------------------
/pkg/mjpeg/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - https://www.rfc-editor.org/rfc/rfc2435
4 | - https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
5 | - https://mjpeg.sanford.io/
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .tmp/
3 |
4 | go2rtc.yaml
5 | go2rtc.json
6 |
7 | go2rtc_freebsd*
8 | go2rtc_linux*
9 | go2rtc_mac*
10 | go2rtc_win*
11 |
12 | /go2rtc
13 | /go2rtc.exe
14 |
15 | 0_test.go
16 |
17 | .DS_Store
18 |
--------------------------------------------------------------------------------
/pkg/creds/README.md:
--------------------------------------------------------------------------------
1 | # Credentials
2 |
3 | This module allows you to get variables:
4 |
5 | - from custom storage (ex. config file)
6 | - from [credential files](https://systemd.io/CREDENTIALS/)
7 | - from environment variables
8 |
--------------------------------------------------------------------------------
/examples/mod_pinggy/go.mod:
--------------------------------------------------------------------------------
1 | module pinggy
2 |
3 | go 1.25
4 |
5 | require (
6 | github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 // indirect
7 | golang.org/x/crypto v0.8.0 // indirect
8 | golang.org/x/sys v0.7.0 // indirect
9 | )
10 |
--------------------------------------------------------------------------------
/pkg/ascii/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - https://en.wikipedia.org/wiki/ANSI_escape_code
4 | - https://paulbourke.net/dataformats/asciiart/
5 | - https://github.com/kutuluk/xterm-color-chart
6 | - https://github.com/hugomd/parrot.live
7 |
--------------------------------------------------------------------------------
/internal/ivideon/ivideon.go:
--------------------------------------------------------------------------------
1 | package ivideon
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/streams"
5 | "github.com/AlexxIT/go2rtc/pkg/ivideon"
6 | )
7 |
8 | func Init() {
9 | streams.HandleFunc("ivideon", ivideon.Dial)
10 | }
11 |
--------------------------------------------------------------------------------
/internal/eseecloud/eseecloud.go:
--------------------------------------------------------------------------------
1 | package eseecloud
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/streams"
5 | "github.com/AlexxIT/go2rtc/pkg/eseecloud"
6 | )
7 |
8 | func Init() {
9 | streams.HandleFunc("eseecloud", eseecloud.Dial)
10 | }
11 |
--------------------------------------------------------------------------------
/internal/flussonic/flussonic.go:
--------------------------------------------------------------------------------
1 | package flussonic
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/streams"
5 | "github.com/AlexxIT/go2rtc/pkg/flussonic"
6 | )
7 |
8 | func Init() {
9 | streams.HandleFunc("flussonic", flussonic.Dial)
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/mjpeg/mjpeg_test.go:
--------------------------------------------------------------------------------
1 | package mjpeg
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestRFC2435(t *testing.T) {
10 | lqt, cqt := MakeTables(71)
11 | require.Equal(t, byte(9), lqt[0])
12 | require.Equal(t, byte(10), cqt[0])
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/ioctl/ioctl_linux.go:
--------------------------------------------------------------------------------
1 | package ioctl
2 |
3 | import (
4 | "syscall"
5 | "unsafe"
6 | )
7 |
8 | func Ioctl(fd int, req uint, arg unsafe.Pointer) error {
9 | _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
10 | if err != 0 {
11 | return err
12 | }
13 | return nil
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/mjpeg/jpeg.go:
--------------------------------------------------------------------------------
1 | package mjpeg
2 |
3 | const (
4 | markerSOF = 0xC0 // Start Of Frame (Baseline Sequential)
5 | markerSOI = 0xD8 // Start Of Image
6 | markerEOI = 0xD9 // End Of Image
7 | markerSOS = 0xDA // Start Of Scan
8 | markerDQT = 0xDB // Define Quantization Table
9 | markerDHT = 0xC4 // Define Huffman Table
10 | )
11 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch120_streaming_status.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeStreamingStatus = "120"
4 |
5 | type StreamingStatus struct {
6 | Status byte `tlv8:"1"`
7 | }
8 |
9 | //goland:noinspection ALL
10 | const (
11 | StreamingStatusAvailable = 0
12 | StreamingStatusInUse = 1
13 | StreamingStatusUnavailable = 2
14 | )
15 |
--------------------------------------------------------------------------------
/internal/isapi/init.go:
--------------------------------------------------------------------------------
1 | package isapi
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/streams"
5 | "github.com/AlexxIT/go2rtc/pkg/core"
6 | "github.com/AlexxIT/go2rtc/pkg/isapi"
7 | )
8 |
9 | func Init() {
10 | streams.HandleFunc("isapi", func(source string) (core.Producer, error) {
11 | return isapi.Dial(source)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/internal/bubble/bubble.go:
--------------------------------------------------------------------------------
1 | package bubble
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/streams"
5 | "github.com/AlexxIT/go2rtc/pkg/bubble"
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | )
8 |
9 | func Init() {
10 | streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
11 | return bubble.Dial(source)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch130_data_stream_transport.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedDataStreamTransportConfiguration = "130"
4 |
5 | type SupportedDataStreamTransportConfiguration struct {
6 | Configs []TransferTransportConfiguration `tlv8:"1"`
7 | }
8 |
9 | type TransferTransportConfiguration struct {
10 | TransportType byte `tlv8:"1"`
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/hap/hkdf/hkdf.go:
--------------------------------------------------------------------------------
1 | package hkdf
2 |
3 | import (
4 | "crypto/sha512"
5 | "io"
6 |
7 | "golang.org/x/crypto/hkdf"
8 | )
9 |
10 | func Sha512(key []byte, salt, info string) ([]byte, error) {
11 | r := hkdf.New(sha512.New, key, []byte(salt), []byte(info))
12 |
13 | buf := make([]byte, 32)
14 | _, err := io.ReadFull(r, buf)
15 |
16 | return buf, err
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/opus/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - [RFC 3550: RTP: A Transport Protocol for Real-Time Applications](https://datatracker.ietf.org/doc/html/rfc3550)
4 | - [RFC 6716: Definition of the Opus Audio Codec](https://datatracker.ietf.org/doc/html/rfc6716)
5 | - [RFC 7587: RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)
6 |
--------------------------------------------------------------------------------
/pkg/creds/secrets_test.go:
--------------------------------------------------------------------------------
1 | package creds
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestString(t *testing.T) {
10 | AddSecret("admin")
11 | AddSecret("pa$$word")
12 |
13 | s := SecretString("rtsp://admin:pa$$word@192.168.1.123/stream1")
14 | require.Equal(t, "rtsp://***:***@192.168.1.123/stream1", s)
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch116_supported_rtp.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedRTPConfiguration = "116"
4 |
5 | //goland:noinspection ALL
6 | const (
7 | CryptoAES_CM_128_HMAC_SHA1_80 = 0
8 | CryptoAES_CM_256_HMAC_SHA1_80 = 1
9 | CryptoDisabled = 2
10 | )
11 |
12 | type SupportedRTPConfiguration struct {
13 | SRTPCryptoType []byte `tlv8:"2"`
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch209.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSelectedCameraRecordingConfiguration = "209"
4 |
5 | type SelectedCameraRecordingConfiguration struct {
6 | GeneralConfig SupportedCameraRecordingConfiguration `tlv8:"1"`
7 | VideoConfig SupportedVideoRecordingConfiguration `tlv8:"2"`
8 | AudioConfig SupportedAudioRecordingConfiguration `tlv8:"3"`
9 | }
10 |
--------------------------------------------------------------------------------
/examples/go2rtc_rtsp/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/app"
5 | "github.com/AlexxIT/go2rtc/internal/rtsp"
6 | "github.com/AlexxIT/go2rtc/internal/streams"
7 | "github.com/AlexxIT/go2rtc/pkg/shell"
8 | )
9 |
10 | func main() {
11 | app.Init()
12 | streams.Init()
13 |
14 | rtsp.Init()
15 |
16 | shell.RunUntilSignal()
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/ioctl/ioctl_test.go:
--------------------------------------------------------------------------------
1 | package ioctl
2 |
3 | import (
4 | "runtime"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestIOR(t *testing.T) {
11 | // #define SNDRV_PCM_IOCTL_INFO _IOR('A', 0x01, struct snd_pcm_info)
12 | if runtime.GOARCH == "arm64" {
13 | c := IOR('A', 0x01, 288)
14 | require.Equal(t, uintptr(0x81204101), c)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/core/listener.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | type EventFunc func(msg any)
4 |
5 | // Listener base struct for all classes with support feedback
6 | type Listener struct {
7 | events []EventFunc
8 | }
9 |
10 | func (l *Listener) Listen(f EventFunc) {
11 | l.events = append(l.events, f)
12 | }
13 |
14 | func (l *Listener) Fire(msg any) {
15 | for _, f := range l.events {
16 | f(msg)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/expr/expr_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestMatchHost(t *testing.T) {
10 | v, err := Eval(`
11 | let url = "rtsp://user:pass@192.168.1.123/cam/realmonitor?...";
12 | let host = match(url, "//[^/]+")[0][2:];
13 | host
14 | `, nil)
15 | require.Nil(t, err)
16 | require.Equal(t, "user:pass@192.168.1.123", v)
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/h265/README.md:
--------------------------------------------------------------------------------
1 | # H265
2 |
3 | Payloader code taken from [pion](https://github.com/pion/rtp) library branch [h265](https://github.com/pion/rtp/tree/h265). Because it's still not in release. Thanks to [@kevmo314](https://github.com/kevmo314).
4 |
5 | ## Useful links
6 |
7 | - https://datatracker.ietf.org/doc/html/rfc7798
8 | - [Add initial support for WebRTC HEVC](https://trac.webkit.org/changeset/259452/webkit)
9 |
--------------------------------------------------------------------------------
/pkg/webrtc/README.md:
--------------------------------------------------------------------------------
1 | ## StateChange
2 |
3 | 1. offer = pc.CreateOffer()
4 | 2. pc.SetLocalDescription(offer)
5 | 3. OnICEGatheringStateChange: gathering
6 | 4. OnSignalingStateChange: have-local-offer
7 | *. OnICEGatheringStateChange: complete
8 | 5. pc.SetRemoteDescription(answer)
9 | 6. OnSignalingStateChange: stable
10 | 7. OnICEConnectionStateChange: checking
11 | 8. OnICEConnectionStateChange: connected
12 |
--------------------------------------------------------------------------------
/pkg/hap/setup/setup_test.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestFormatAlphaNum(t *testing.T) {
13 | value := int64(999)
14 | n := 5
15 | s1 := strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36)))
16 | s2 := FormatInt36(value, n)
17 | require.Equal(t, s1, s2)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/mdns/syscall_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package mdns
4 |
5 | import "syscall"
6 |
7 | func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
8 | return syscall.SetsockoptInt(syscall.Handle(fd), level, opt, value)
9 | }
10 |
11 | func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
12 | return syscall.SetsockoptIPMreq(syscall.Handle(fd), level, opt, mreq)
13 | }
14 |
--------------------------------------------------------------------------------
/examples/go2rtc_hass/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/api"
5 | "github.com/AlexxIT/go2rtc/internal/app"
6 | "github.com/AlexxIT/go2rtc/internal/hass"
7 | "github.com/AlexxIT/go2rtc/internal/streams"
8 | "github.com/AlexxIT/go2rtc/pkg/shell"
9 | )
10 |
11 | func main() {
12 | app.Init()
13 | streams.Init()
14 |
15 | api.Init()
16 |
17 | hass.Init()
18 |
19 | shell.RunUntilSignal()
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/wyoming/README.md:
--------------------------------------------------------------------------------
1 | ## Default wake words
2 |
3 | - alexa_v0.1
4 | - hey_jarvis_v0.1
5 | - hey_mycroft_v0.1
6 | - hey_rhasspy_v0.1
7 | - ok_nabu_v0.1
8 |
9 | ## Useful Links
10 |
11 | - https://github.com/rhasspy/wyoming-satellite
12 | - https://github.com/rhasspy/wyoming-openwakeword
13 | - https://github.com/fwartner/home-assistant-wakewords-collection
14 | - https://github.com/esphome/micro-wake-word-models/tree/main?tab=readme-ov-file
15 |
--------------------------------------------------------------------------------
/pkg/mdns/mdns_test.go:
--------------------------------------------------------------------------------
1 | package mdns
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestDiscovery(t *testing.T) {
10 | onentry := func(entry *ServiceEntry) bool {
11 | return true
12 | }
13 | err := Discovery(ServiceHAP, onentry)
14 | //err := Discovery("_ewelink._tcp.local.", time.Second, onentry)
15 | // err := Discovery("_googlecast._tcp.local.", time.Second, onentry)
16 | require.Nil(t, err)
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/mdns/syscall.go:
--------------------------------------------------------------------------------
1 | //go:build !(darwin || ios || freebsd || openbsd || netbsd || dragonfly || windows)
2 |
3 | package mdns
4 |
5 | import (
6 | "syscall"
7 | )
8 |
9 | func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
10 | return syscall.SetsockoptInt(int(fd), level, opt, value)
11 | }
12 |
13 | func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
14 | return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
15 | }
16 |
--------------------------------------------------------------------------------
/internal/streams/helpers.go:
--------------------------------------------------------------------------------
1 | package streams
2 |
3 | import (
4 | "net/url"
5 | "strings"
6 | )
7 |
8 | func ParseQuery(s string) url.Values {
9 | if len(s) == 0 {
10 | return nil
11 | }
12 | params := url.Values{}
13 | for _, key := range strings.Split(s, "#") {
14 | var value string
15 | i := strings.IndexByte(key, '=')
16 | if i > 0 {
17 | key, value = key[:i], key[i+1:]
18 | }
19 | params[key] = append(params[key], value)
20 | }
21 | return params
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/hls/producer.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "io"
5 | "net/url"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/mpegts"
8 | )
9 |
10 | func OpenURL(u *url.URL, body io.ReadCloser) (*mpegts.Producer, error) {
11 | rd, err := NewReader(u, body)
12 | if err != nil {
13 | return nil, err
14 | }
15 | prod, err := mpegts.Open(rd)
16 | if err != nil {
17 | return nil, err
18 | }
19 | prod.FormatName = "hls/mpegts"
20 | prod.RemoteAddr = u.Host
21 | return prod, nil
22 | }
23 |
--------------------------------------------------------------------------------
/website/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "go2rtc",
3 | "icons": [
4 | {
5 | "src": "https://go2rtc.org/icons/android-chrome-192x192.png",
6 | "sizes": "192x192",
7 | "type": "image/png"
8 | },
9 | {
10 | "src": "https://go2rtc.org/icons/android-chrome-512x512.png",
11 | "sizes": "512x512",
12 | "type": "image/png"
13 | }
14 | ],
15 | "display": "standalone",
16 | "theme_color": "#000000",
17 | "background_color": "#000000"
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/h265/h265_test.go:
--------------------------------------------------------------------------------
1 | package h265
2 |
3 | import (
4 | "encoding/base64"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestDecodeSPS(t *testing.T) {
11 | s := "QgEBAWAAAAMAAAMAAAMAAAMAmaAAoAgBaH+KrTuiS7/8AAQABbAgApMuADN/mAE="
12 | b, err := base64.StdEncoding.DecodeString(s)
13 | require.Nil(t, err)
14 |
15 | sps := DecodeSPS(b)
16 | require.NotNil(t, sps)
17 | require.Equal(t, uint16(5120), sps.Width())
18 | require.Equal(t, uint16(1440), sps.Height())
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/hap/curve25519/curve25519.go:
--------------------------------------------------------------------------------
1 | package curve25519
2 |
3 | import (
4 | "crypto/rand"
5 |
6 | "golang.org/x/crypto/curve25519"
7 | )
8 |
9 | func GenerateKeyPair() ([]byte, []byte) {
10 | var publicKey, privateKey [32]byte
11 | _, _ = rand.Read(privateKey[:])
12 | curve25519.ScalarBaseMult(&publicKey, &privateKey)
13 | return publicKey[:], privateKey[:]
14 | }
15 |
16 | func SharedSecret(privateKey, otherPublicKey []byte) ([]byte, error) {
17 | return curve25519.X25519(privateKey, otherPublicKey)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/tuya/README.md:
--------------------------------------------------------------------------------
1 | ## Useful links
2 |
3 | - https://developer.tuya.com/en/docs/iot/webrtc?id=Kacsd4x2hl0se
4 | - https://github.com/tuya/webrtc-demo-go
5 | - https://github.com/bacco007/HomeAssistantConfig/blob/master/custom_components/xtend_tuya/multi_manager/tuya_iot/ipc/webrtc/xt_tuya_iot_webrtc_manager.py
6 | - https://github.com/tuya/tuya-device-sharing-sdk
7 | - https://github.com/make-all/tuya-local/blob/main/custom_components/tuya_local/cloud.py
8 | - https://ipc-us.ismartlife.me/
9 | - https://protect-us.ismartlife.me/
--------------------------------------------------------------------------------
/pkg/alsa/device/ioctl_linux.go:
--------------------------------------------------------------------------------
1 | package device
2 |
3 | import (
4 | "bytes"
5 | "reflect"
6 | "syscall"
7 | )
8 |
9 | func ioctl(fd, req uintptr, arg any) error {
10 | var ptr uintptr
11 | if arg != nil {
12 | ptr = reflect.ValueOf(arg).Pointer()
13 | }
14 | _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, req, ptr)
15 | if err != 0 {
16 | return err
17 | }
18 | return nil
19 | }
20 |
21 | func str(b []byte) string {
22 | if i := bytes.IndexByte(b, 0); i >= 0 {
23 | return string(b[:i])
24 | }
25 | return string(b)
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch131_data_stream.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSetupDataStreamTransport = "131"
4 |
5 | type SetupDataStreamTransportRequest struct {
6 | SessionCommandType byte `tlv8:"1"`
7 | TransportType byte `tlv8:"2"`
8 | ControllerKeySalt string `tlv8:"3"`
9 | }
10 |
11 | type SetupDataStreamTransportResponse struct {
12 | Status byte `tlv8:"1"`
13 | TransportTypeSessionParameters struct {
14 | TCPListeningPort uint16 `tlv8:"1"`
15 | } `tlv8:"2"`
16 | AccessoryKeySalt string `tlv8:"3"`
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/shell/shell_test.go:
--------------------------------------------------------------------------------
1 | package shell
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQuoteSplit(t *testing.T) {
10 | s := `
11 | python "-c" 'import time
12 | print("time", time.time())'
13 | `
14 | require.Equal(t, []string{"python", "-c", "import time\nprint(\"time\", time.time())"}, QuoteSplit(s))
15 |
16 | s = `ffmpeg -i "video=FaceTime HD Camera" -i "DeckLink SDI (2)"`
17 | require.Equal(t, []string{"ffmpeg", "-i", `video=FaceTime HD Camera`, "-i", "DeckLink SDI (2)"}, QuoteSplit(s))
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch205.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedCameraRecordingConfiguration = "205"
4 |
5 | type SupportedCameraRecordingConfiguration struct {
6 | PrebufferLength uint32 `tlv8:"1"`
7 | EventTriggerOptions uint64 `tlv8:"2"`
8 | MediaContainerConfigurations `tlv8:"3"`
9 | }
10 |
11 | type MediaContainerConfigurations struct {
12 | MediaContainerType uint8 `tlv8:"1"`
13 | MediaContainerParameters `tlv8:"2"`
14 | }
15 |
16 | type MediaContainerParameters struct {
17 | FragmentLength uint32 `tlv8:"1"`
18 | }
19 |
--------------------------------------------------------------------------------
/internal/exec/README.md:
--------------------------------------------------------------------------------
1 | ## Backchannel
2 |
3 | - You can check audio card names in the **Go2rtc > WebUI > Add**
4 | - You can specify multiple backchannel lines with different codecs
5 |
6 | ```yaml
7 | sources:
8 | two_way_audio_win:
9 | - exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
10 | - exec:ffplay -nodisp -probesize 32 -f s16le -ar 16000 -#backchannel=1#audio=s16le/16000
11 | - exec:ffplay -nodisp -probesize 32 -f alaw -ar 8000 -#backchannel=1#audio=alaw/8000
12 | ```
13 |
--------------------------------------------------------------------------------
/pkg/wyoming/wyoming.go:
--------------------------------------------------------------------------------
1 | package wyoming
2 |
3 | import (
4 | "net"
5 | "net/url"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | )
9 |
10 | func Dial(rawURL string) (core.Producer, error) {
11 | u, err := url.Parse(rawURL)
12 | if err != nil {
13 | return nil, err
14 | }
15 |
16 | conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
17 | if err != nil {
18 | return nil, err
19 | }
20 |
21 | if u.Query().Get("backchannel") != "1" {
22 | return newProducer(conn), nil
23 | } else {
24 | return newBackchannel(conn), nil
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/internal/ffmpeg/virtual/virtual_test.go:
--------------------------------------------------------------------------------
1 | package virtual
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestGetInput(t *testing.T) {
10 | s := GetInput("video")
11 | require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s)
12 |
13 | s = GetInput("video=testsrc2&size=4K")
14 | require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s)
15 | }
16 |
17 | func TestGetInputTTS(t *testing.T) {
18 | s := GetInputTTS("text=hello world&voice=slt")
19 | require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s)
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/hap/ed25519/ed25519.go:
--------------------------------------------------------------------------------
1 | package ed25519
2 |
3 | import (
4 | "crypto/ed25519"
5 | "errors"
6 | )
7 |
8 | var ErrInvalidParams = errors.New("ed25519: invalid params")
9 |
10 | func ValidateSignature(key, data, signature []byte) bool {
11 | if len(key) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize {
12 | return false
13 | }
14 |
15 | return ed25519.Verify(key, data, signature)
16 | }
17 |
18 | func Signature(key, data []byte) ([]byte, error) {
19 | if len(key) != ed25519.PrivateKeySize {
20 | return nil, ErrInvalidParams
21 | }
22 |
23 | return ed25519.Sign(key, data), nil
24 | }
25 |
--------------------------------------------------------------------------------
/internal/tapo/tapo.go:
--------------------------------------------------------------------------------
1 | package tapo
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/streams"
5 | "github.com/AlexxIT/go2rtc/pkg/core"
6 | "github.com/AlexxIT/go2rtc/pkg/kasa"
7 | "github.com/AlexxIT/go2rtc/pkg/tapo"
8 | )
9 |
10 | func Init() {
11 | streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
12 | return kasa.Dial(source)
13 | })
14 |
15 | streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
16 | return tapo.Dial(source)
17 | })
18 |
19 | streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
20 | return tapo.Dial(source)
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/internal/srtp/srtp.go:
--------------------------------------------------------------------------------
1 | package srtp
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/app"
5 | "github.com/AlexxIT/go2rtc/pkg/srtp"
6 | )
7 |
8 | func Init() {
9 | var cfg struct {
10 | Mod struct {
11 | Listen string `yaml:"listen"`
12 | } `yaml:"srtp"`
13 | }
14 |
15 | // default config
16 | cfg.Mod.Listen = ":8443"
17 |
18 | // load config from YAML
19 | app.LoadConfig(&cfg)
20 |
21 | if cfg.Mod.Listen == "" {
22 | return
23 | }
24 |
25 | // create SRTP server (endpoint) for receiving video from HomeKit cameras
26 | Server = srtp.NewServer(cfg.Mod.Listen)
27 | }
28 |
29 | var Server *srtp.Server
30 |
--------------------------------------------------------------------------------
/internal/api/static.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/AlexxIT/go2rtc/www"
7 | )
8 |
9 | func initStatic(staticDir string) {
10 | var root http.FileSystem
11 | if staticDir != "" {
12 | log.Info().Str("dir", staticDir).Msg("[api] serve static")
13 | root = http.Dir(staticDir)
14 | } else {
15 | root = http.FS(www.Static)
16 | }
17 |
18 | base := len(basePath)
19 | fileServer := http.FileServer(root)
20 |
21 | HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
22 | if base > 0 {
23 | r.URL.Path = r.URL.Path[base:]
24 | }
25 | fileServer.ServeHTTP(w, r)
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch207.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedAudioRecordingConfiguration = "207"
4 |
5 | type SupportedAudioRecordingConfiguration struct {
6 | CodecConfigs []AudioRecordingCodecConfiguration `tlv8:"1"`
7 | }
8 |
9 | type AudioRecordingCodecConfiguration struct {
10 | CodecType byte `tlv8:"1"`
11 | CodecParams []AudioRecordingCodecParameters `tlv8:"2"`
12 | }
13 |
14 | type AudioRecordingCodecParameters struct {
15 | Channels uint8 `tlv8:"1"`
16 | BitrateMode []byte `tlv8:"2"`
17 | SampleRate []byte `tlv8:"3"`
18 | MaxAudioBitrate []uint32 `tlv8:"4"`
19 | }
20 |
--------------------------------------------------------------------------------
/examples/go2rtc_mjpeg/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/internal/api"
5 | "github.com/AlexxIT/go2rtc/internal/api/ws"
6 | "github.com/AlexxIT/go2rtc/internal/app"
7 | "github.com/AlexxIT/go2rtc/internal/ffmpeg"
8 | "github.com/AlexxIT/go2rtc/internal/mjpeg"
9 | "github.com/AlexxIT/go2rtc/internal/streams"
10 | "github.com/AlexxIT/go2rtc/internal/v4l2"
11 | "github.com/AlexxIT/go2rtc/pkg/shell"
12 | )
13 |
14 | func main() {
15 | app.Init()
16 | streams.Init()
17 |
18 | api.Init()
19 | ws.Init()
20 |
21 | ffmpeg.Init()
22 | mjpeg.Init()
23 | v4l2.Init()
24 |
25 | shell.RunUntilSignal()
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/tcp/textproto_test.go:
--------------------------------------------------------------------------------
1 | package tcp
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "net/http"
7 | "testing"
8 | )
9 |
10 | func assert(t *testing.T, one, two any) {
11 | if one != two {
12 | t.FailNow()
13 | }
14 | }
15 |
16 | func TestName(t *testing.T) {
17 | data := []byte(`RTSP/1.0 401 Unauthorized
18 | WWW-Authenticate: Digest realm="testrealm@host.com",
19 | nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
20 |
21 | `)
22 |
23 | buf := bytes.NewBuffer(data)
24 | r := bufio.NewReader(buf)
25 |
26 | res, err := ReadResponse(r)
27 | assert(t, err, nil)
28 |
29 | assert(t, res.StatusCode, http.StatusUnauthorized)
30 | }
31 |
--------------------------------------------------------------------------------
/internal/expr/expr.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/app"
7 | "github.com/AlexxIT/go2rtc/internal/streams"
8 | "github.com/AlexxIT/go2rtc/pkg/expr"
9 | )
10 |
11 | func Init() {
12 | log := app.GetLogger("expr")
13 |
14 | streams.RedirectFunc("expr", func(url string) (string, error) {
15 | v, err := expr.Eval(url[5:], nil)
16 | if err != nil {
17 | return "", err
18 | }
19 |
20 | log.Debug().Msgf("[expr] url=%s", url)
21 |
22 | if url = v.(string); url == "" {
23 | return "", errors.New("expr: result is empty")
24 | }
25 |
26 | return url, nil
27 | })
28 | streams.MarkInsecure("expr")
29 | }
30 |
--------------------------------------------------------------------------------
/internal/onvif/README.md:
--------------------------------------------------------------------------------
1 | # ONVIF
2 |
3 | A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
4 |
5 | Go2rtc has one video source and one profile per stream.
6 |
7 | ## Tested clients
8 |
9 | Go2rtc works as ONVIF server:
10 |
11 | - Happytime onvif client (windows)
12 | - Home Assistant ONVIF integration (linux)
13 | - Onvier (android)
14 | - ONVIF Device Manager (windows)
15 |
16 | PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
17 |
18 | ## Tested cameras
19 |
20 | Go2rtc works as ONVIF client:
21 |
22 | - Dahua IPC-K42
23 | - OpenIPC
24 | - Reolink RLC-520A
25 | - TP-Link Tapo TC60
26 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch206.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedVideoRecordingConfiguration = "206"
4 |
5 | type SupportedVideoRecordingConfiguration struct {
6 | CodecConfigs []VideoRecordingCodecConfiguration `tlv8:"1"`
7 | }
8 |
9 | type VideoRecordingCodecConfiguration struct {
10 | CodecType uint8 `tlv8:"1"`
11 | CodecParams VideoRecordingCodecParameters `tlv8:"2"`
12 | CodecAttrs VideoCodecAttributes `tlv8:"3"`
13 | }
14 |
15 | type VideoRecordingCodecParameters struct {
16 | ProfileID uint8 `tlv8:"1"`
17 | Level uint8 `tlv8:"2"`
18 | Bitrate uint32 `tlv8:"3"`
19 | IFrameInterval uint32 `tlv8:"4"`
20 | }
21 |
--------------------------------------------------------------------------------
/website/api/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | go2rtc - API
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/pkg/flv/flv_test.go:
--------------------------------------------------------------------------------
1 | package flv
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestTimeToRTP(t *testing.T) {
10 | // Reolink camera has 20 FPS
11 | // Video timestamp increases by 50ms, SampleRate 90000, RTP timestamp increases by 4500
12 | // Audio timestamp increases by 64ms, SampleRate 16000, RTP timestamp increases by 1024
13 | frameN := 1
14 | for i := 0; i < 32; i++ {
15 | // 1000ms/(90000/4500) = 50ms
16 | require.Equal(t, uint32(frameN*4500), TimeToRTP(uint32(frameN*50), 90000))
17 | // 1000ms/(16000/1024) = 64ms
18 | require.Equal(t, uint32(frameN*1024), TimeToRTP(uint32(frameN*64), 16000))
19 | frameN *= 2
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/internal/gopro/gopro.go:
--------------------------------------------------------------------------------
1 | package gopro
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/api"
7 | "github.com/AlexxIT/go2rtc/internal/streams"
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/AlexxIT/go2rtc/pkg/gopro"
10 | )
11 |
12 | func Init() {
13 | streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
14 | return gopro.Dial(source)
15 | })
16 |
17 | api.HandleFunc("api/gopro", apiGoPro)
18 | }
19 |
20 | func apiGoPro(w http.ResponseWriter, r *http.Request) {
21 | var items []*api.Source
22 |
23 | for _, host := range gopro.Discovery() {
24 | items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
25 | }
26 |
27 | api.ResponseSources(w, items)
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/ioctl/ioctl.go:
--------------------------------------------------------------------------------
1 | package ioctl
2 |
3 | import (
4 | "bytes"
5 | )
6 |
7 | func Str(b []byte) string {
8 | if i := bytes.IndexByte(b, 0); i >= 0 {
9 | return string(b[:i])
10 | }
11 | return string(b)
12 | }
13 |
14 | func io(mode byte, type_ byte, number byte, size uint16) uintptr {
15 | return uintptr(mode)<<30 | uintptr(size)<<16 | uintptr(type_)<<8 | uintptr(number)
16 | }
17 |
18 | func IOR(type_ byte, number byte, size uint16) uintptr {
19 | return io(read, type_, number, size)
20 | }
21 |
22 | func IOW(type_ byte, number byte, size uint16) uintptr {
23 | return io(write, type_, number, size)
24 | }
25 |
26 | func IORW(type_ byte, number byte, size uint16) uintptr {
27 | return io(read|write, type_, number, size)
28 | }
29 |
--------------------------------------------------------------------------------
/internal/streams/publish.go:
--------------------------------------------------------------------------------
1 | package streams
2 |
3 | import "time"
4 |
5 | func (s *Stream) Publish(url string) error {
6 | cons, run, err := GetConsumer(url)
7 | if err != nil {
8 | return err
9 | }
10 |
11 | if err = s.AddConsumer(cons); err != nil {
12 | return err
13 | }
14 |
15 | go func() {
16 | run()
17 | s.RemoveConsumer(cons)
18 |
19 | // TODO: more smart retry
20 | time.Sleep(5 * time.Second)
21 | _ = s.Publish(url)
22 | }()
23 |
24 | return nil
25 | }
26 |
27 | func Publish(stream *Stream, destination any) {
28 | switch v := destination.(type) {
29 | case string:
30 | if err := stream.Publish(v); err != nil {
31 | log.Error().Err(err).Caller().Send()
32 | }
33 | case []any:
34 | for _, v := range v {
35 | Publish(stream, v)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/internal/mpegts/aac.go:
--------------------------------------------------------------------------------
1 | package mpegts
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/api"
7 | "github.com/AlexxIT/go2rtc/internal/streams"
8 | "github.com/AlexxIT/go2rtc/pkg/aac"
9 | )
10 |
11 | func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
12 | src := r.URL.Query().Get("src")
13 | stream := streams.Get(src)
14 | if stream == nil {
15 | http.Error(w, api.StreamNotFound, http.StatusNotFound)
16 | return
17 | }
18 |
19 | cons := aac.NewConsumer()
20 | cons.WithRequest(r)
21 |
22 | if err := stream.AddConsumer(cons); err != nil {
23 | http.Error(w, err.Error(), http.StatusInternalServerError)
24 | return
25 | }
26 |
27 | w.Header().Add("Content-Type", "audio/aac")
28 |
29 | _, _ = cons.WriteTo(w)
30 |
31 | stream.RemoveConsumer(cons)
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/v4l2/device/README.md:
--------------------------------------------------------------------------------
1 | # Video For Linux Two
2 |
3 | Build on Ubuntu
4 |
5 | ```bash
6 | sudo apt install gcc-x86-64-linux-gnu
7 | sudo apt install gcc-i686-linux-gnu
8 | sudo apt install gcc-aarch64-linux-gnu binutils
9 | sudo apt install gcc-arm-linux-gnueabihf
10 | sudo apt install gcc-mipsel-linux-gnu
11 |
12 | x86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64
13 | i686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686
14 | aarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64
15 | arm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf
16 | mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel -D_TIME_BITS=32
17 | ```
18 |
19 | ## Useful links
20 |
21 | - https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h
22 |
--------------------------------------------------------------------------------
/pkg/hap/hds/hds_test.go:
--------------------------------------------------------------------------------
1 | package hds
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "testing"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestEncryption(t *testing.T) {
13 | key := []byte(core.RandString(16, 0))
14 | salt := core.RandString(32, 0)
15 |
16 | c, err := Client(nil, key, salt, true)
17 | require.NoError(t, err)
18 |
19 | buf := bytes.NewBuffer(nil)
20 | c.wr = bufio.NewWriter(buf)
21 |
22 | n, err := c.Write([]byte("test"))
23 | require.NoError(t, err)
24 | require.Equal(t, 4, n)
25 |
26 | c, err = Client(nil, key, salt, false)
27 | c.rd = bufio.NewReader(buf)
28 | require.NoError(t, err)
29 |
30 | b := make([]byte, 32)
31 | n, err = c.Read(b)
32 | require.NoError(t, err)
33 |
34 | require.Equal(t, "test", string(b[:n]))
35 | }
36 |
--------------------------------------------------------------------------------
/internal/ffmpeg/jpeg_test.go:
--------------------------------------------------------------------------------
1 | package ffmpeg
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestParseQuery(t *testing.T) {
11 | args := parseQuery(nil)
12 | require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())
13 |
14 | query, err := url.ParseQuery("h=480")
15 | require.Nil(t, err)
16 | args = parseQuery(query)
17 | require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf "scale=-1:480" -f mjpeg -`, args.String())
18 |
19 | query, err = url.ParseQuery("hw=vaapi")
20 | require.Nil(t, err)
21 | args = parseQuery(query)
22 | require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
23 | }
24 |
--------------------------------------------------------------------------------
/internal/yandex/README.md:
--------------------------------------------------------------------------------
1 | # Yandex
2 |
3 | Source for receiving stream from new [Yandex IP camera](https://alice.yandex.ru/smart-home/security/ipcamera).
4 |
5 | ## Get Yandex token
6 |
7 | 1. Install HomeAssistant integration [YandexStation](https://github.com/AlexxIT/YandexStation).
8 | 2. Copy token from HomeAssistant config folder: `/config/.storage/core.config_entries`, key: `"x_token"`.
9 |
10 | ## Get device ID
11 |
12 | 1. Open this link in any browser: https://iot.quasar.yandex.ru/m/v3/user/devices
13 | 2. Copy ID of your camera, key: `"id"`.
14 |
15 | ## Config examples
16 |
17 | ```yaml
18 | streams:
19 | yandex_stream: yandex:?x_token=XXXX&device_id=XXXX
20 | yandex_snapshot: yandex:?x_token=XXXX&device_id=XXXX&snapshot
21 | yandex_snapshot_custom_size: yandex:?x_token=XXXX&device_id=XXXX&snapshot=h=540
22 | ```
23 |
--------------------------------------------------------------------------------
/examples/mdns/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/mdns"
8 | )
9 |
10 | func main() {
11 | var service = mdns.ServiceHAP
12 |
13 | if len(os.Args) >= 2 {
14 | service = os.Args[1]
15 | }
16 |
17 | onentry := func(entry *mdns.ServiceEntry) bool {
18 | log.Printf("name=%s, addr=%s, info=%s\n", entry.Name, entry.Addr(), entry.Info)
19 | return false
20 | }
21 |
22 | var err error
23 |
24 | if len(os.Args) >= 3 {
25 | host := os.Args[2]
26 |
27 | log.Printf("run discovery service=%s host=%s\n", service, host)
28 |
29 | err = mdns.QueryOrDiscovery(host, service, onentry)
30 | } else {
31 | log.Printf("run discovery service=%s\n", service)
32 |
33 | err = mdns.Discovery(service, onentry)
34 | }
35 |
36 | if err != nil {
37 | log.Println(err)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "eslint": "^8.44.0",
4 | "eslint-plugin-html": "^7.1.0"
5 | },
6 | "eslintConfig": {
7 | "env": {
8 | "browser": true,
9 | "es6": true
10 | },
11 | "parserOptions": {
12 | "ecmaVersion": 2017,
13 | "sourceType": "module"
14 | },
15 | "rules": {
16 | "no-var": "error",
17 | "no-undef": "error",
18 | "no-unused-vars": "warn",
19 | "prefer-const": "error",
20 | "quotes": [
21 | "error",
22 | "single"
23 | ],
24 | "semi": "error"
25 | },
26 | "plugins": [
27 | "html"
28 | ],
29 | "overrides": [
30 | {
31 | "files": [
32 | "*.html"
33 | ],
34 | "parserOptions": {
35 | "sourceType": "script"
36 | }
37 | }
38 | ]
39 | }
40 | }
--------------------------------------------------------------------------------
/pkg/wyoming/mic.go:
--------------------------------------------------------------------------------
1 | package wyoming
2 |
3 | import (
4 | "fmt"
5 | "net"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | )
9 |
10 | func (s *Server) HandleMic(conn net.Conn) {
11 | defer conn.Close()
12 |
13 | var closed core.Waiter
14 | var timestamp int
15 |
16 | api := NewAPI(conn)
17 | mic := newMicConsumer(func(chunk []byte) {
18 | data := fmt.Sprintf(`{"rate":16000,"width":2,"channels":1,"timestamp":%d}`, timestamp)
19 | evt := &Event{Type: "audio-chunk", Data: data, Payload: chunk}
20 | if err := api.WriteEvent(evt); err != nil {
21 | closed.Done(nil)
22 | }
23 |
24 | timestamp += len(chunk) / 2
25 | })
26 | mic.RemoteAddr = api.conn.RemoteAddr().String()
27 |
28 | if err := s.MicHandler(mic); err != nil {
29 | s.Error("mic error: %s", err)
30 | return
31 | }
32 |
33 | _ = closed.Wait()
34 | _ = mic.Stop()
35 | }
36 |
--------------------------------------------------------------------------------
/internal/gopro/README.md:
--------------------------------------------------------------------------------
1 | # GoPro
2 |
3 | Supported models: HERO9, HERO10, HERO11, HERO12.
4 | Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
5 |
6 | The other camera models have different APIs. I will try to add them in the next versions.
7 |
8 | ## Config
9 |
10 | - USB-connected cameras create a new network interface in the system
11 | - Linux users do not need to install anything
12 | - Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
13 | - if the camera is detected but the stream does not start - you need to disable firewall
14 |
15 | 1. Discover camera address: WebUI > Add > GoPro
16 | 2. Add camera to config
17 |
18 | ```yaml
19 | streams:
20 | hero12: gopro://172.20.100.51
21 | ```
22 |
23 | ## Useful links
24 |
25 | - https://gopro.github.io/OpenGoPro/
26 |
--------------------------------------------------------------------------------
/pkg/pcm/s16le/s16le.go:
--------------------------------------------------------------------------------
1 | package s16le
2 |
3 | func PeaksRMS(b []byte) int16 {
4 | // RMS of sine wave = peak / sqrt2
5 | // https://en.wikipedia.org/wiki/Root_mean_square
6 | // https://www.youtube.com/watch?v=MUDkL4KZi0I
7 | var peaks int32
8 | var peaksSum int32
9 | var prevSample int16
10 | var prevUp bool
11 |
12 | var i int
13 | for n := len(b); i < n; {
14 | lo := b[i]
15 | i++
16 | hi := b[i]
17 | i++
18 |
19 | sample := int16(hi)<<8 | int16(lo)
20 | up := sample >= prevSample
21 |
22 | if i >= 4 {
23 | if up != prevUp {
24 | if prevSample >= 0 {
25 | peaksSum += int32(prevSample)
26 | } else {
27 | peaksSum -= int32(prevSample)
28 | }
29 | peaks++
30 | }
31 | }
32 |
33 | prevSample = sample
34 | prevUp = up
35 | }
36 |
37 | if peaks == 0 {
38 | return 0
39 | }
40 |
41 | return int16(peaksSum / peaks)
42 | }
43 |
--------------------------------------------------------------------------------
/internal/doorbird/doorbird.go:
--------------------------------------------------------------------------------
1 | package doorbird
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/streams"
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/AlexxIT/go2rtc/pkg/doorbird"
9 | )
10 |
11 | func Init() {
12 | streams.RedirectFunc("doorbird", func(rawURL string) (string, error) {
13 | u, err := url.Parse(rawURL)
14 | if err != nil {
15 | return "", err
16 | }
17 |
18 | // https://www.doorbird.com/downloads/api_lan.pdf
19 | switch u.Query().Get("media") {
20 | case "video":
21 | u.Path = "/bha-api/video.cgi"
22 | case "audio":
23 | u.Path = "/bha-api/audio-receive.cgi"
24 | default:
25 | return "", nil
26 | }
27 |
28 | u.Scheme = "http"
29 |
30 | return u.String(), nil
31 | })
32 |
33 | streams.HandleFunc("doorbird", func(source string) (core.Producer, error) {
34 | return doorbird.Dial(source)
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/mdns/syscall_bsd.go:
--------------------------------------------------------------------------------
1 | //go:build darwin || ios || freebsd || openbsd || netbsd || dragonfly
2 |
3 | package mdns
4 |
5 | import (
6 | "syscall"
7 | )
8 |
9 | func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
10 | // change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS
11 | // https://github.com/AlexxIT/go2rtc/issues/626
12 | // https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707
13 | if opt == syscall.SO_REUSEADDR {
14 | if err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
15 | return
16 | }
17 |
18 | opt = syscall.SO_REUSEPORT
19 | }
20 |
21 | return syscall.SetsockoptInt(int(fd), level, opt, value)
22 | }
23 |
24 | func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
25 | return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
26 | }
27 |
--------------------------------------------------------------------------------
/examples/rtsp_client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/AlexxIT/go2rtc/pkg/rtsp"
9 | "github.com/AlexxIT/go2rtc/pkg/shell"
10 | )
11 |
12 | func main() {
13 | client := rtsp.NewClient(os.Args[1])
14 | if err := client.Dial(); err != nil {
15 | log.Panic(err)
16 | }
17 |
18 | client.Medias = []*core.Media{
19 | {
20 | Kind: core.KindAudio,
21 | Direction: core.DirectionRecvonly,
22 | Codecs: []*core.Codec{
23 | {Name: core.CodecPCMU, ClockRate: 8000},
24 | },
25 | ID: "streamid=0",
26 | },
27 | }
28 | if err := client.Announce(); err != nil {
29 | log.Panic(err)
30 | }
31 | if _, err := client.SetupMedia(client.Medias[0]); err != nil {
32 | log.Panic(err)
33 | }
34 | if err := client.Record(); err != nil {
35 | log.Panic(err)
36 | }
37 |
38 | shell.RunUntilSignal()
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/alsa/README.md:
--------------------------------------------------------------------------------
1 | ## Build
2 |
3 | ```shell
4 | x86_64-linux-gnu-gcc -w -static asound_arch.c -o asound_amd64
5 | i686-linux-gnu-gcc -w -static asound_arch.c -o asound_i386
6 | aarch64-linux-gnu-gcc -w -static asound_arch.c -o asound_arm64
7 | arm-linux-gnueabihf-gcc -w -static asound_arch.c -o asound_arm
8 | mipsel-linux-gnu-gcc -w -static asound_arch.c -o asound_mipsle -D_TIME_BITS=32
9 | ```
10 |
11 | ## Useful links
12 |
13 | - https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
14 | - https://github.com/yobert/alsa
15 | - https://github.com/Narsil/alsa-go
16 | - https://github.com/alsa-project/alsa-lib
17 | - https://github.com/anisse/alsa
18 | - https://github.com/tinyalsa/tinyalsa
19 |
20 | **Broken pipe**
21 |
22 | - https://stackoverflow.com/questions/26545139/alsa-cannot-recovery-from-underrun-prepare-failed-broken-pipe
23 | - https://klipspringer.avadeaux.net/alsa-broken-pipe-errors/
24 |
--------------------------------------------------------------------------------
/pkg/core/worker.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Worker struct {
8 | timer *time.Timer
9 | done chan struct{}
10 | }
11 |
12 | // NewWorker run f after d
13 | func NewWorker(d time.Duration, f func() time.Duration) *Worker {
14 | timer := time.NewTimer(d)
15 | done := make(chan struct{})
16 |
17 | go func() {
18 | for {
19 | select {
20 | case <-timer.C:
21 | if d = f(); d > 0 {
22 | timer.Reset(d)
23 | continue
24 | }
25 | case <-done:
26 | timer.Stop()
27 | }
28 | break
29 | }
30 | }()
31 |
32 | return &Worker{timer: timer, done: done}
33 | }
34 |
35 | // Do - instant timer run
36 | func (w *Worker) Do() {
37 | if w == nil {
38 | return
39 | }
40 | w.timer.Reset(0)
41 | }
42 |
43 | func (w *Worker) Stop() {
44 | if w == nil {
45 | return
46 | }
47 |
48 | select {
49 | case w.done <- struct{}{}:
50 | default:
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/hap/setup/setup.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | const (
9 | FlagNFC = 1
10 | FlagIP = 2
11 | FlagBLE = 4
12 | FlagWAC = 8 // Wireless Accessory Configuration (WAC)/Apples MFi
13 | )
14 |
15 | func GenerateSetupURI(category, pin, setupID string) string {
16 | c, _ := strconv.Atoi(category)
17 | p, _ := strconv.Atoi(strings.ReplaceAll(pin, "-", ""))
18 | payload := int64(c&0xFF)<<31 | int64(FlagIP&0xF)<<27 | int64(p&0x7FFFFFF)
19 | return "X-HM://" + FormatInt36(payload, 9) + setupID
20 | }
21 |
22 | // FormatInt36 equal to strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36)))
23 | func FormatInt36(value int64, n int) string {
24 | b := make([]byte, n)
25 | for i := n - 1; 0 <= i; i-- {
26 | b[i] = digits[value%36]
27 | value /= 36
28 | }
29 | return string(b)
30 | }
31 |
32 | const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
33 |
--------------------------------------------------------------------------------
/pkg/shell/shell.go:
--------------------------------------------------------------------------------
1 | package shell
2 |
3 | import (
4 | "os"
5 | "os/signal"
6 | "strings"
7 | "syscall"
8 | )
9 |
10 | func QuoteSplit(s string) []string {
11 | var a []string
12 |
13 | for len(s) > 0 {
14 | switch c := s[0]; c {
15 | case '\t', '\n', '\r', ' ': // unicode.IsSpace
16 | s = s[1:]
17 | case '"', '\'': // quote chars
18 | if i := strings.IndexByte(s[1:], c); i > 0 {
19 | a = append(a, s[1:i+1])
20 | s = s[i+2:]
21 | } else {
22 | return nil // error
23 | }
24 | default:
25 | i := strings.IndexAny(s, "\t\n\r ")
26 | if i > 0 {
27 | a = append(a, s[:i])
28 | s = s[i:]
29 | } else {
30 | a = append(a, s)
31 | s = ""
32 | }
33 | }
34 | }
35 |
36 | return a
37 | }
38 |
39 | func RunUntilSignal() {
40 | sigs := make(chan os.Signal, 1)
41 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
42 | println("exit with signal:", (<-sigs).String())
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/wyoming/snd.go:
--------------------------------------------------------------------------------
1 | package wyoming
2 |
3 | import (
4 | "bytes"
5 | "net"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/AlexxIT/go2rtc/pkg/pcm"
9 | )
10 |
11 | func (s *Server) HandleSnd(conn net.Conn) {
12 | defer conn.Close()
13 |
14 | var snd []byte
15 |
16 | api := NewAPI(conn)
17 | for {
18 | evt, err := api.ReadEvent()
19 | if err != nil {
20 | return
21 | }
22 |
23 | s.Trace("event: %s data: %s payload: %d", evt.Type, evt.Data, len(evt.Payload))
24 |
25 | switch evt.Type {
26 | case "audio-start":
27 | snd = snd[:0]
28 | case "audio-chunk":
29 | snd = append(snd, evt.Payload...)
30 | case "audio-stop":
31 | prod := pcm.OpenSync(sndCodec, bytes.NewReader(snd))
32 | if err = s.SndHandler(prod); err != nil {
33 | s.Error("snd error: %s", err)
34 | return
35 | }
36 | }
37 | }
38 | }
39 |
40 | var sndCodec = &core.Codec{Name: core.CodecPCML, ClockRate: 22050}
41 |
--------------------------------------------------------------------------------
/pkg/y4m/README.md:
--------------------------------------------------------------------------------
1 | ## Planar YUV formats
2 |
3 | Packed YUV - yuyv422 - YUYV 4:2:2
4 | Semi-Planar - nv12 - Y/CbCr 4:2:0
5 | Planar YUV - yuv420p - Planar YUV 4:2:0 - aka. [cosited](https://manned.org/yuv4mpeg.5)
6 |
7 | ```
8 | [video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuyv422 : YUYV 4:2:2 : 1920x1080
9 | [video4linux2,v4l2 @ 0x55fddc42a940] Raw : nv12 : Y/CbCr 4:2:0 : 1920x1080
10 | [video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuv420p : Planar YUV 4:2:0 : 1920x1080
11 | ```
12 |
13 | ## Useful links
14 |
15 | - https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering
16 | - https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts
17 | - https://fourcc.org/yuv.php#YV12
18 | - https://docs.kernel.org/userspace-api/media/v4l/pixfmt-yuv-planar.html
19 | - https://gist.github.com/Jim-Bar/3cbba684a71d1a9d468a6711a6eddbeb
20 |
--------------------------------------------------------------------------------
/pkg/aac/README.md:
--------------------------------------------------------------------------------
1 | ## AAC-LD and AAC-ELD
2 |
3 | | Codec | Rate | QuickTime | ffmpeg | VLC |
4 | |---------|-------|-----------|--------|-----|
5 | | AAC-LD | 8000 | yes | no | no |
6 | | AAC-LD | 16000 | yes | no | no |
7 | | AAC-LD | 22050 | yes | yes | no |
8 | | AAC-LD | 24000 | yes | yes | no |
9 | | AAC-LD | 32000 | yes | yes | no |
10 | | AAC-ELD | 8000 | yes | no | no |
11 | | AAC-ELD | 16000 | yes | no | no |
12 | | AAC-ELD | 22050 | yes | yes | yes |
13 | | AAC-ELD | 24000 | yes | yes | yes |
14 | | AAC-ELD | 32000 | yes | yes | yes |
15 |
16 | ## Useful links
17 |
18 | - [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)
19 | - https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets
20 | - https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c
21 |
--------------------------------------------------------------------------------
/pkg/gopro/discovery.go:
--------------------------------------------------------------------------------
1 | package gopro
2 |
3 | import (
4 | "net"
5 | "net/http"
6 | "regexp"
7 | )
8 |
9 | func Discovery() (urls []string) {
10 | ints, err := net.Interfaces()
11 | if err != nil {
12 | return nil
13 | }
14 |
15 | // The socket address for USB connections is 172.2X.1YZ.51:8080
16 | // https://gopro.github.io/OpenGoPro/http_2_0#socket-address
17 | re := regexp.MustCompile(`^172\.2\d\.1\d\d\.`)
18 |
19 | for _, itf := range ints {
20 | addrs, err := itf.Addrs()
21 | if err != nil {
22 | continue
23 | }
24 |
25 | for _, addr := range addrs {
26 | host := addr.String()
27 | if !re.MatchString(host) {
28 | continue
29 | }
30 |
31 | host = host[:11] + "51" // 172.2x.1xx.xxx
32 | res, err := http.Get("http://" + host + ":8080/gopro/webcam/status")
33 | if err != nil {
34 | continue
35 | }
36 | _ = res.Body.Close()
37 |
38 | urls = append(urls, host)
39 | }
40 | }
41 |
42 | return
43 | }
44 |
--------------------------------------------------------------------------------
/internal/ffmpeg/device/devices.go:
--------------------------------------------------------------------------------
1 | package device
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "strconv"
7 | "sync"
8 |
9 | "github.com/AlexxIT/go2rtc/internal/api"
10 | )
11 |
12 | func Init(bin string) {
13 | Bin = bin
14 |
15 | api.HandleFunc("api/ffmpeg/devices", apiDevices)
16 | }
17 |
18 | func GetInput(src string) string {
19 | query, err := url.ParseQuery(src)
20 | if err != nil {
21 | return ""
22 | }
23 |
24 | runonce.Do(initDevices)
25 |
26 | return queryToInput(query)
27 | }
28 |
29 | var Bin string
30 |
31 | var videos, audios []string
32 | var streams []*api.Source
33 | var runonce sync.Once
34 |
35 | func apiDevices(w http.ResponseWriter, r *http.Request) {
36 | runonce.Do(initDevices)
37 |
38 | api.ResponseSources(w, streams)
39 | }
40 |
41 | func indexToItem(items []string, index string) string {
42 | if i, err := strconv.Atoi(index); err == nil && i < len(items) {
43 | return items[i]
44 | }
45 | return index
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/mpegts/README.md:
--------------------------------------------------------------------------------
1 | ## PTS/DTS/CTS
2 |
3 | ```
4 | if DTS == 0 {
5 | // for I and P frames
6 | packet.Timestamp = PTS (presentation time)
7 | } else {
8 | // for B frames
9 | packet.Timestamp = DTS (decode time)
10 | CTS = PTS-DTS (composition time)
11 | }
12 | ```
13 |
14 | - MPEG-TS container uses PTS and optional DTS.
15 | - MP4 container uses DTS and CTS
16 | - RTP container uses PTS
17 |
18 | ## MPEG-TS
19 |
20 | FFmpeg:
21 | - PMTID=4096
22 | - H264: PESID=256, StreamType=27, StreamID=224
23 | - H265: PESID=256, StreamType=36, StreamID=224
24 | - AAC: PESID=257, StreamType=15, StreamID=192
25 |
26 | Tapo:
27 | - PMTID=18
28 | - H264: PESID=68, StreamType=27, StreamID=224
29 | - AAC: PESID=69, StreamType=144, StreamID=192
30 |
31 | ## Useful links
32 |
33 | - https://github.com/theREDspace/video-onboarding/blob/main/MPEGTS%20Knowledge.md
34 | - https://en.wikipedia.org/wiki/MPEG_transport_stream
35 | - https://en.wikipedia.org/wiki/Program-specific_information
36 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch117_selected_stream.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSelectedStreamConfiguration = "117"
4 |
5 | type SelectedStreamConfiguration struct {
6 | Control SessionControl `tlv8:"1"`
7 | VideoCodec VideoCodecConfiguration `tlv8:"2"`
8 | AudioCodec AudioCodecConfiguration `tlv8:"3"`
9 | }
10 |
11 | //goland:noinspection ALL
12 | const (
13 | SessionCommandEnd = 0
14 | SessionCommandStart = 1
15 | SessionCommandSuspend = 2
16 | SessionCommandResume = 3
17 | SessionCommandReconfigure = 4
18 | )
19 |
20 | type SessionControl struct {
21 | SessionID string `tlv8:"1"`
22 | Command byte `tlv8:"2"`
23 | }
24 |
25 | type RTPParams struct {
26 | PayloadType uint8 `tlv8:"1"`
27 | SSRC uint32 `tlv8:"2"`
28 | MaxBitrate uint16 `tlv8:"3"`
29 | RTCPInterval float32 `tlv8:"4"`
30 | MaxMTU []uint16 `tlv8:"5"`
31 | ComfortNoisePayloadType []uint8 `tlv8:"6"`
32 | }
33 |
--------------------------------------------------------------------------------
/examples/mod_pinggy/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/Pinggy-io/pinggy-go/pinggy"
8 | )
9 |
10 | func main() {
11 | tunType := os.Args[1]
12 | address := os.Args[2]
13 |
14 | log.SetFlags(log.Llongfile | log.LstdFlags)
15 |
16 | config := pinggy.Config{
17 | Type: pinggy.TunnelType(tunType),
18 | TcpForwardingAddr: address,
19 |
20 | //SshOverSsl: true,
21 | //Stdout: os.Stderr,
22 | //Stderr: os.Stderr,
23 | }
24 |
25 | if tunType == "http" {
26 | hman := pinggy.CreateHeaderManipulationAndAuthConfig()
27 | //hman.SetReverseProxy(address)
28 | //hman.SetPassPreflight(true)
29 | //hman.SetNoReverseProxy()
30 | config.HeaderManipulationAndAuth = hman
31 | }
32 |
33 | pl, err := pinggy.ConnectWithConfig(config)
34 | if err != nil {
35 | log.Panicln(err)
36 | }
37 | log.Println("Addrs: ", pl.RemoteUrls())
38 | //err = pl.InitiateWebDebug("localhost:3424")
39 | //log.Println(err)
40 | pl.StartForwarding()
41 | }
42 |
--------------------------------------------------------------------------------
/internal/ffmpeg/version.go:
--------------------------------------------------------------------------------
1 | package ffmpeg
2 |
3 | import (
4 | "errors"
5 | "os/exec"
6 | "sync"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/ffmpeg"
9 | )
10 |
11 | var verMu sync.Mutex
12 | var verErr error
13 | var verFF string
14 | var verAV string
15 |
16 | func Version() (string, error) {
17 | verMu.Lock()
18 | defer verMu.Unlock()
19 |
20 | if verFF != "" {
21 | return verFF, verErr
22 | }
23 |
24 | cmd := exec.Command(defaults["bin"], "-version")
25 | b, err := cmd.Output()
26 | if err != nil {
27 | verFF = "-"
28 | verErr = err
29 | return verFF, verErr
30 | }
31 |
32 | verFF, verAV = ffmpeg.ParseVersion(b)
33 |
34 | if verFF == "" {
35 | verFF = "?"
36 | }
37 |
38 | // better to compare libavformat, because nightly/master builds
39 | if verAV != "" && verAV < ffmpeg.Version50 {
40 | verErr = errors.New("ffmpeg: unsupported version: " + verFF)
41 | }
42 |
43 | log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin")
44 |
45 | return verFF, verErr
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/mjpeg/writer.go:
--------------------------------------------------------------------------------
1 | package mjpeg
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "strconv"
7 | )
8 |
9 | func NewWriter(w io.Writer) io.Writer {
10 | h := w.(http.ResponseWriter).Header()
11 | h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
12 | return &writer{wr: w, buf: []byte(header)}
13 | }
14 |
15 | const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
16 |
17 | type writer struct {
18 | wr io.Writer
19 | buf []byte
20 | }
21 |
22 | func (w *writer) Write(p []byte) (n int, err error) {
23 | w.buf = w.buf[:len(header)]
24 | w.buf = append(w.buf, strconv.Itoa(len(p))...)
25 | w.buf = append(w.buf, "\r\n\r\n"...)
26 | w.buf = append(w.buf, p...)
27 | w.buf = append(w.buf, "\r\n"...)
28 |
29 | // Chrome bug: mjpeg image always shows the second to last image
30 | // https://bugs.chromium.org/p/chromium/issues/detail?id=527446
31 | if _, err = w.wr.Write(w.buf); err != nil {
32 | return 0, err
33 | }
34 |
35 | w.wr.(http.Flusher).Flush()
36 |
37 | return len(p), nil
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/pcm/pcmu.go:
--------------------------------------------------------------------------------
1 | // Package pcm
2 | // https://www.codeproject.com/Articles/14237/Using-the-G711-standard
3 | package pcm
4 |
5 | const bias = 0x84 // 132 or 1000 0100
6 | const ulawMax = alawMax - bias
7 |
8 | func PCMUtoPCM(ulaw byte) int16 {
9 | ulaw = ^ulaw
10 |
11 | exponent := (ulaw & 0x70) >> 4
12 | data := (int16((((ulaw&0x0F)|0x10)<<1)+1) << (exponent + 2)) - bias
13 |
14 | // sign
15 | if ulaw&0x80 == 0 {
16 | return data
17 | } else if data == 0 {
18 | return -1
19 | } else {
20 | return -data
21 | }
22 | }
23 |
24 | func PCMtoPCMU(pcm int16) byte {
25 | var ulaw byte
26 |
27 | if pcm < 0 {
28 | pcm = -pcm
29 | ulaw = 0x80
30 | }
31 |
32 | if pcm > ulawMax {
33 | pcm = ulawMax
34 | }
35 |
36 | pcm += bias
37 |
38 | exponent := byte(7)
39 | for expMask := int16(0x4000); (pcm & expMask) == 0; expMask >>= 1 {
40 | exponent--
41 | }
42 |
43 | // mantisa
44 | ulaw |= byte(pcm>>(exponent+3)) & 0x0F
45 |
46 | if exponent > 0 {
47 | ulaw |= exponent << 4
48 | }
49 |
50 | return ^ulaw
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/pcm/v1/pcm_test.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "testing"
5 |
6 | v2 "github.com/AlexxIT/go2rtc/pkg/pcm"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestPCMUtoPCM(t *testing.T) {
11 | for pcmu := byte(0); pcmu < 255; pcmu++ {
12 | pcm1 := MuLawDecompressTable[pcmu]
13 | pcm2 := v2.PCMUtoPCM(pcmu)
14 | require.Equal(t, pcm1, pcm2)
15 | }
16 | }
17 |
18 | func TestPCMAtoPCM(t *testing.T) {
19 | for pcma := byte(0); pcma < 255; pcma++ {
20 | pcm1 := ALawDecompressTable[pcma]
21 | pcm2 := v2.PCMAtoPCM(pcma)
22 | require.Equal(t, pcm1, pcm2)
23 | }
24 | }
25 |
26 | func TestPCMtoPCMU(t *testing.T) {
27 | for pcm := int16(-32768); pcm < 32767; pcm++ {
28 | pcmu1 := LinearToMuLawSample(pcm)
29 | pcmu2 := v2.PCMtoPCMU(pcm)
30 | require.Equal(t, pcmu1, pcmu2)
31 | }
32 | }
33 |
34 | func TestPCMtoPCMA(t *testing.T) {
35 | for pcm := int16(-32768); pcm < 32767; pcm++ {
36 | pcma1 := LinearToALawSample(pcm)
37 | pcma2 := v2.PCMtoPCMA(pcm)
38 | require.Equal(t, pcma1, pcma2)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/pcm/pcma.go:
--------------------------------------------------------------------------------
1 | // Package pcm
2 | // https://www.codeproject.com/Articles/14237/Using-the-G711-standard
3 | package pcm
4 |
5 | const alawMax = 0x7FFF
6 |
7 | func PCMAtoPCM(alaw byte) int16 {
8 | alaw ^= 0xD5
9 |
10 | data := int16(((alaw & 0x0F) << 4) + 8)
11 | exponent := (alaw & 0x70) >> 4
12 |
13 | if exponent != 0 {
14 | data |= 0x100
15 | }
16 |
17 | if exponent > 1 {
18 | data <<= exponent - 1
19 | }
20 |
21 | // sign
22 | if alaw&0x80 == 0 {
23 | return data
24 | } else {
25 | return -data
26 | }
27 | }
28 |
29 | func PCMtoPCMA(pcm int16) byte {
30 | var alaw byte
31 |
32 | if pcm < 0 {
33 | pcm = -pcm
34 | alaw = 0x80
35 | }
36 |
37 | if pcm > alawMax {
38 | pcm = alawMax
39 | }
40 |
41 | exponent := byte(7)
42 | for expMask := int16(0x4000); (pcm&expMask) == 0 && exponent > 0; expMask >>= 1 {
43 | exponent--
44 | }
45 |
46 | if exponent == 0 {
47 | alaw |= byte(pcm>>4) & 0x0F
48 | } else {
49 | alaw |= (exponent << 4) | (byte(pcm>>(exponent+3)) & 0x0F)
50 | }
51 |
52 | return alaw ^ 0xD5
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/mp4/mime.go:
--------------------------------------------------------------------------------
1 | package mp4
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/pkg/core"
5 | "github.com/AlexxIT/go2rtc/pkg/h264"
6 | )
7 |
8 | const (
9 | MimeH264 = "avc1.640029"
10 | MimeH265 = "hvc1.1.6.L153.B0"
11 | MimeAAC = "mp4a.40.2"
12 | MimeFlac = "flac"
13 | MimeOpus = "opus"
14 | )
15 |
16 | func MimeCodecs(codecs []*core.Codec) string {
17 | var s string
18 |
19 | for i, codec := range codecs {
20 | if i > 0 {
21 | s += ","
22 | }
23 |
24 | switch codec.Name {
25 | case core.CodecH264:
26 | s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
27 | case core.CodecH265:
28 | // H.265 profile=main level=5.1
29 | // hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
30 | s += MimeH265
31 | case core.CodecAAC:
32 | s += MimeAAC
33 | case core.CodecOpus:
34 | s += MimeOpus
35 | case core.CodecFLAC:
36 | s += MimeFlac
37 | }
38 | }
39 |
40 | return s
41 | }
42 |
43 | func ContentType(codecs []*core.Codec) string {
44 | return `video/mp4; codecs="` + MimeCodecs(codecs) + `"`
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch118_setup_endpoints.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSetupEndpoints = "118"
4 |
5 | type SetupEndpointsRequest struct {
6 | SessionID string `tlv8:"1"`
7 | Address Address `tlv8:"3"`
8 | VideoCrypto SRTPCryptoSuite `tlv8:"4"`
9 | AudioCrypto SRTPCryptoSuite `tlv8:"5"`
10 | }
11 |
12 | type SetupEndpointsResponse struct {
13 | SessionID string `tlv8:"1"`
14 | Status byte `tlv8:"2"`
15 | Address Address `tlv8:"3"`
16 | VideoCrypto SRTPCryptoSuite `tlv8:"4"`
17 | AudioCrypto SRTPCryptoSuite `tlv8:"5"`
18 | VideoSSRC uint32 `tlv8:"6"`
19 | AudioSSRC uint32 `tlv8:"7"`
20 | }
21 |
22 | type Address struct {
23 | IPVersion byte `tlv8:"1"`
24 | IPAddr string `tlv8:"2"`
25 | VideoRTPPort uint16 `tlv8:"3"`
26 | AudioRTPPort uint16 `tlv8:"4"`
27 | }
28 |
29 | type SRTPCryptoSuite struct {
30 | CryptoSuite byte `tlv8:"1"`
31 | MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
32 | MasterSalt string `tlv8:"3"` // 14 byte
33 | }
34 |
--------------------------------------------------------------------------------
/internal/ffmpeg/hardware/hardware_darwin.go:
--------------------------------------------------------------------------------
1 | //go:build darwin || ios
2 |
3 | package hardware
4 |
5 | import (
6 | "github.com/AlexxIT/go2rtc/internal/api"
7 | )
8 |
9 | const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
10 | const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
11 |
12 | func ProbeAll(bin string) []*api.Source {
13 | return []*api.Source{
14 | {
15 | Name: runToString(bin, ProbeVideoToolboxH264),
16 | URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
17 | },
18 | {
19 | Name: runToString(bin, ProbeVideoToolboxH265),
20 | URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox,
21 | },
22 | }
23 | }
24 |
25 | func ProbeHardware(bin, name string) string {
26 | switch name {
27 | case "h264":
28 | if run(bin, ProbeVideoToolboxH264) {
29 | return EngineVideoToolbox
30 | }
31 |
32 | case "h265":
33 | if run(bin, ProbeVideoToolboxH265) {
34 | return EngineVideoToolbox
35 | }
36 | }
37 |
38 | return EngineSoftware
39 | }
40 |
--------------------------------------------------------------------------------
/internal/echo/echo.go:
--------------------------------------------------------------------------------
1 | package echo
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "os/exec"
7 | "slices"
8 |
9 | "github.com/AlexxIT/go2rtc/internal/app"
10 | "github.com/AlexxIT/go2rtc/internal/streams"
11 | "github.com/AlexxIT/go2rtc/pkg/shell"
12 | )
13 |
14 | func Init() {
15 | var cfg struct {
16 | Mod struct {
17 | AllowPaths []string `yaml:"allow_paths"`
18 | } `yaml:"echo"`
19 | }
20 |
21 | app.LoadConfig(&cfg)
22 |
23 | allowPaths := cfg.Mod.AllowPaths
24 |
25 | log := app.GetLogger("echo")
26 |
27 | streams.RedirectFunc("echo", func(url string) (string, error) {
28 | args := shell.QuoteSplit(url[5:])
29 |
30 | if allowPaths != nil && !slices.Contains(allowPaths, args[0]) {
31 | return "", errors.New("echo: bin not in allow_paths: " + args[0])
32 | }
33 |
34 | b, err := exec.Command(args[0], args[1:]...).Output()
35 | if err != nil {
36 | return "", err
37 | }
38 |
39 | b = bytes.TrimSpace(b)
40 |
41 | log.Debug().Str("url", url).Msgf("[echo] %s", b)
42 |
43 | return string(b), nil
44 | })
45 | streams.MarkInsecure("echo")
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/alsa/open_linux.go:
--------------------------------------------------------------------------------
1 | package alsa
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/url"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/alsa/device"
9 | "github.com/AlexxIT/go2rtc/pkg/core"
10 | )
11 |
12 | func Open(rawURL string) (core.Producer, error) {
13 | // Example (ffmpeg source compatible):
14 | // alsa:device?audio=/dev/snd/pcmC0D0p
15 | // TODO: ?audio=default
16 | // TODO: ?audio=hw:0,0
17 | // TODO: &sample_rate=48000&channels=2
18 | // TODO: &backchannel=1
19 | u, err := url.Parse(rawURL)
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | path := u.Query().Get("audio")
25 | dev, err := device.Open(path)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | if !dev.CheckFormat(device.SNDRV_PCM_FORMAT_S16_LE) {
31 | _ = dev.Close()
32 | return nil, errors.New("alsa: format S16LE not supported")
33 | }
34 |
35 | switch path[len(path)-1] {
36 | case 'p': // playback
37 | return newPlayback(dev)
38 | case 'c': // capture
39 | return newCapture(dev)
40 | }
41 |
42 | _ = dev.Close()
43 | return nil, fmt.Errorf("alsa: unknown path: %s", path)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/dvrip/dvrip.go:
--------------------------------------------------------------------------------
1 | package dvrip
2 |
3 | import "github.com/AlexxIT/go2rtc/pkg/core"
4 |
5 | func Dial(url string) (core.Producer, error) {
6 | client := &Client{}
7 | if err := client.Dial(url); err != nil {
8 | return nil, err
9 | }
10 |
11 | conn := core.Connection{
12 | ID: core.NewID(),
13 | FormatName: "dvrip",
14 | Protocol: "tcp",
15 | RemoteAddr: client.conn.RemoteAddr().String(),
16 | Transport: client.conn,
17 | }
18 |
19 | if client.stream != "" {
20 | prod := &Producer{Connection: conn, client: client}
21 | if err := prod.probe(); err != nil {
22 | return nil, err
23 | }
24 | return prod, nil
25 | } else {
26 | conn.Medias = []*core.Media{
27 | {
28 | Kind: core.KindAudio,
29 | Direction: core.DirectionSendonly,
30 | Codecs: []*core.Codec{
31 | // leave only one codec here for better compatibility with cameras
32 | // https://github.com/AlexxIT/go2rtc/issues/1111
33 | {Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
34 | },
35 | },
36 | }
37 | return &Backchannel{Connection: conn, client: client}, nil
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/roborock/producer.go:
--------------------------------------------------------------------------------
1 | package roborock
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/pkg/core"
5 | )
6 |
7 | func (c *Client) GetMedias() []*core.Media {
8 | return c.conn.GetMedias()
9 | }
10 |
11 | func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
12 | if media.Kind == core.KindAudio {
13 | c.audio = true
14 | }
15 |
16 | return c.conn.GetTrack(media, codec)
17 | }
18 |
19 | func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
20 | c.backchannel = true
21 | return c.conn.AddTrack(media, codec, track)
22 | }
23 |
24 | func (c *Client) Start() error {
25 | if c.audio || c.backchannel {
26 | if err := c.StartVoiceChat(); err != nil {
27 | return err
28 | }
29 |
30 | if c.backchannel {
31 | if err := c.SetVoiceChatVolume(80); err != nil {
32 | return err
33 | }
34 | }
35 | }
36 | return c.conn.Start()
37 | }
38 |
39 | func (c *Client) Stop() error {
40 | _ = c.iot.Close()
41 | return c.conn.Stop()
42 | }
43 |
44 | func (c *Client) MarshalJSON() ([]byte, error) {
45 | return c.conn.MarshalJSON()
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Allows you to run this workflow manually from the Actions tab
6 | workflow_dispatch:
7 |
8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
9 | permissions:
10 | contents: read
11 | pages: write
12 | id-token: write
13 |
14 | # Allow one concurrent deployment
15 | concurrency:
16 | group: "pages"
17 | cancel-in-progress: true
18 |
19 | jobs:
20 | # Single deploy job since we're just deploying
21 | deploy:
22 | environment:
23 | name: github-pages
24 | url: ${{ steps.deployment.outputs.page_url }}
25 | runs-on: ubuntu-latest
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v4
29 | - name: Setup Pages
30 | uses: actions/configure-pages@v4
31 | - name: Upload artifact
32 | uses: actions/upload-pages-artifact@v3
33 | with:
34 | path: './website'
35 | - name: Deploy to GitHub Pages
36 | id: deployment
37 | uses: actions/deploy-pages@v4
38 |
--------------------------------------------------------------------------------
/pkg/pcm/producer.go:
--------------------------------------------------------------------------------
1 | package pcm
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/pion/rtp"
8 | )
9 |
10 | type Producer struct {
11 | core.Connection
12 | rd io.Reader
13 | }
14 |
15 | func Open(rd io.Reader) (*Producer, error) {
16 | medias := []*core.Media{
17 | {
18 | Kind: core.KindAudio,
19 | Direction: core.DirectionRecvonly,
20 | Codecs: []*core.Codec{
21 | {Name: core.CodecPCMU, ClockRate: 8000},
22 | },
23 | },
24 | }
25 | return &Producer{
26 | core.Connection{
27 | ID: core.NewID(),
28 | FormatName: "pcm",
29 | Medias: medias,
30 | Transport: rd,
31 | },
32 | rd,
33 | }, nil
34 | }
35 |
36 | func (c *Producer) Start() error {
37 | for {
38 | payload := make([]byte, 1024)
39 | if _, err := io.ReadFull(c.rd, payload); err != nil {
40 | return err
41 | }
42 |
43 | c.Recv += 1024
44 |
45 | if len(c.Receivers) == 0 {
46 | continue
47 | }
48 |
49 | pkt := &rtp.Packet{
50 | Header: rtp.Header{Timestamp: core.Now90000()},
51 | Payload: payload,
52 | }
53 | c.Receivers[0].WriteRTP(pkt)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alexey Khit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/internal/webrtc/switchbot.go:
--------------------------------------------------------------------------------
1 | package webrtc
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/AlexxIT/go2rtc/pkg/webrtc"
8 | )
9 |
10 | func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
11 | return kinesisClient(rawURL, query, "webrtc/switchbot", func(prod *webrtc.Conn, query url.Values) (any, error) {
12 | medias := []*core.Media{
13 | {Kind: core.KindVideo, Direction: core.DirectionRecvonly},
14 | }
15 |
16 | offer, err := prod.CreateOffer(medias)
17 | if err != nil {
18 | return nil, err
19 | }
20 |
21 | v := struct {
22 | Type string `json:"type"`
23 | SDP string `json:"sdp"`
24 | Resolution int `json:"resolution"`
25 | PlayType int `json:"play_type"`
26 | }{
27 | Type: "offer",
28 | SDP: offer,
29 | }
30 |
31 | switch query.Get("resolution") {
32 | case "hd":
33 | v.Resolution = 0
34 | case "sd":
35 | v.Resolution = 1
36 | case "auto":
37 | v.Resolution = 2
38 | }
39 |
40 | v.PlayType = core.Atoi(query.Get("play_type")) // zero by default
41 |
42 | return v, nil
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/www/hls.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | hls - go2rtc
6 |
18 |
19 |
20 |
21 |
22 |
36 |
37 |
--------------------------------------------------------------------------------
/pkg/core/slices.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | // This code copied from go1.21 for backward support in go1.20.
4 | // We need to support go1.20 for Windows 7
5 |
6 | // Index returns the index of the first occurrence of v in s,
7 | // or -1 if not present.
8 | func Index[S ~[]E, E comparable](s S, v E) int {
9 | for i := range s {
10 | if v == s[i] {
11 | return i
12 | }
13 | }
14 | return -1
15 | }
16 |
17 | // Contains reports whether v is present in s.
18 | func Contains[S ~[]E, E comparable](s S, v E) bool {
19 | return Index(s, v) >= 0
20 | }
21 |
22 | type Ordered interface {
23 | ~int | ~int8 | ~int16 | ~int32 | ~int64 |
24 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
25 | ~float32 | ~float64 |
26 | ~string
27 | }
28 |
29 | // Max returns the maximal value in x. It panics if x is empty.
30 | // For floating-point E, Max propagates NaNs (any NaN value in x
31 | // forces the output to be NaN).
32 | func Max[S ~[]E, E Ordered](x S) E {
33 | if len(x) < 1 {
34 | panic("slices.Max: empty list")
35 | }
36 | m := x[0]
37 | for i := 1; i < len(x); i++ {
38 | if x[i] > m {
39 | m = x[i]
40 | }
41 | }
42 | return m
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/homekit/log/debug.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | func Debug(v any) {
11 | switch v := v.(type) {
12 | case *http.Request:
13 | if v == nil {
14 | return
15 | }
16 | if v.ContentLength != 0 {
17 | b, err := io.ReadAll(v.Body)
18 | if err != nil {
19 | panic(err)
20 | }
21 | v.Body = io.NopCloser(bytes.NewReader(b))
22 | log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b)
23 | } else {
24 | log.Printf("[homekit] request: %s %s ", v.Method, v.RequestURI)
25 | }
26 | case *http.Response:
27 | if v == nil {
28 | return
29 | }
30 | if v.Header.Get("Content-Type") == "image/jpeg" {
31 | log.Printf("[homekit] response: %d ", v.StatusCode)
32 | return
33 | }
34 | if v.ContentLength != 0 {
35 | b, err := io.ReadAll(v.Body)
36 | if err != nil {
37 | panic(err)
38 | }
39 | v.Body = io.NopCloser(bytes.NewReader(b))
40 | log.Printf("[homekit] response: %s %d\n%s", v.Proto, v.StatusCode, b)
41 | } else {
42 | log.Printf("[homekit] response: %s %d ", v.Proto, v.StatusCode)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/h264/README.md:
--------------------------------------------------------------------------------
1 | # H264
2 |
3 | Payloader code taken from [pion](https://github.com/pion/rtp) library. And changed to AVC packets support.
4 |
5 | ## Useful Links
6 |
7 | - [RTP Payload Format for H.264 Video](https://datatracker.ietf.org/doc/html/rfc6184)
8 | - [The H264 Sequence parameter set](https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set)
9 | - [H.264 Video Types (Microsoft)](https://docs.microsoft.com/en-us/windows/win32/directshow/h-264-video-types)
10 | - [Automatic Generation of H.264 Parameter Sets to Recover Video File Fragments](https://arxiv.org/pdf/2104.14522.pdf)
11 | - [Chromium sources](https://chromium.googlesource.com/external/webrtc/+/HEAD/common_video/h264)
12 | - [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
13 | - [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
14 | - [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)
15 | - [Two stream formats, Annex-B, AVCC (H.264) and HVCC (H.265)](https://www.programmersought.com/article/3901815022/)
16 | - https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/producer-reference-nal.html
17 |
--------------------------------------------------------------------------------
/internal/yandex/yandex.go:
--------------------------------------------------------------------------------
1 | package yandex
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/streams"
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/AlexxIT/go2rtc/pkg/yandex"
9 | )
10 |
11 | func Init() {
12 | streams.HandleFunc("yandex", func(source string) (core.Producer, error) {
13 | u, err := url.Parse(source)
14 | if err != nil {
15 | return nil, err
16 | }
17 |
18 | query := u.Query()
19 | token := query.Get("x_token")
20 |
21 | session, err := yandex.GetSession(token)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | deviceID := query.Get("device_id")
27 |
28 | if query.Has("snapshot") {
29 | rawURL, err := session.GetSnapshotURL(deviceID)
30 | if err != nil {
31 | return nil, err
32 | }
33 | rawURL += "/current.jpg?" + query.Get("snapshot") + "#header=Cookie:" + session.GetCookieString(rawURL)
34 | return streams.GetProducer(rawURL)
35 | }
36 |
37 | room, err := session.WebrtcCreateRoom(deviceID)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | return goloomClient(room.ServiceUrl, room.ServiceName, room.RoomId, room.ParticipantId, room.Credentials)
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/internal/app/storage.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/creds"
7 | "github.com/AlexxIT/go2rtc/pkg/yaml"
8 | )
9 |
10 | func initStorage() {
11 | storage = &envStorage{data: make(map[string]string)}
12 | creds.SetStorage(storage)
13 | }
14 |
15 | func loadEnv(data []byte) {
16 | var cfg struct {
17 | Env map[string]string `yaml:"env"`
18 | }
19 |
20 | if err := yaml.Unmarshal(data, &cfg); err != nil {
21 | return
22 | }
23 |
24 | storage.mu.Lock()
25 | for name, value := range cfg.Env {
26 | storage.data[name] = value
27 | creds.AddSecret(value)
28 | }
29 | storage.mu.Unlock()
30 | }
31 |
32 | var storage *envStorage
33 |
34 | type envStorage struct {
35 | data map[string]string
36 | mu sync.Mutex
37 | }
38 |
39 | func (s *envStorage) SetValue(name, value string) error {
40 | if err := PatchConfig([]string{"env", name}, value); err != nil {
41 | return err
42 | }
43 |
44 | s.mu.Lock()
45 | s.data[name] = value
46 | s.mu.Unlock()
47 |
48 | return nil
49 | }
50 |
51 | func (s *envStorage) GetValue(name string) (value string, ok bool) {
52 | s.mu.Lock()
53 | value, ok = s.data[name]
54 | s.mu.Unlock()
55 | return
56 | }
57 |
--------------------------------------------------------------------------------
/internal/pinggy/pinggy.go:
--------------------------------------------------------------------------------
1 | package pinggy
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/app"
7 | "github.com/AlexxIT/go2rtc/pkg/pinggy"
8 | "github.com/rs/zerolog"
9 | )
10 |
11 | func Init() {
12 | var cfg struct {
13 | Mod struct {
14 | Tunnel string `yaml:"tunnel"`
15 | } `yaml:"pinggy"`
16 | }
17 |
18 | app.LoadConfig(&cfg)
19 |
20 | if cfg.Mod.Tunnel == "" {
21 | return
22 | }
23 |
24 | log = app.GetLogger("pinggy")
25 |
26 | u, err := url.Parse(cfg.Mod.Tunnel)
27 | if err != nil {
28 | log.Error().Err(err).Send()
29 | return
30 | }
31 |
32 | go proxy(u.Scheme, u.Host)
33 | }
34 |
35 | var log zerolog.Logger
36 |
37 | func proxy(proto, address string) {
38 | client, err := pinggy.NewClient(proto)
39 | if err != nil {
40 | log.Error().Err(err).Send()
41 | return
42 | }
43 | defer client.Close()
44 |
45 | urls, err := client.GetURLs()
46 | if err != nil {
47 | log.Error().Err(err).Send()
48 | return
49 | }
50 |
51 | for _, s := range urls {
52 | log.Info().Str("url", s).Msgf("[pinggy] proxy")
53 | }
54 |
55 | err = client.Proxy(address)
56 | if err != nil {
57 | log.Error().Err(err).Send()
58 | return
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/core/track_test.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestSenser(t *testing.T) {
10 | recv := make(chan *Packet) // blocking receiver
11 |
12 | sender := NewSender(nil, &Codec{})
13 | sender.Output = func(packet *Packet) {
14 | recv <- packet
15 | }
16 | require.Equal(t, "new", sender.State())
17 |
18 | sender.Start()
19 | require.Equal(t, "connected", sender.State())
20 |
21 | sender.Input(&Packet{})
22 | sender.Input(&Packet{})
23 |
24 | require.Equal(t, 2, sender.Packets)
25 | require.Equal(t, 0, sender.Drops)
26 |
27 | // important to read one before close
28 | // because goroutine in Start() can run with nil chan
29 | // it's OK in real life, but bad for test
30 | _, ok := <-recv
31 | require.True(t, ok)
32 |
33 | sender.Close()
34 | require.Equal(t, "closed", sender.State())
35 |
36 | sender.Input(&Packet{})
37 |
38 | require.Equal(t, 2, sender.Packets)
39 | require.Equal(t, 1, sender.Drops)
40 |
41 | // read 2nd
42 | _, ok = <-recv
43 | require.True(t, ok)
44 |
45 | // read 3rd
46 | select {
47 | case <-recv:
48 | ok = true
49 | default:
50 | ok = false
51 | }
52 | require.False(t, ok)
53 | }
54 |
--------------------------------------------------------------------------------
/internal/streams/stream_test.go:
--------------------------------------------------------------------------------
1 | package streams
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestRecursion(t *testing.T) {
12 | // create stream with some source
13 | stream1 := New("from_yaml", "does_not_matter")
14 | require.Len(t, streams, 1)
15 |
16 | // ask another unnamed stream that links go2rtc
17 | query, err := url.ParseQuery("src=rtsp://localhost:8554/from_yaml?video")
18 | require.Nil(t, err)
19 | stream2 := GetOrPatch(query)
20 |
21 | // check stream is same
22 | require.Equal(t, stream1, stream2)
23 | // check stream urls is same
24 | require.Equal(t, stream1.producers[0].url, stream2.producers[0].url)
25 | require.Len(t, streams, 2)
26 | }
27 |
28 | func TestTempate(t *testing.T) {
29 | HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil }) // bypass HasProducer
30 |
31 | // config from yaml
32 | stream1 := New("camera.from_hass", "ffmpeg:{input}#video=copy")
33 | // request from hass
34 | stream2 := Patch("camera.from_hass", "rtsp://example.com")
35 |
36 | require.Equal(t, stream1, stream2)
37 | require.Equal(t, "ffmpeg:rtsp://example.com#video=copy", stream1.producers[0].url)
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/shell/command.go:
--------------------------------------------------------------------------------
1 | package shell
2 |
3 | import (
4 | "context"
5 | "os/exec"
6 | )
7 |
8 | // Command like exec.Cmd, but with support:
9 | // - io.Closer interface
10 | // - Wait from multiple places
11 | // - Done channel
12 | type Command struct {
13 | *exec.Cmd
14 | ctx context.Context
15 | cancel context.CancelFunc
16 | err error
17 | }
18 |
19 | func NewCommand(s string) *Command {
20 | ctx, cancel := context.WithCancel(context.Background())
21 | args := QuoteSplit(s)
22 | cmd := exec.CommandContext(ctx, args[0], args[1:]...)
23 | cmd.SysProcAttr = procAttr
24 | return &Command{cmd, ctx, cancel, nil}
25 | }
26 |
27 | func (c *Command) Start() error {
28 | if err := c.Cmd.Start(); err != nil {
29 | return err
30 | }
31 |
32 | go func() {
33 | c.err = c.Cmd.Wait()
34 | c.cancel() // release context resources
35 | }()
36 |
37 | return nil
38 | }
39 |
40 | func (c *Command) Wait() error {
41 | <-c.ctx.Done()
42 | return c.err
43 | }
44 |
45 | func (c *Command) Run() error {
46 | if err := c.Start(); err != nil {
47 | return err
48 | }
49 | return c.Wait()
50 | }
51 |
52 | func (c *Command) Done() <-chan struct{} {
53 | return c.ctx.Done()
54 | }
55 |
56 | func (c *Command) Close() error {
57 | c.cancel()
58 | return nil
59 | }
60 |
--------------------------------------------------------------------------------
/internal/ffmpeg/api.go:
--------------------------------------------------------------------------------
1 | package ffmpeg
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "github.com/AlexxIT/go2rtc/internal/streams"
8 | )
9 |
10 | func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
11 | if r.Method != "POST" {
12 | http.Error(w, "", http.StatusMethodNotAllowed)
13 | return
14 | }
15 |
16 | query := r.URL.Query()
17 | dst := query.Get("dst")
18 | stream := streams.Get(dst)
19 | if stream == nil {
20 | http.Error(w, "", http.StatusNotFound)
21 | return
22 | }
23 |
24 | var src string
25 | if s := query.Get("file"); s != "" {
26 | if streams.Validate(s) == nil {
27 | src = "ffmpeg:" + s + "#audio=auto#input=file"
28 | }
29 | } else if s = query.Get("live"); s != "" {
30 | if streams.Validate(s) == nil {
31 | src = "ffmpeg:" + s + "#audio=auto"
32 | }
33 | } else if s = query.Get("text"); s != "" {
34 | if strings.IndexAny(s, `'"&%$`) < 0 {
35 | src = "ffmpeg:tts?text=" + s
36 | if s = query.Get("voice"); s != "" {
37 | src += "&voice=" + s
38 | }
39 | src += "#audio=auto"
40 | }
41 | }
42 |
43 | if src == "" {
44 | http.Error(w, "", http.StatusBadRequest)
45 | return
46 | }
47 |
48 | if err := stream.Play(src); err != nil {
49 | http.Error(w, err.Error(), http.StatusInternalServerError)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/probe/consumer.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "net/url"
5 | "strings"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | )
9 |
10 | type Probe struct {
11 | core.Connection
12 | }
13 |
14 | func Create(name string, query url.Values) *Probe {
15 | medias := core.ParseQuery(query)
16 |
17 | for _, value := range query["microphone"] {
18 | media := &core.Media{Kind: core.KindAudio, Direction: core.DirectionRecvonly}
19 |
20 | for _, name := range strings.Split(value, ",") {
21 | name = strings.ToUpper(name)
22 | switch name {
23 | case "", "COPY":
24 | name = core.CodecAny
25 | }
26 | media.Codecs = append(media.Codecs, &core.Codec{Name: name})
27 | }
28 |
29 | medias = append(medias, media)
30 | }
31 |
32 | return &Probe{
33 | Connection: core.Connection{
34 | ID: core.NewID(),
35 | FormatName: name,
36 | Medias: medias,
37 | },
38 | }
39 | }
40 |
41 | func (p *Probe) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
42 | sender := core.NewSender(media, track.Codec)
43 | sender.Handler = func(pkt *core.Packet) {
44 | p.Send += len(pkt.Payload)
45 | }
46 | sender.HandleRTP(track)
47 | p.Senders = append(p.Senders, sender)
48 | return nil
49 | }
50 |
51 | func (p *Probe) Start() error {
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/wyoming/producer.go:
--------------------------------------------------------------------------------
1 | package wyoming
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/pion/rtp"
8 | )
9 |
10 | type Producer struct {
11 | core.Connection
12 | api *API
13 | }
14 |
15 | func newProducer(conn net.Conn) *Producer {
16 | return &Producer{
17 | core.Connection{
18 | ID: core.NewID(),
19 | FormatName: "wyoming",
20 | Medias: []*core.Media{
21 | {
22 | Kind: core.KindAudio,
23 | Direction: core.DirectionRecvonly,
24 | Codecs: []*core.Codec{
25 | {Name: core.CodecPCML, ClockRate: 16000},
26 | },
27 | },
28 | },
29 | Transport: conn,
30 | },
31 | NewAPI(conn),
32 | }
33 | }
34 |
35 | func (p *Producer) Start() error {
36 | var seq uint16
37 | var ts uint32
38 |
39 | for {
40 | evt, err := p.api.ReadEvent()
41 | if err != nil {
42 | return err
43 | }
44 |
45 | if evt.Type != "audio-chunk" {
46 | continue
47 | }
48 |
49 | p.Recv += len(evt.Payload)
50 |
51 | pkt := &core.Packet{
52 | Header: rtp.Header{
53 | Version: 2,
54 | Marker: true,
55 | SequenceNumber: seq,
56 | Timestamp: ts,
57 | },
58 | Payload: evt.Payload,
59 | }
60 | p.Receivers[0].WriteRTP(pkt)
61 |
62 | seq++
63 | ts += uint32(len(evt.Payload) / 2)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/h265/avc.go:
--------------------------------------------------------------------------------
1 | package h265
2 |
3 | import "github.com/AlexxIT/go2rtc/pkg/h264"
4 |
5 | const forbiddenZeroBit = 0x80
6 | const nalUnitType = 0x3F
7 |
8 | // Deprecated: DecodeStream - find and return first AU in AVC format
9 | // useful for processing live streams with unknown separator size
10 | func DecodeStream(annexb []byte) ([]byte, int) {
11 | startPos := -1
12 |
13 | i := 0
14 | for {
15 | // search next separator
16 | if i = h264.IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 {
17 | break
18 | }
19 |
20 | // move i to next AU
21 | if i += 3; i >= len(annexb) {
22 | break
23 | }
24 |
25 | // check if AU type valid
26 | octet := annexb[i]
27 | if octet&forbiddenZeroBit != 0 {
28 | continue
29 | }
30 |
31 | nalType := (octet >> 1) & nalUnitType
32 | if startPos >= 0 {
33 | switch nalType {
34 | case NALUTypeVPS, NALUTypePFrame:
35 | if annexb[i-4] == 0 {
36 | return h264.DecodeAnnexB(annexb[startPos : i-4]), i - 4
37 | } else {
38 | return h264.DecodeAnnexB(annexb[startPos : i-3]), i - 3
39 | }
40 | }
41 | } else {
42 | switch nalType {
43 | case NALUTypeVPS, NALUTypePFrame:
44 | if i >= 4 && annexb[i-4] == 0 {
45 | startPos = i - 4
46 | } else {
47 | startPos = i - 3
48 | }
49 | }
50 | }
51 | }
52 |
53 | return nil, 0
54 | }
55 |
--------------------------------------------------------------------------------
/internal/hls/ws.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/AlexxIT/go2rtc/internal/api"
8 | "github.com/AlexxIT/go2rtc/internal/api/ws"
9 | "github.com/AlexxIT/go2rtc/internal/streams"
10 | "github.com/AlexxIT/go2rtc/pkg/mp4"
11 | )
12 |
13 | func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
14 | stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
15 | if stream == nil {
16 | return errors.New(api.StreamNotFound)
17 | }
18 |
19 | codecs := msg.String()
20 | medias := mp4.ParseCodecs(codecs, true)
21 | cons := mp4.NewConsumer(medias)
22 | cons.FormatName = "hls/fmp4"
23 | cons.WithRequest(tr.Request)
24 |
25 | log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
26 |
27 | if err := stream.AddConsumer(cons); err != nil {
28 | log.Error().Err(err).Caller().Send()
29 | return err
30 | }
31 |
32 | session := NewSession(cons)
33 |
34 | session.alive = time.AfterFunc(keepalive, func() {
35 | sessionsMu.Lock()
36 | delete(sessions, session.id)
37 | sessionsMu.Unlock()
38 |
39 | stream.RemoveConsumer(cons)
40 | })
41 |
42 | sessionsMu.Lock()
43 | sessions[session.id] = session
44 | sessionsMu.Unlock()
45 |
46 | go session.Run()
47 |
48 | main := session.Main()
49 | tr.Write(&ws.Message{Type: "hls", Value: string(main)})
50 |
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/mpjpeg/multipart.go:
--------------------------------------------------------------------------------
1 | package mpjpeg
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "io"
7 | "net/http"
8 | "net/textproto"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | func Next(rd *bufio.Reader) (http.Header, []byte, error) {
14 | for {
15 | // search next boundary and skip empty lines
16 | s, err := rd.ReadString('\n')
17 | if err != nil {
18 | return nil, nil, err
19 | }
20 |
21 | if s == "\r\n" {
22 | continue
23 | }
24 |
25 | if !strings.HasPrefix(s, "--") {
26 | return nil, nil, errors.New("multipart: wrong boundary: " + s)
27 | }
28 |
29 | // Foscam G2 has a awful implementation of MJPEG
30 | // https://github.com/AlexxIT/go2rtc/issues/1258
31 | if b, _ := rd.Peek(2); string(b) == "--" {
32 | continue
33 | }
34 |
35 | break
36 | }
37 |
38 | tp := textproto.NewReader(rd)
39 | header, err := tp.ReadMIMEHeader()
40 | if err != nil {
41 | return nil, nil, err
42 | }
43 |
44 | s := header.Get("Content-Length")
45 | if s == "" {
46 | return nil, nil, errors.New("multipart: no content length")
47 | }
48 |
49 | size, err := strconv.Atoi(s)
50 | if err != nil {
51 | return nil, nil, err
52 | }
53 |
54 | buf := make([]byte, size)
55 | if _, err = io.ReadFull(rd, buf); err != nil {
56 | return nil, nil, err
57 | }
58 |
59 | return http.Header(header), buf, nil
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/core/README.md:
--------------------------------------------------------------------------------
1 | ## PCM
2 |
3 | **RTSP**
4 |
5 | - PayloadType=10 - L16/44100/2 - Linear PCM 16-bit big endian
6 | - PayloadType=11 - L16/44100/1 - Linear PCM 16-bit big endian
7 |
8 | https://en.wikipedia.org/wiki/RTP_payload_formats
9 |
10 | **Apple QuickTime**
11 |
12 | - `raw` - 16-bit data is stored in little endian format
13 | - `twos` - 16-bit data is stored in big endian format
14 | - `sowt` - 16-bit data is stored in little endian format
15 | - `in24` - denotes 24-bit, big endian
16 | - `in32` - denotes 32-bit, big endian
17 | - `fl32` - denotes 32-bit floating point PCM
18 | - `fl64` - denotes 64-bit floating point PCM
19 | - `alaw` - denotes A-law logarithmic PCM
20 | - `ulaw` - denotes mu-law logarithmic PCM
21 |
22 | https://wiki.multimedia.cx/index.php/PCM
23 |
24 | **FFmpeg RTSP**
25 |
26 | ```
27 | pcm_s16be, 44100 Hz, stereo => 10
28 | pcm_s16be, 48000 Hz, stereo => 96 L16/48000/2
29 | pcm_s16be, 44100 Hz, mono => 11
30 |
31 | pcm_s16le, 48000 Hz, stereo => 96 (b=AS:1536)
32 | pcm_s16le, 44100 Hz, stereo => 96 (b=AS:1411)
33 | pcm_s16le, 16000 Hz, stereo => 96 (b=AS:512)
34 | pcm_s16le, 8000 Hz, stereo => 96 (b=AS:256)
35 |
36 | pcm_s16le, 48000 Hz, mono => 96 (b=AS:768)
37 | pcm_s16le, 44100 Hz, mono => 96 (b=AS:705)
38 | pcm_s16le, 16000 Hz, mono => 96 (b=AS:256)
39 | pcm_s16le, 8000 Hz, mono => 96 (b=AS:128)
40 | ```
--------------------------------------------------------------------------------
/pkg/v4l2/device/formats.go:
--------------------------------------------------------------------------------
1 | package device
2 |
3 | const (
4 | V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24
5 | V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
6 | V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24
7 | V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24
8 | V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24
9 | )
10 |
11 | type Format struct {
12 | FourCC uint32
13 | Name string
14 | FFmpeg string
15 | }
16 |
17 | var Formats = []Format{
18 | {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"},
19 | {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"},
20 | {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"},
21 | {V4L2_PIX_FMT_H264, "H.264", "h264"},
22 | {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"},
23 | }
24 |
25 | func YUYVtoYUV(dst, src []byte) {
26 | n := len(src)
27 | i0 := 0
28 | iy := 0
29 | iu := n / 2
30 | iv := n / 4 * 3
31 | for i0 < n {
32 | dst[iy] = src[i0]
33 | i0++
34 | iy++
35 | dst[iu] = src[i0]
36 | i0++
37 | iu++
38 | dst[iy] = src[i0]
39 | i0++
40 | iy++
41 | dst[iv] = src[i0]
42 | i0++
43 | iv++
44 | }
45 | }
46 |
47 | func NV12toYUV(dst, src []byte) {
48 | n := len(src)
49 | k := n / 6
50 | i0 := k * 4
51 | iu := i0
52 | iv := i0 + k
53 | copy(dst, src[:i0]) // copy Y
54 | for i0 < n {
55 | dst[iu] = src[i0]
56 | i0++
57 | iu++
58 | dst[iv] = src[i0]
59 | i0++
60 | iv++
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/core/waiter.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | // Waiter support:
8 | // - autotart on first Wait
9 | // - block new waiters after last Done
10 | // - safe Done after finish
11 | type Waiter struct {
12 | sync.WaitGroup
13 | mu sync.Mutex
14 | state int // state < 0 means finish
15 | err error
16 | }
17 |
18 | func (w *Waiter) Add(delta int) {
19 | w.mu.Lock()
20 | if w.state >= 0 {
21 | w.state += delta
22 | w.WaitGroup.Add(delta)
23 | }
24 | w.mu.Unlock()
25 | }
26 |
27 | func (w *Waiter) Wait() error {
28 | w.mu.Lock()
29 | // first wait auto start waiter
30 | if w.state == 0 {
31 | w.state++
32 | w.WaitGroup.Add(1)
33 | }
34 | w.mu.Unlock()
35 |
36 | w.WaitGroup.Wait()
37 |
38 | return w.err
39 | }
40 |
41 | func (w *Waiter) Done(err error) {
42 | w.mu.Lock()
43 |
44 | // safe run Done only when have tasks
45 | if w.state > 0 {
46 | w.state--
47 | w.WaitGroup.Done()
48 | }
49 |
50 | // block waiter for any operations after last done
51 | if w.state == 0 {
52 | w.state = -1
53 | w.err = err
54 | }
55 |
56 | w.mu.Unlock()
57 | }
58 |
59 | func (w *Waiter) WaitChan() <-chan error {
60 | var ch chan error
61 |
62 | w.mu.Lock()
63 |
64 | if w.state >= 0 {
65 | ch = make(chan error)
66 | go func() {
67 | ch <- w.Wait()
68 | }()
69 | }
70 |
71 | w.mu.Unlock()
72 |
73 | return ch
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/debug/debug.go:
--------------------------------------------------------------------------------
1 | package debug
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/pion/rtp"
8 | )
9 |
10 | func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {
11 | var lastTime = time.Now()
12 | var lastTS uint32
13 |
14 | var secCnt int
15 | var secSize int
16 | var secTS uint32
17 | var secTime time.Time
18 |
19 | return func(packet *rtp.Packet) {
20 | if include != nil && !include(packet) {
21 | return
22 | }
23 |
24 | now := time.Now()
25 |
26 | fmt.Printf(
27 | "%s: size=%6d ts=%10d type=%2d ssrc=%d seq=%5d mark=%t dts=%4d dtime=%3dms\n",
28 | now.Format("15:04:05.000"),
29 | len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
30 | packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),
31 | )
32 |
33 | lastTS = packet.Timestamp
34 | lastTime = now
35 |
36 | if secTS == 0 {
37 | secTS = lastTS
38 | secTime = now
39 | return
40 | }
41 |
42 | if dt := now.Sub(secTime); dt > time.Second {
43 | fmt.Printf(
44 | "%s: size=%6d cnt=%d dts=%d dtime=%3dms\n",
45 | now.Format("15:04:05.000"),
46 | secSize, secCnt, lastTS-secTS, dt.Milliseconds(),
47 | )
48 |
49 | secCnt = 0
50 | secSize = 0
51 | secTS = lastTS
52 | secTime = now
53 | }
54 |
55 | secCnt++
56 | secSize += len(packet.Payload)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/mpjpeg/producer.go:
--------------------------------------------------------------------------------
1 | package mpjpeg
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "io"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/pion/rtp"
10 | )
11 |
12 | type Producer struct {
13 | core.Connection
14 | rd *bufio.Reader
15 | }
16 |
17 | func Open(rd io.Reader) (*Producer, error) {
18 | return &Producer{
19 | Connection: core.Connection{
20 | ID: core.NewID(),
21 | FormatName: "mpjpeg", // Multipart JPEG
22 | Transport: rd,
23 | Medias: []*core.Media{
24 | {
25 | Kind: core.KindVideo,
26 | Direction: core.DirectionRecvonly,
27 | Codecs: []*core.Codec{
28 | {
29 | Name: core.CodecJPEG,
30 | ClockRate: 90000,
31 | PayloadType: core.PayloadTypeRAW,
32 | },
33 | },
34 | },
35 | },
36 | },
37 | }, nil
38 | }
39 |
40 | func (c *Producer) Start() error {
41 | if len(c.Receivers) != 1 {
42 | return errors.New("mjpeg: no receivers")
43 | }
44 |
45 | rd := bufio.NewReader(c.Transport.(io.Reader))
46 |
47 | mjpeg := c.Receivers[0]
48 |
49 | for {
50 | _, body, err := Next(rd)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | c.Recv += len(body)
56 |
57 | if mjpeg != nil {
58 | packet := &rtp.Packet{
59 | Header: rtp.Header{Timestamp: core.Now90000()},
60 | Payload: body,
61 | }
62 | mjpeg.WriteRTP(packet)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/aac/consumer.go:
--------------------------------------------------------------------------------
1 | package aac
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/pion/rtp"
8 | )
9 |
10 | type Consumer struct {
11 | core.Connection
12 | wr *core.WriteBuffer
13 | }
14 |
15 | func NewConsumer() *Consumer {
16 | medias := []*core.Media{
17 | {
18 | Kind: core.KindAudio,
19 | Direction: core.DirectionSendonly,
20 | Codecs: []*core.Codec{
21 | {Name: core.CodecAAC},
22 | },
23 | },
24 | }
25 | wr := core.NewWriteBuffer(nil)
26 | return &Consumer{
27 | Connection: core.Connection{
28 | ID: core.NewID(),
29 | FormatName: "adts",
30 | Medias: medias,
31 | Transport: wr,
32 | },
33 | wr: wr,
34 | }
35 | }
36 |
37 | func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
38 | sender := core.NewSender(media, track.Codec)
39 |
40 | sender.Handler = func(pkt *rtp.Packet) {
41 | if n, err := c.wr.Write(pkt.Payload); err == nil {
42 | c.Send += n
43 | }
44 | }
45 |
46 | if track.Codec.IsRTP() {
47 | sender.Handler = RTPToADTS(track.Codec, sender.Handler)
48 | } else {
49 | sender.Handler = EncodeToADTS(track.Codec, sender.Handler)
50 | }
51 |
52 | sender.HandleRTP(track)
53 | c.Senders = append(c.Senders, sender)
54 | return nil
55 | }
56 |
57 | func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
58 | return c.wr.WriteTo(wr)
59 | }
60 |
--------------------------------------------------------------------------------
/internal/nest/init.go:
--------------------------------------------------------------------------------
1 | package nest
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "github.com/AlexxIT/go2rtc/internal/api"
8 | "github.com/AlexxIT/go2rtc/internal/streams"
9 | "github.com/AlexxIT/go2rtc/pkg/core"
10 | "github.com/AlexxIT/go2rtc/pkg/nest"
11 | )
12 |
13 | func Init() {
14 | streams.HandleFunc("nest", func(source string) (core.Producer, error) {
15 | return nest.Dial(source)
16 | })
17 |
18 | api.HandleFunc("api/nest", apiNest)
19 | }
20 |
21 | func apiNest(w http.ResponseWriter, r *http.Request) {
22 | query := r.URL.Query()
23 | cliendID := query.Get("client_id")
24 | cliendSecret := query.Get("client_secret")
25 | refreshToken := query.Get("refresh_token")
26 | projectID := query.Get("project_id")
27 |
28 | nestAPI, err := nest.NewAPI(cliendID, cliendSecret, refreshToken)
29 | if err != nil {
30 | http.Error(w, err.Error(), http.StatusInternalServerError)
31 | return
32 | }
33 |
34 | devices, err := nestAPI.GetDevices(projectID)
35 | if err != nil {
36 | http.Error(w, err.Error(), http.StatusInternalServerError)
37 | return
38 | }
39 |
40 | var items []*api.Source
41 |
42 | for _, device := range devices {
43 | query.Set("device_id", device.DeviceID)
44 | query.Set("protocols", strings.Join(device.Protocols, ","))
45 |
46 | items = append(items, &api.Source{
47 | Name: device.Name, URL: "nest:?" + query.Encode(),
48 | })
49 | }
50 |
51 | api.ResponseSources(w, items)
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/ring/snapshot.go:
--------------------------------------------------------------------------------
1 | package ring
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/pion/rtp"
8 | )
9 |
10 | type SnapshotProducer struct {
11 | core.Connection
12 |
13 | client *RingApi
14 | cameraID int
15 | }
16 |
17 | func NewSnapshotProducer(client *RingApi, cameraID int) *SnapshotProducer {
18 | return &SnapshotProducer{
19 | Connection: core.Connection{
20 | ID: core.NewID(),
21 | FormatName: "ring/snapshot",
22 | Protocol: "https",
23 | RemoteAddr: "app-snaps.ring.com",
24 | Medias: []*core.Media{
25 | {
26 | Kind: core.KindVideo,
27 | Direction: core.DirectionRecvonly,
28 | Codecs: []*core.Codec{
29 | {
30 | Name: core.CodecJPEG,
31 | ClockRate: 90000,
32 | PayloadType: core.PayloadTypeRAW,
33 | },
34 | },
35 | },
36 | },
37 | },
38 | client: client,
39 | cameraID: cameraID,
40 | }
41 | }
42 |
43 | func (p *SnapshotProducer) Start() error {
44 | response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", p.cameraID), nil)
45 | if err != nil {
46 | return err
47 | }
48 |
49 | pkt := &rtp.Packet{
50 | Header: rtp.Header{Timestamp: core.Now90000()},
51 | Payload: response,
52 | }
53 |
54 | p.Receivers[0].WriteRTP(pkt)
55 |
56 | return nil
57 | }
58 |
59 | func (p *SnapshotProducer) Stop() error {
60 | return p.Connection.Stop()
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/webrtc/producer.go:
--------------------------------------------------------------------------------
1 | package webrtc
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/pkg/core"
5 | "github.com/pion/webrtc/v4"
6 | )
7 |
8 | func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
9 | core.Assert(media.Direction == core.DirectionRecvonly)
10 |
11 | for _, track := range c.Receivers {
12 | if track.Codec == codec {
13 | return track, nil
14 | }
15 | }
16 |
17 | switch c.Mode {
18 | case core.ModePassiveConsumer: // backchannel from browser
19 | // set codec for consumer recv track so remote peer should send media with this codec
20 | params := webrtc.RTPCodecParameters{
21 | RTPCodecCapability: webrtc.RTPCodecCapability{
22 | MimeType: MimeType(codec),
23 | ClockRate: codec.ClockRate,
24 | Channels: uint16(codec.Channels),
25 | },
26 | PayloadType: 0, // don't know if this necessary
27 | }
28 |
29 | tr := c.getTranseiver(media.ID)
30 |
31 | _ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params})
32 |
33 | case core.ModePassiveProducer, core.ModeActiveProducer:
34 | // Passive producers: OBS Studio via WHIP or Browser
35 | // Active producers: go2rtc as WebRTC client or WebTorrent
36 |
37 | default:
38 | panic(core.Caller())
39 | }
40 |
41 | track := core.NewReceiver(media, codec)
42 | c.Receivers = append(c.Receivers, track)
43 | return track, nil
44 | }
45 |
46 | func (c *Conn) Start() error {
47 | c.closed.Wait()
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/mjpeg/consumer.go:
--------------------------------------------------------------------------------
1 | package mjpeg
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/pion/rtp"
8 | )
9 |
10 | type Consumer struct {
11 | core.Connection
12 | wr *core.WriteBuffer
13 | }
14 |
15 | func NewConsumer() *Consumer {
16 | medias := []*core.Media{
17 | {
18 | Kind: core.KindVideo,
19 | Direction: core.DirectionSendonly,
20 | Codecs: []*core.Codec{
21 | {Name: core.CodecJPEG},
22 | {Name: core.CodecRAW},
23 | },
24 | },
25 | }
26 | wr := core.NewWriteBuffer(nil)
27 | return &Consumer{
28 | Connection: core.Connection{
29 | ID: core.NewID(),
30 | FormatName: "mjpeg",
31 | Medias: medias,
32 | Transport: wr,
33 | },
34 | wr: wr,
35 | }
36 | }
37 |
38 | func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
39 | sender := core.NewSender(media, track.Codec)
40 | sender.Handler = func(packet *rtp.Packet) {
41 | if n, err := c.wr.Write(packet.Payload); err == nil {
42 | c.Send += n
43 | }
44 | }
45 |
46 | if track.Codec.IsRTP() {
47 | sender.Handler = RTPDepay(sender.Handler)
48 | } else if track.Codec.Name == core.CodecRAW {
49 | sender.Handler = Encoder(track.Codec, 0, sender.Handler)
50 | }
51 |
52 | sender.HandleRTP(track)
53 | c.Senders = append(c.Senders, sender)
54 | return nil
55 | }
56 |
57 | func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
58 | return c.wr.WriteTo(wr)
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/onvif/README.md:
--------------------------------------------------------------------------------
1 | ## Profiles
2 |
3 | - Profile A - For access control configuration
4 | - Profile C - For door control and event management
5 | - Profile S - For basic video streaming
6 | - Video streaming and configuration
7 | - Profile T - For advanced video streaming
8 | - H.264 / H.265 video compression
9 | - Imaging settings
10 | - Motion alarm and tampering events
11 | - Metadata streaming
12 | - Bi-directional audio
13 |
14 | ## Services
15 |
16 | https://www.onvif.org/profiles/specifications/
17 |
18 | - https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl
19 | - https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl
20 | - https://www.onvif.org/ver10/media/wsdl/media.wsdl
21 |
22 | ## TMP
23 |
24 | | | Dahua | Reolink | TP-Link |
25 | |------------------------|---------|---------|---------|
26 | | GetCapabilities | no auth | no auth | no auth |
27 | | GetServices | no auth | no auth | no auth |
28 | | GetServiceCapabilities | no auth | no auth | auth |
29 | | GetSystemDateAndTime | no auth | no auth | no auth |
30 | | GetNetworkInterfaces | auth | auth | auth |
31 | | GetDeviceInformation | auth | auth | auth |
32 | | GetProfiles | auth | auth | auth |
33 | | GetScopes | auth | auth | auth |
34 |
35 | - Dahua - onvif://192.168.10.90:80
36 | - Reolink - onvif://192.168.10.92:8000
37 | - TP-Link - onvif://192.168.10.91:2020/onvif/device_service
38 | -
--------------------------------------------------------------------------------
/pkg/rtmp/README.md:
--------------------------------------------------------------------------------
1 | ## Tests
2 |
3 | - go2rtc rtmp client => Reolink
4 | - go2rtc rtmp server <= Dahua
5 | - go2rtc rtmp publish => YouTube
6 | - go2rtc rtmp publish => Telegram
7 |
8 | ## Logs
9 |
10 | ```
11 | request []interface {}{"connect", 1, map[string]interface {}{"app":"s", "flashVer":"FMLE/3.0 (compatible; FMSc/1.0)", "tcUrl":"rtmps://xxx.rtmp.t.me/s/xxxxx"}}
12 | response []interface {}{"_result", 1, map[string]interface {}{"capabilities":31, "fmsVer":"FMS/3,0,1,123"}, map[string]interface {}{"code":"NetConnection.Connect.Success", "description":"Connection succeeded.", "level":"status", "objectEncoding":0}}
13 | request []interface {}{"releaseStream", 2, interface {}(nil), "xxxxx"}
14 | request []interface {}{"FCPublish", 3, interface {}(nil), "xxxxx"}
15 | request []interface {}{"createStream", 4, interface {}(nil)}
16 | response []interface {}{"_result", 2, interface {}(nil)}
17 | response []interface {}{"_result", 4, interface {}(nil), 1}
18 | request []interface {}{"publish", 5, interface {}(nil), "xxxxx", "live"}
19 | response []interface {}{"onStatus", 0, interface {}(nil), map[string]interface {}{"code":"NetStream.Publish.Start", "description":"xxxxx is now published", "detail":"xxxxx", "level":"status"}}
20 | ```
21 |
22 | ## Useful links
23 |
24 | - https://en.wikipedia.org/wiki/Flash_Video
25 | - https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
26 | - https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf
27 | - https://rtmp.veriskope.com/docs/spec/
28 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch114_supported_video.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedVideoStreamConfiguration = "114"
4 |
5 | type SupportedVideoStreamConfiguration struct {
6 | Codecs []VideoCodecConfiguration `tlv8:"1"`
7 | }
8 |
9 | type VideoCodecConfiguration struct {
10 | CodecType byte `tlv8:"1"`
11 | CodecParams []VideoCodecParameters `tlv8:"2"`
12 | VideoAttrs []VideoCodecAttributes `tlv8:"3"`
13 | RTPParams []RTPParams `tlv8:"4"`
14 | }
15 |
16 | //goland:noinspection ALL
17 | const (
18 | VideoCodecTypeH264 = 0
19 |
20 | VideoCodecProfileConstrainedBaseline = 0
21 | VideoCodecProfileMain = 1
22 | VideoCodecProfileHigh = 2
23 |
24 | VideoCodecLevel31 = 0
25 | VideoCodecLevel32 = 1
26 | VideoCodecLevel40 = 2
27 |
28 | VideoCodecPacketizationModeNonInterleaved = 0
29 |
30 | VideoCodecCvoNotSuppported = 0
31 | VideoCodecCvoSuppported = 1
32 | )
33 |
34 | type VideoCodecParameters struct {
35 | ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high
36 | Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0
37 | PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved
38 | CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported
39 | CVOID []byte `tlv8:"5"` // ID for CVO RTP extensio
40 | }
41 |
42 | type VideoCodecAttributes struct {
43 | Width uint16 `tlv8:"1"`
44 | Height uint16 `tlv8:"2"`
45 | Framerate uint8 `tlv8:"3"`
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/tcp/dial.go:
--------------------------------------------------------------------------------
1 | package tcp
2 |
3 | import (
4 | "crypto/tls"
5 | "errors"
6 | "net"
7 | "net/url"
8 | "strings"
9 | "time"
10 | )
11 |
12 | // Dial - for RTSP(S|X) and RTMP(S|X)
13 | func Dial(u *url.URL, timeout time.Duration) (net.Conn, error) {
14 | var address string
15 | var hostname string // without port
16 | if i := strings.IndexByte(u.Host, ':'); i > 0 {
17 | address = u.Host
18 | hostname = u.Host[:i]
19 | } else {
20 | switch u.Scheme {
21 | case "rtsp", "rtsps", "rtspx":
22 | address = u.Host + ":554"
23 | case "rtmp":
24 | address = u.Host + ":1935"
25 | case "rtmps", "rtmpx":
26 | address = u.Host + ":443"
27 | }
28 | hostname = u.Host
29 | }
30 |
31 | var secure *tls.Config
32 |
33 | switch u.Scheme {
34 | case "rtsp", "rtmp":
35 | case "rtsps", "rtspx", "rtmps", "rtmpx":
36 | if u.Scheme[4] == 'x' || IsIP(hostname) {
37 | secure = &tls.Config{InsecureSkipVerify: true}
38 | } else {
39 | secure = &tls.Config{ServerName: hostname}
40 | }
41 | default:
42 | return nil, errors.New("unsupported scheme: " + u.Scheme)
43 | }
44 |
45 | conn, err := net.DialTimeout("tcp", address, timeout)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | if secure == nil {
51 | return conn, nil
52 | }
53 |
54 | tlsConn := tls.Client(conn, secure)
55 | if err = tlsConn.Handshake(); err != nil {
56 | return nil, err
57 | }
58 |
59 | if u.Scheme[4] == 'x' {
60 | u.Scheme = u.Scheme[:4] + "s"
61 | }
62 |
63 | return tlsConn, nil
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/core/readbuffer_test.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestReadSeeker(t *testing.T) {
12 | b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
13 | buf := bytes.NewReader(b)
14 |
15 | rd := NewReadBuffer(buf)
16 | rd.BufferSize = ProbeSize
17 |
18 | // 1. Read to buffer
19 | b = make([]byte, 3)
20 | n, err := rd.Read(b)
21 | require.Nil(t, err)
22 | require.Equal(t, []byte{0, 1, 2}, b[:n])
23 |
24 | // 2. Seek to start
25 | _, err = rd.Seek(0, io.SeekStart)
26 | require.Nil(t, err)
27 |
28 | // 3. Read from buffer
29 | b = make([]byte, 2)
30 | n, err = rd.Read(b)
31 | require.Nil(t, err)
32 | require.Equal(t, []byte{0, 1}, b[:n])
33 |
34 | // 4. Read from buffer
35 | n, err = rd.Read(b)
36 | require.Nil(t, err)
37 | require.Equal(t, []byte{2}, b[:n])
38 |
39 | // 5. Read to buffer
40 | n, err = rd.Read(b)
41 | require.Nil(t, err)
42 | require.Equal(t, []byte{3, 4}, b[:n])
43 |
44 | // 6. Seek to start
45 | _, err = rd.Seek(0, io.SeekStart)
46 | require.Nil(t, err)
47 |
48 | // 7. Disable buffer
49 | rd.BufferSize = -1
50 |
51 | // 8. Read from buffer
52 | b = make([]byte, 10)
53 | n, err = rd.Read(b)
54 | require.Nil(t, err)
55 | require.Equal(t, []byte{0, 1, 2, 3, 4}, b[:n])
56 |
57 | // 9. Direct read
58 | n, err = rd.Read(b)
59 | require.Nil(t, err)
60 | require.Equal(t, []byte{5, 6, 7, 8, 9}, b[:n])
61 |
62 | // 10. Check buffer empty
63 | require.Nil(t, rd.buf)
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/hap/camera/ch115_supported_audio.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | const TypeSupportedAudioStreamConfiguration = "115"
4 |
5 | type SupportedAudioStreamConfiguration struct {
6 | Codecs []AudioCodecConfiguration `tlv8:"1"`
7 | ComfortNoiseSupport byte `tlv8:"2"`
8 | }
9 |
10 | //goland:noinspection ALL
11 | const (
12 | AudioCodecTypePCMU = 0
13 | AudioCodecTypePCMA = 1
14 | AudioCodecTypeAACELD = 2
15 | AudioCodecTypeOpus = 3
16 | AudioCodecTypeMSBC = 4
17 | AudioCodecTypeAMR = 5
18 | AudioCodecTypeARMWB = 6
19 |
20 | AudioCodecBitrateVariable = 0
21 | AudioCodecBitrateConstant = 1
22 |
23 | AudioCodecSampleRate8Khz = 0
24 | AudioCodecSampleRate16Khz = 1
25 | AudioCodecSampleRate24Khz = 2
26 |
27 | RTPTimeAACELD8 = 60 // 8000/1000*60=480
28 | RTPTimeAACELD16 = 30 // 16000/1000*30=480
29 | RTPTimeAACELD24 = 20 // 24000/1000*20=480
30 | RTPTimeAACLD16 = 60 // 16000/1000*60=960
31 | RTPTimeAACLD24 = 40 // 24000/1000*40=960
32 | )
33 |
34 | type AudioCodecConfiguration struct {
35 | CodecType byte `tlv8:"1"`
36 | CodecParams []AudioCodecParameters `tlv8:"2"`
37 | RTPParams []RTPParams `tlv8:"3"`
38 | ComfortNoise []byte `tlv8:"4"`
39 | }
40 |
41 | type AudioCodecParameters struct {
42 | Channels uint8 `tlv8:"1"`
43 | BitrateMode byte `tlv8:"2"` // 0 - variable, 1 - constant
44 | SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
45 | RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60
46 | }
47 |
--------------------------------------------------------------------------------
/internal/xiaomi/README.md:
--------------------------------------------------------------------------------
1 | # Xiaomi
2 |
3 | This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.
4 |
5 | **Important:**
6 |
7 | 1. **Not all cameras are supported**. There are several P2P protocol vendors in the Xiaomi ecosystem.
8 | Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not supported.
9 | 2. Each time you connect to the camera, you need internet access to obtain encryption keys.
10 | 3. Connection to the camera is local only.
11 |
12 | **Features:**
13 |
14 | - Multiple Xiaomi accounts supported
15 | - Cameras from multiple regions are supported for a single account
16 | - Two-way audio is supported
17 | - Cameras with multiple lenses are supported
18 |
19 | ## Setup
20 |
21 | 1. Goto go2rtc WebUI > Add > Xiaomi > Login with username and password
22 | 2. Receive verification code by email or phone if required.
23 | 3. Complete the captcha if required.
24 | 4. If everything is OK, your account will be added and you can load cameras from it.
25 |
26 | **Example**
27 |
28 | ```yaml
29 | xiaomi:
30 | 1234567890: V1:***
31 |
32 | streams:
33 | xiaomi1: xiaomi://1234567890:cn@192.168.1.123?did=9876543210&model=isa.camera.hlc7
34 | ```
35 |
36 | ## Configuration
37 |
38 | You can change camera's quality: `subtype=hd/sd/auto`
39 |
40 | ```yaml
41 | streams:
42 | xiaomi1: xiaomi://***&subtype=sd
43 | ```
44 |
45 | You can use second channel for Dual cameras: `channel=1`
46 |
47 | ```yaml
48 | streams:
49 | xiaomi1: xiaomi://***&channel=1
50 | ```
51 |
--------------------------------------------------------------------------------
/pkg/xnet/net.go:
--------------------------------------------------------------------------------
1 | package xnet
2 |
3 | import (
4 | "net"
5 | "strconv"
6 | )
7 |
8 | // Docker has common docker addresses (class B):
9 | // https://en.wikipedia.org/wiki/Private_network
10 | // - docker0 172.17.0.1/16
11 | // - br-xxxx 172.18.0.1/16
12 | // - hassio 172.30.32.1/23
13 | var Docker = net.IPNet{
14 | IP: []byte{172, 16, 0, 0},
15 | Mask: []byte{255, 240, 0, 0},
16 | }
17 |
18 | // ParseUnspecifiedPort will return port if address is unspecified
19 | // ex. ":8555" or "0.0.0.0:8555"
20 | func ParseUnspecifiedPort(address string) int {
21 | host, port, err := net.SplitHostPort(address)
22 | if err != nil {
23 | return 0
24 | }
25 |
26 | if host != "" && host != "0.0.0.0" && host != "[::]" {
27 | return 0
28 | }
29 |
30 | i, _ := strconv.Atoi(port)
31 | return i
32 | }
33 |
34 | func IPNets(ipFilter func(ip net.IP) bool) ([]*net.IPNet, error) {
35 | ifaces, err := net.Interfaces()
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | var nets []*net.IPNet
41 |
42 | for _, iface := range ifaces {
43 | if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
44 | continue
45 | }
46 |
47 | addrs, _ := iface.Addrs() // range on nil slice is OK
48 | for _, addr := range addrs {
49 | switch v := addr.(type) {
50 | case *net.IPNet:
51 | ip := v.IP.To4()
52 | if ip == nil {
53 | continue
54 | }
55 | if ipFilter != nil && !ipFilter(ip) {
56 | continue
57 | }
58 | nets = append(nets, v)
59 | }
60 | }
61 | }
62 |
63 | return nets, nil
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/ngrok/ngrok.go:
--------------------------------------------------------------------------------
1 | package ngrok
2 |
3 | import (
4 | "bufio"
5 | "encoding/json"
6 | "io"
7 | "os/exec"
8 | "strings"
9 |
10 | "github.com/AlexxIT/go2rtc/pkg/core"
11 | )
12 |
13 | type Ngrok struct {
14 | core.Listener
15 |
16 | Tunnels map[string]string
17 |
18 | reader *bufio.Reader
19 | }
20 |
21 | type Message struct {
22 | Msg string `json:"msg"`
23 | Addr string `json:"addr"`
24 | URL string `json:"url"`
25 | Line string
26 | }
27 |
28 | func NewNgrok(command any) (*Ngrok, error) {
29 | var arg []string
30 | switch command.(type) {
31 | case string:
32 | arg = strings.Split(command.(string), " ")
33 | case []string:
34 | arg = command.([]string)
35 | }
36 |
37 | arg = append(arg, "--log", "stdout", "--log-format", "json")
38 |
39 | cmd := exec.Command(arg[0], arg[1:]...)
40 |
41 | r, err := cmd.StdoutPipe()
42 | if err != nil {
43 | return nil, err
44 | }
45 | cmd.Stderr = cmd.Stdout
46 |
47 | n := &Ngrok{
48 | Tunnels: map[string]string{},
49 | reader: bufio.NewReader(r),
50 | }
51 |
52 | if err = cmd.Start(); err != nil {
53 | return nil, err
54 | }
55 |
56 | return n, nil
57 | }
58 |
59 | func (n *Ngrok) Serve() error {
60 | for {
61 | line, _, err := n.reader.ReadLine()
62 | if err != nil {
63 | if err != io.EOF {
64 | return err
65 | }
66 | return nil
67 | }
68 |
69 | msg := new(Message)
70 | _ = json.Unmarshal(line, msg)
71 |
72 | if msg.Msg == "started tunnel" {
73 | n.Tunnels[msg.Addr] = msg.URL
74 | }
75 |
76 | msg.Line = string(line)
77 |
78 | n.Fire(msg)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/hap/chacha20poly1305/chacha20poly1305.go:
--------------------------------------------------------------------------------
1 | package chacha20poly1305
2 |
3 | import (
4 | "errors"
5 |
6 | "golang.org/x/crypto/chacha20poly1305"
7 | )
8 |
9 | var ErrInvalidParams = errors.New("chacha20poly1305: invalid params")
10 |
11 | // Decrypt - decrypt without verify
12 | func Decrypt(key32 []byte, nonce8 string, ciphertext []byte) ([]byte, error) {
13 | return DecryptAndVerify(key32, nil, []byte(nonce8), ciphertext, nil)
14 | }
15 |
16 | // Encrypt - encrypt without seal
17 | func Encrypt(key32 []byte, nonce8 string, plaintext []byte) ([]byte, error) {
18 | return EncryptAndSeal(key32, nil, []byte(nonce8), plaintext, nil)
19 | }
20 |
21 | func DecryptAndVerify(key32, dst, nonce8, ciphertext, verify []byte) ([]byte, error) {
22 | if len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 {
23 | return nil, ErrInvalidParams
24 | }
25 |
26 | aead, err := chacha20poly1305.New(key32)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | nonce := make([]byte, chacha20poly1305.NonceSize)
32 | copy(nonce[4:], nonce8)
33 |
34 | return aead.Open(dst, nonce, ciphertext, verify)
35 | }
36 |
37 | func EncryptAndSeal(key32, dst, nonce8, plaintext, verify []byte) ([]byte, error) {
38 | if len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 {
39 | return nil, ErrInvalidParams
40 | }
41 |
42 | aead, err := chacha20poly1305.New(key32)
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | nonce := make([]byte, chacha20poly1305.NonceSize)
48 | copy(nonce[4:], nonce8)
49 |
50 | return aead.Seal(dst, nonce, plaintext, verify), nil
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/wyoming/backchannel.go:
--------------------------------------------------------------------------------
1 | package wyoming
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/pion/rtp"
10 | )
11 |
12 | type Backchannel struct {
13 | core.Connection
14 | api *API
15 | }
16 |
17 | func newBackchannel(conn net.Conn) *Backchannel {
18 | return &Backchannel{
19 | core.Connection{
20 | ID: core.NewID(),
21 | FormatName: "wyoming",
22 | Medias: []*core.Media{
23 | {
24 | Kind: core.KindAudio,
25 | Direction: core.DirectionSendonly,
26 | Codecs: []*core.Codec{
27 | {Name: core.CodecPCML, ClockRate: 22050},
28 | },
29 | },
30 | },
31 | Transport: conn,
32 | },
33 | NewAPI(conn),
34 | }
35 | }
36 |
37 | func (b *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
38 | return nil, core.ErrCantGetTrack
39 | }
40 |
41 | func (b *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
42 | sender := core.NewSender(media, codec)
43 | sender.Handler = func(pkt *rtp.Packet) {
44 | ts := time.Now().Nanosecond()
45 | evt := &Event{
46 | Type: "audio-chunk",
47 | Data: fmt.Sprintf(`{"rate":22050,"width":2,"channels":1,"timestamp":%d}`, ts),
48 | Payload: pkt.Payload,
49 | }
50 | _ = b.api.WriteEvent(evt)
51 | }
52 | sender.HandleRTP(track)
53 | b.Senders = append(b.Senders, sender)
54 | return nil
55 | }
56 |
57 | func (b *Backchannel) Start() error {
58 | for {
59 | if _, err := b.api.ReadEvent(); err != nil {
60 | return err
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/isapi/backchannel.go:
--------------------------------------------------------------------------------
1 | package isapi
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/pion/rtp"
8 | )
9 |
10 | func (c *Client) GetMedias() []*core.Media {
11 | return c.medias
12 | }
13 |
14 | func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
15 | return nil, core.ErrCantGetTrack
16 | }
17 |
18 | func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
19 | if c.sender == nil {
20 | c.sender = core.NewSender(media, track.Codec)
21 | c.sender.Handler = func(packet *rtp.Packet) {
22 | if c.conn == nil {
23 | return
24 | }
25 | c.send += len(packet.Payload)
26 | _, _ = c.conn.Write(packet.Payload)
27 | }
28 | }
29 |
30 | c.sender.HandleRTP(track)
31 | return nil
32 | }
33 |
34 | func (c *Client) Start() (err error) {
35 | if err = c.Open(); err != nil {
36 | return
37 | }
38 | return
39 | }
40 |
41 | func (c *Client) Stop() (err error) {
42 | if c.sender != nil {
43 | c.sender.Close()
44 | }
45 |
46 | if c.conn != nil {
47 | _ = c.Close()
48 | return c.conn.Close()
49 | }
50 |
51 | return nil
52 | }
53 |
54 | func (c *Client) MarshalJSON() ([]byte, error) {
55 | info := &core.Connection{
56 | ID: core.ID(c),
57 | FormatName: "isapi",
58 | Protocol: "http",
59 | Medias: c.medias,
60 | Send: c.send,
61 | }
62 | if c.conn != nil {
63 | info.RemoteAddr = c.conn.RemoteAddr().String()
64 | }
65 | if c.sender != nil {
66 | info.Senders = []*core.Sender{c.sender}
67 | }
68 | return json.Marshal(info)
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/mp4/README.md:
--------------------------------------------------------------------------------
1 | ## Fragmented MP4
2 |
3 | ```
4 | ffmpeg -i "rtsp://..." -movflags +frag_keyframe+separate_moof+default_base_moof+empty_moov -frag_duration 1 -c copy -t 5 sample.mp4
5 | ```
6 |
7 | - movflags frag_keyframe
8 | Start a new fragment at each video keyframe.
9 | - frag_duration duration
10 | Create fragments that are duration microseconds long.
11 | - movflags separate_moof
12 | Write a separate moof (movie fragment) atom for each track.
13 | - movflags default_base_moof
14 | Similarly to the omit_tfhd_offset, this flag avoids writing the absolute base_data_offset field in tfhd atoms, but does so by using the new default-base-is-moof flag instead.
15 |
16 | https://ffmpeg.org/ffmpeg-formats.html#Options-13
17 |
18 | ## HEVC
19 |
20 | | Browser | avc1 | hvc1 | hev1 |
21 | |-------------|------|------|------|
22 | | Mac Chrome | + | - | + |
23 | | Mac Safari | + | + | - |
24 | | iOS 15? | + | + | - |
25 | | Mac Firefox | + | - | - |
26 | | iOS 12 | + | - | - |
27 | | Android 13 | + | - | - |
28 |
29 | ## Useful links
30 |
31 | - https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1
32 | - https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
33 | - https://jellyfin.org/docs/general/clients/codec-support.html
34 | - https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding
35 | - https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter
36 | - https://gstreamer-devel.narkive.com/rhkUolp2/rtp-dts-pts-result-in-varying-mp4-frame-durations
37 |
--------------------------------------------------------------------------------
/pkg/y4m/consumer.go:
--------------------------------------------------------------------------------
1 | package y4m
2 |
3 | import (
4 | "fmt"
5 | "io"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/pion/rtp"
9 | )
10 |
11 | type Consumer struct {
12 | core.Connection
13 | wr *core.WriteBuffer
14 | }
15 |
16 | func NewConsumer() *Consumer {
17 | wr := core.NewWriteBuffer(nil)
18 | return &Consumer{
19 | core.Connection{
20 | ID: core.NewID(),
21 | Transport: wr,
22 | FormatName: "yuv4mpegpipe",
23 | Medias: []*core.Media{
24 | {
25 | Kind: core.KindVideo,
26 | Direction: core.DirectionSendonly,
27 | Codecs: []*core.Codec{
28 | {Name: core.CodecRAW},
29 | },
30 | },
31 | },
32 | },
33 | wr,
34 | }
35 | }
36 |
37 | func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
38 | sender := core.NewSender(media, track.Codec)
39 | sender.Handler = func(packet *rtp.Packet) {
40 | if n, err := c.wr.Write([]byte(frameHdr)); err == nil {
41 | c.Send += n
42 | }
43 | if n, err := c.wr.Write(packet.Payload); err == nil {
44 | c.Send += n
45 | }
46 | }
47 |
48 | hdr := fmt.Sprintf(
49 | "YUV4MPEG2 W%s H%s C%s\n",
50 | core.Between(track.Codec.FmtpLine, "width=", ";"),
51 | core.Between(track.Codec.FmtpLine, "height=", ";"),
52 | core.Between(track.Codec.FmtpLine, "colorspace=", ";"),
53 | )
54 | if _, err := c.wr.Write([]byte(hdr)); err != nil {
55 | return err
56 | }
57 |
58 | sender.HandleRTP(track)
59 | c.Senders = append(c.Senders, sender)
60 | return nil
61 | }
62 |
63 | func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
64 | return c.wr.WriteTo(wr)
65 | }
66 |
--------------------------------------------------------------------------------
/internal/rtmp/README.md:
--------------------------------------------------------------------------------
1 | ## Tested client
2 |
3 | | From | To | Comment |
4 | |--------|---------------------------------|---------|
5 | | go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK |
6 |
7 | **go2rtc.yaml**
8 |
9 | ```yaml
10 | streams:
11 | rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
12 | rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
13 | rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
14 | ```
15 |
16 | ## Tested server
17 |
18 | | From | To | Comment |
19 | |------------------------|--------|---------------------|
20 | | OBS 31.0.2 | go2rtc | OK |
21 | | OpenIPC 2.5.03.02-lite | go2rtc | OK |
22 | | FFmpeg 6.1 | go2rtc | OK |
23 | | GoPro Black 12 | go2rtc | OK, 1080p, 5000kbps |
24 |
25 | **go2rtc.yaml**
26 |
27 | ```yaml
28 | rtmp:
29 | listen: :1935
30 | streams:
31 | tmp:
32 | ```
33 |
34 | **OBS**
35 |
36 | Settings > Stream:
37 |
38 | - Service: Custom
39 | - Server: rtmp://192.168.10.101/tmp
40 | - Stream Key:
41 | - Use auth:
42 |
43 | **OpenIPC**
44 |
45 | WebUI > Majestic > Settings > Outgoing
46 |
47 | - Enable
48 | - Address: rtmp://192.168.10.101/tmp
49 | - Save
50 | - Restart
51 |
52 | **FFmpeg**
53 |
54 | ```shell
55 | ffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp
56 | ```
57 |
58 | **GoPro**
59 |
60 | GoPro Quik > Camera > Translation > Other
61 |
--------------------------------------------------------------------------------
/internal/streams/preload.go:
--------------------------------------------------------------------------------
1 | package streams
2 |
3 | import (
4 | "fmt"
5 | "maps"
6 | "net/url"
7 | "sync"
8 |
9 | "github.com/AlexxIT/go2rtc/pkg/probe"
10 | )
11 |
12 | type Preload struct {
13 | stream *Stream // Don't output the stream to JSON to not worry about its secrets.
14 | Cons *probe.Probe `json:"consumer"`
15 | Query string `json:"query"`
16 | }
17 |
18 | var preloads = map[string]*Preload{}
19 | var preloadsMu sync.Mutex
20 |
21 | func AddPreload(name, rawQuery string) error {
22 | if rawQuery == "" {
23 | rawQuery = "video&audio"
24 | }
25 |
26 | query, err := url.ParseQuery(rawQuery)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | preloadsMu.Lock()
32 | defer preloadsMu.Unlock()
33 |
34 | if p := preloads[name]; p != nil {
35 | p.stream.RemoveConsumer(p.Cons)
36 | }
37 |
38 | stream := Get(name)
39 | if stream == nil {
40 | return fmt.Errorf("streams: stream not found: %s", name)
41 | }
42 | cons := probe.Create("preload", query)
43 |
44 | if err = stream.AddConsumer(cons); err != nil {
45 | return err
46 | }
47 |
48 | preloads[name] = &Preload{stream: stream, Cons: cons, Query: rawQuery}
49 | return nil
50 | }
51 |
52 | func DelPreload(name string) error {
53 | preloadsMu.Lock()
54 | defer preloadsMu.Unlock()
55 |
56 | if p := preloads[name]; p != nil {
57 | p.stream.RemoveConsumer(p.Cons)
58 | delete(preloads, name)
59 | return nil
60 | }
61 |
62 | return fmt.Errorf("streams: preload not found: %s", name)
63 | }
64 |
65 | func GetPreloads() map[string]*Preload {
66 | preloadsMu.Lock()
67 | defer preloadsMu.Unlock()
68 | return maps.Clone(preloads)
69 | }
70 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:labs
2 |
3 | # 0. Prepare images
4 | ARG PYTHON_VERSION="3.13"
5 | ARG GO_VERSION="1.25"
6 |
7 |
8 | # 1. Build go2rtc binary
9 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
10 | ARG TARGETPLATFORM
11 | ARG TARGETOS
12 | ARG TARGETARCH
13 |
14 | ENV GOOS=${TARGETOS}
15 | ENV GOARCH=${TARGETARCH}
16 |
17 | WORKDIR /build
18 |
19 | RUN apk add git
20 |
21 | # Cache dependencies
22 | COPY go.mod go.sum ./
23 | RUN --mount=type=cache,target=/root/.cache/go-build go mod download
24 |
25 | COPY . .
26 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
27 |
28 |
29 | # 2. Final image
30 | FROM python:${PYTHON_VERSION}-alpine AS base
31 |
32 | # Install ffmpeg, tini (for signal handling),
33 | # and other common tools for the echo source.
34 | # alsa-plugins-pulse for ALSA support (+0MB)
35 | # font-droid for FFmpeg drawtext filter (+2MB)
36 | RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
37 |
38 | # Hardware Acceleration for Intel CPU (+50MB)
39 | ARG TARGETARCH
40 |
41 | RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi
42 |
43 | # Hardware: AMD and NVidia VAAPI (not sure about this)
44 | # RUN libva-glx mesa-va-gallium
45 | # Hardware: AMD and NVidia VDPAU (not sure about this)
46 | # RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
47 |
48 | COPY --from=build /build/go2rtc /usr/local/bin/
49 |
50 | ENTRYPOINT ["/sbin/tini", "--"]
51 | VOLUME /config
52 | WORKDIR /config
53 |
54 | CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
55 |
--------------------------------------------------------------------------------
/pkg/aac/aac_test.go:
--------------------------------------------------------------------------------
1 | package aac
2 |
3 | import (
4 | "encoding/hex"
5 | "testing"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestConfigToCodec(t *testing.T) {
12 | s := "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
13 | s = core.Between(s, "config=", ";")
14 | src, err := hex.DecodeString(s)
15 | require.Nil(t, err)
16 |
17 | codec := ConfigToCodec(src)
18 | require.Equal(t, core.CodecAAC, codec.Name)
19 | require.Equal(t, uint32(24000), codec.ClockRate)
20 | require.Equal(t, uint16(1), codec.Channels)
21 |
22 | dst := EncodeConfig(TypeAACELD, 24000, 1, true)
23 | require.Equal(t, src, dst)
24 | }
25 |
26 | func TestADTS(t *testing.T) {
27 | // FFmpeg MPEG-TS AAC (one packet)
28 | s := "fff15080021ffc210049900219002380fff15080021ffc212049900219002380" //...
29 | src, err := hex.DecodeString(s)
30 | require.Nil(t, err)
31 |
32 | codec := ADTSToCodec(src)
33 | require.Equal(t, uint32(44100), codec.ClockRate)
34 | require.Equal(t, uint16(2), codec.Channels)
35 |
36 | size := ReadADTSSize(src)
37 | require.Equal(t, uint16(16), size)
38 |
39 | dst := CodecToADTS(codec)
40 | WriteADTSSize(dst, size)
41 |
42 | require.Equal(t, src[:len(dst)], dst)
43 | }
44 |
45 | func TestEncodeConfig(t *testing.T) {
46 | conf := EncodeConfig(TypeAACLC, 48000, 1, false)
47 | require.Equal(t, "1188", hex.EncodeToString(conf))
48 | conf = EncodeConfig(TypeAACLC, 16000, 1, false)
49 | require.Equal(t, "1408", hex.EncodeToString(conf))
50 | conf = EncodeConfig(TypeAACLC, 8000, 1, false)
51 | require.Equal(t, "1588", hex.EncodeToString(conf))
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/wav/backchannel.go:
--------------------------------------------------------------------------------
1 | package wav
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/pkg/core"
5 | "github.com/AlexxIT/go2rtc/pkg/shell"
6 | "github.com/pion/rtp"
7 | )
8 |
9 | type Backchannel struct {
10 | core.Connection
11 | cmd *shell.Command
12 | }
13 |
14 | func NewBackchannel(cmd *shell.Command) (core.Producer, error) {
15 | medias := []*core.Media{
16 | {
17 | Kind: core.KindAudio,
18 | Direction: core.DirectionSendonly,
19 | Codecs: []*core.Codec{
20 | //{Name: core.CodecPCML},
21 | {Name: core.CodecPCMA},
22 | {Name: core.CodecPCMU},
23 | },
24 | },
25 | }
26 |
27 | return &Backchannel{
28 | Connection: core.Connection{
29 | ID: core.NewID(),
30 | FormatName: "wav",
31 | Protocol: "pipe",
32 | Medias: medias,
33 | Transport: cmd,
34 | },
35 | cmd: cmd,
36 | }, nil
37 | }
38 |
39 | func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
40 | return nil, core.ErrCantGetTrack
41 | }
42 |
43 | func (c *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
44 | wr, err := c.cmd.StdinPipe()
45 | if err != nil {
46 | return err
47 | }
48 |
49 | b := Header(track.Codec)
50 | if _, err = wr.Write(b); err != nil {
51 | return err
52 | }
53 |
54 | sender := core.NewSender(media, track.Codec)
55 | sender.Handler = func(packet *rtp.Packet) {
56 | if n, err := wr.Write(packet.Payload); err != nil {
57 | c.Send += n
58 | }
59 | }
60 | sender.HandleRTP(track)
61 | c.Senders = append(c.Senders, sender)
62 | return nil
63 | }
64 |
65 | func (c *Backchannel) Start() error {
66 | return c.cmd.Run()
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/h265/avcc.go:
--------------------------------------------------------------------------------
1 | // Package h265 - AVCC format related functions
2 | package h265
3 |
4 | import (
5 | "bytes"
6 | "encoding/base64"
7 | "encoding/binary"
8 |
9 | "github.com/AlexxIT/go2rtc/pkg/core"
10 | "github.com/AlexxIT/go2rtc/pkg/h264"
11 | "github.com/pion/rtp"
12 | )
13 |
14 | func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
15 | vds, sps, pps := GetParameterSet(codec.FmtpLine)
16 | ps := h264.JoinNALU(vds, sps, pps)
17 |
18 | return func(packet *rtp.Packet) {
19 | switch NALUType(packet.Payload) {
20 | case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
21 | clone := *packet
22 | clone.Payload = h264.Join(ps, packet.Payload)
23 | handler(&clone)
24 | default:
25 | handler(packet)
26 | }
27 | }
28 | }
29 |
30 | func AVCCToCodec(avcc []byte) *core.Codec {
31 | buf := bytes.NewBufferString("profile-id=1")
32 |
33 | for {
34 | size := 4 + int(binary.BigEndian.Uint32(avcc))
35 |
36 | switch NALUType(avcc) {
37 | case NALUTypeVPS:
38 | buf.WriteString(";sprop-vps=")
39 | buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
40 | case NALUTypeSPS:
41 | buf.WriteString(";sprop-sps=")
42 | buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
43 | case NALUTypePPS:
44 | buf.WriteString(";sprop-pps=")
45 | buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
46 | }
47 |
48 | if size < len(avcc) {
49 | avcc = avcc[size:]
50 | } else {
51 | break
52 | }
53 | }
54 |
55 | return &core.Codec{
56 | Name: core.CodecH265,
57 | ClockRate: 90000,
58 | FmtpLine: buf.String(),
59 | PayloadType: core.PayloadTypeRAW,
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/h265/helper.go:
--------------------------------------------------------------------------------
1 | package h265
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/binary"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | )
9 |
10 | const (
11 | NALUTypePFrame = 1
12 | NALUTypeIFrame = 19
13 | NALUTypeIFrame2 = 20
14 | NALUTypeIFrame3 = 21
15 | NALUTypeVPS = 32
16 | NALUTypeSPS = 33
17 | NALUTypePPS = 34
18 | NALUTypePrefixSEI = 39
19 | NALUTypeSuffixSEI = 40
20 | NALUTypeFU = 49
21 | )
22 |
23 | func NALUType(b []byte) byte {
24 | return (b[4] >> 1) & 0x3F
25 | }
26 |
27 | func IsKeyframe(b []byte) bool {
28 | for {
29 | switch NALUType(b) {
30 | case NALUTypePFrame:
31 | return false
32 | case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
33 | return true
34 | }
35 |
36 | size := int(binary.BigEndian.Uint32(b)) + 4
37 | if size < len(b) {
38 | b = b[size:]
39 | continue
40 | } else {
41 | return false
42 | }
43 | }
44 | }
45 |
46 | func Types(data []byte) []byte {
47 | var types []byte
48 | for {
49 | types = append(types, NALUType(data))
50 |
51 | size := 4 + int(binary.BigEndian.Uint32(data))
52 | if size < len(data) {
53 | data = data[size:]
54 | } else {
55 | break
56 | }
57 | }
58 | return types
59 | }
60 |
61 | func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
62 | if fmtp == "" {
63 | return
64 | }
65 |
66 | s := core.Between(fmtp, "sprop-vps=", ";")
67 | vps, _ = base64.StdEncoding.DecodeString(s)
68 |
69 | s = core.Between(fmtp, "sprop-sps=", ";")
70 | sps, _ = base64.StdEncoding.DecodeString(s)
71 |
72 | s = core.Between(fmtp, "sprop-pps=", ";")
73 | pps, _ = base64.StdEncoding.DecodeString(s)
74 |
75 | return
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/mpegts/opus.go:
--------------------------------------------------------------------------------
1 | package mpegts
2 |
3 | import (
4 | "github.com/AlexxIT/go2rtc/pkg/bits"
5 | )
6 |
7 | // opusDT - each AU from FFmpeg has 5 OPUS packets. Each packet len = 960 in the 48000 clock.
8 | const opusDT = 960 * ClockRate / 48000
9 |
10 | // https://opus-codec.org/docs/
11 | var opusInfo = []byte{ // registration_descriptor
12 | 0x05, // descriptor_tag
13 | 0x04, // descriptor_length
14 | 'O', 'p', 'u', 's', // format_identifier
15 | }
16 |
17 | //goland:noinspection GoSnakeCaseUsage
18 | func CutOPUSPacket(b []byte) (packet []byte, left []byte) {
19 | r := bits.NewReader(b)
20 |
21 | size := opus_control_header(r)
22 | if size == 0 {
23 | return nil, nil
24 | }
25 |
26 | packet = r.ReadBytes(size)
27 | left = r.Left()
28 | return
29 | }
30 |
31 | //goland:noinspection GoSnakeCaseUsage
32 | func opus_control_header(r *bits.Reader) int {
33 | control_header_prefix := r.ReadBits(11)
34 | if control_header_prefix != 0x3FF {
35 | return 0
36 | }
37 |
38 | start_trim_flag := r.ReadBit()
39 | end_trim_flag := r.ReadBit()
40 | control_extension_flag := r.ReadBit()
41 | _ = r.ReadBits(2) // reserved
42 |
43 | var payload_size int
44 | for {
45 | i := r.ReadByte()
46 | payload_size += int(i)
47 | if i < 255 {
48 | break
49 | }
50 | }
51 |
52 | if start_trim_flag != 0 {
53 | _ = r.ReadBits(3)
54 | _ = r.ReadBits(13)
55 | }
56 | if end_trim_flag != 0 {
57 | _ = r.ReadBits(3)
58 | _ = r.ReadBits(13)
59 | }
60 | if control_extension_flag != 0 {
61 | control_extension_length := r.ReadByte()
62 | _ = r.ReadBytes(int(control_extension_length)) // reserved
63 | }
64 |
65 | return payload_size
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/webtorrent/crypto.go:
--------------------------------------------------------------------------------
1 | package webtorrent
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/sha256"
7 | "encoding/base64"
8 | "fmt"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | type Cipher struct {
14 | gcm cipher.AEAD
15 | iv []byte
16 | nonce []byte
17 | }
18 |
19 | func NewCipher(share, pwd, nonce string) (*Cipher, error) {
20 | timestamp, err := strconv.ParseInt(nonce, 36, 64)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | delta := time.Duration(time.Now().UnixNano() - timestamp)
26 | if delta < 0 {
27 | delta = -delta
28 | }
29 |
30 | // protect from replay attack, but respect wrong timezone on server
31 | if delta > 12*time.Hour {
32 | return nil, fmt.Errorf("wrong timedelta %s", delta)
33 | }
34 |
35 | c := &Cipher{}
36 |
37 | hash := sha256.New()
38 | hash.Write([]byte(nonce + ":" + pwd))
39 | key := hash.Sum(nil)
40 |
41 | hash.Reset()
42 | hash.Write([]byte(share + ":" + nonce))
43 | c.iv = hash.Sum(nil)[:12]
44 |
45 | block, err := aes.NewCipher(key)
46 | if err != nil {
47 | return nil, err
48 | }
49 | c.gcm, err = cipher.NewGCM(block)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | c.nonce = []byte(nonce)
55 |
56 | return c, nil
57 | }
58 |
59 | func (c *Cipher) Decrypt(ciphertext []byte) ([]byte, error) {
60 | return c.gcm.Open(nil, c.iv, ciphertext, c.nonce)
61 | }
62 |
63 | func (c *Cipher) Encrypt(plaintext []byte) []byte {
64 | return c.gcm.Seal(nil, c.iv, plaintext, c.nonce)
65 | }
66 |
67 | func InfoHash(share string) string {
68 | hash := sha256.New()
69 | hash.Write([]byte(share))
70 | sum := hash.Sum(nil)
71 | return base64.StdEncoding.EncodeToString(sum)
72 | }
73 |
--------------------------------------------------------------------------------
/docker/rockchip.Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:labs
2 |
3 | # 0. Prepare images
4 | ARG PYTHON_VERSION="3.13-slim-bookworm"
5 | ARG GO_VERSION="1.25-bookworm"
6 |
7 |
8 | # 1. Build go2rtc binary
9 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
10 | ARG TARGETPLATFORM
11 | ARG TARGETOS
12 | ARG TARGETARCH
13 |
14 | ENV GOOS=${TARGETOS}
15 | ENV GOARCH=${TARGETARCH}
16 |
17 | WORKDIR /build
18 |
19 | # Cache dependencies
20 | COPY go.mod go.sum ./
21 | RUN --mount=type=cache,target=/root/.cache/go-build go mod download
22 |
23 | COPY . .
24 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
25 |
26 |
27 | # 2. Final image
28 | FROM python:${PYTHON_VERSION}
29 |
30 | # Prepare apt for buildkit cache
31 | RUN rm -f /etc/apt/apt.conf.d/docker-clean \
32 | && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
33 |
34 | # Install ffmpeg, tini (for signal handling),
35 | # and other common tools for the echo source.
36 | # libasound2-plugins for ALSA support
37 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
38 | apt-get -y update && apt-get -y install tini \
39 | curl jq \
40 | libasound2-plugins && \
41 | apt-get clean && rm -rf /var/lib/apt/lists/*
42 |
43 | COPY --from=build /build/go2rtc /usr/local/bin/
44 | ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin
45 |
46 | ENTRYPOINT ["/usr/bin/tini", "--"]
47 | VOLUME /config
48 | WORKDIR /config
49 |
50 | CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
51 |
--------------------------------------------------------------------------------
/internal/debug/stack.go:
--------------------------------------------------------------------------------
1 | package debug
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "runtime"
8 |
9 | "github.com/AlexxIT/go2rtc/internal/api"
10 | )
11 |
12 | var stackSkip = [][]byte{
13 | // main.go
14 | []byte("main.main()"),
15 | []byte("created by os/signal.Notify"),
16 |
17 | // api/stack.go
18 | []byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"),
19 |
20 | // api/api.go
21 | []byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"),
22 | []byte("created by net/http.(*connReader).startBackgroundRead"),
23 | []byte("created by net/http.(*Server).Serve"), // TODO: why two?
24 |
25 | []byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
26 | []byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
27 |
28 | // homekit
29 | []byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
30 |
31 | // webrtc/api.go
32 | []byte("created by github.com/pion/ice/v4.NewTCPMuxDefault"),
33 | []byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
34 | }
35 |
36 | func stackHandler(w http.ResponseWriter, r *http.Request) {
37 | sep := []byte("\n\n")
38 | buf := make([]byte, 65535)
39 | i := 0
40 | n := runtime.Stack(buf, true)
41 | skipped := 0
42 | for _, item := range bytes.Split(buf[:n], sep) {
43 | for _, skip := range stackSkip {
44 | if bytes.Contains(item, skip) {
45 | item = nil
46 | skipped++
47 | break
48 | }
49 | }
50 | if item != nil {
51 | i += copy(buf[i:], item)
52 | i += copy(buf[i:], sep)
53 | }
54 | }
55 | i += copy(buf[i:], fmt.Sprintf(
56 | "Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
57 | )
58 |
59 | api.Response(w, buf[:i], api.MimeText)
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/core/media_test.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "testing"
7 |
8 | "github.com/pion/sdp/v3"
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestSDP(t *testing.T) {
14 | medias := []*Media{{
15 | Kind: KindAudio, Direction: DirectionSendonly,
16 | Codecs: []*Codec{
17 | {Name: CodecPCMU, ClockRate: 8000},
18 | },
19 | }}
20 |
21 | data, err := MarshalSDP("go2rtc/1.0.0", medias)
22 | assert.Empty(t, err)
23 |
24 | sd := &sdp.SessionDescription{}
25 | err = sd.Unmarshal(data)
26 | assert.Empty(t, err)
27 | }
28 |
29 | func TestParseQuery(t *testing.T) {
30 | u, _ := url.Parse("rtsp://localhost:8554/camera1")
31 | medias := ParseQuery(u.Query())
32 | assert.Nil(t, medias)
33 |
34 | for _, rawULR := range []string{
35 | "rtsp://localhost:8554/camera1?video",
36 | "rtsp://localhost:8554/camera1?video=copy",
37 | "rtsp://localhost:8554/camera1?video=any",
38 | } {
39 | u, _ = url.Parse(rawULR)
40 | medias = ParseQuery(u.Query())
41 | assert.Equal(t, []*Media{
42 | {Kind: KindVideo, Direction: DirectionSendonly, Codecs: []*Codec{{Name: CodecAny}}},
43 | }, medias)
44 | }
45 | }
46 |
47 | func TestClone(t *testing.T) {
48 | media1 := &Media{
49 | Kind: KindVideo,
50 | Direction: DirectionRecvonly,
51 | Codecs: []*Codec{
52 | {Name: CodecPCMU, ClockRate: 8000},
53 | },
54 | }
55 | media2 := media1.Clone()
56 |
57 | p1 := fmt.Sprintf("%p", media1)
58 | p2 := fmt.Sprintf("%p", media2)
59 | require.NotEqualValues(t, p1, p2)
60 |
61 | p3 := fmt.Sprintf("%p", media1.Codecs[0])
62 | p4 := fmt.Sprintf("%p", media2.Codecs[0])
63 | require.NotEqualValues(t, p3, p4)
64 | }
65 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e # Exit immediately if a command exits with a non-zero status.
4 | set -u # Treat unset variables as an error when substituting.
5 |
6 | check_command() {
7 | if ! command -v "$1" >/dev/null
8 | then
9 | echo "Error: $1 could not be found. Please install it." >&2
10 | return 1
11 | fi
12 | }
13 |
14 | build_zip() {
15 | go build -ldflags "-s -w" -trimpath -o $2
16 | 7z a -mx9 -sdel $1 $2
17 | }
18 |
19 | build_upx() {
20 | go build -ldflags "-s -w" -trimpath -o $1
21 | upx --best --lzma $1
22 | }
23 |
24 | check_command go
25 | check_command 7z
26 | check_command upx
27 |
28 | export CGO_ENABLED=0
29 |
30 | set -x # Print commands and their arguments as they are executed.
31 |
32 | GOOS=windows GOARCH=amd64 build_zip go2rtc_win64.zip go2rtc.exe
33 | GOOS=windows GOARCH=386 build_zip go2rtc_win32.zip go2rtc.exe
34 | GOOS=windows GOARCH=arm64 build_zip go2rtc_win_arm64.zip go2rtc.exe
35 |
36 | GOOS=linux GOARCH=amd64 build_upx go2rtc_linux_amd64
37 | GOOS=linux GOARCH=386 build_upx go2rtc_linux_i386
38 | GOOS=linux GOARCH=arm64 build_upx go2rtc_linux_arm64
39 | GOOS=linux GOARCH=mipsle build_upx go2rtc_linux_mipsel
40 | GOOS=linux GOARCH=arm GOARM=7 build_upx go2rtc_linux_arm
41 | GOOS=linux GOARCH=arm GOARM=6 build_upx go2rtc_linux_armv6
42 |
43 | GOOS=darwin GOARCH=amd64 build_zip go2rtc_mac_amd64.zip go2rtc
44 | GOOS=darwin GOARCH=arm64 build_zip go2rtc_mac_arm64.zip go2rtc
45 |
46 | GOOS=freebsd GOARCH=amd64 build_zip go2rtc_freebsd_amd64.zip go2rtc
47 | GOOS=freebsd GOARCH=arm64 build_zip go2rtc_freebsd_arm64.zip go2rtc
48 |
--------------------------------------------------------------------------------
/internal/ffmpeg/hardware/hardware_bsd.go:
--------------------------------------------------------------------------------
1 | //go:build freebsd || netbsd || openbsd || dragonfly
2 |
3 | package hardware
4 |
5 | import (
6 | "runtime"
7 |
8 | "github.com/AlexxIT/go2rtc/internal/api"
9 | )
10 |
11 | const (
12 | ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
13 | ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
14 | ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
15 | ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
16 | )
17 |
18 | func ProbeAll(bin string) []*api.Source {
19 | return []*api.Source{
20 | {
21 | Name: runToString(bin, ProbeV4L2M2MH264),
22 | URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
23 | },
24 | {
25 | Name: runToString(bin, ProbeV4L2M2MH265),
26 | URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
27 | },
28 | {
29 | Name: runToString(bin, ProbeRKMPPH264),
30 | URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
31 | },
32 | {
33 | Name: runToString(bin, ProbeRKMPPH265),
34 | URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
35 | },
36 | }
37 | }
38 |
39 | func ProbeHardware(bin, name string) string {
40 | if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
41 | switch name {
42 | case "h264":
43 | if run(bin, ProbeV4L2M2MH264) {
44 | return EngineV4L2M2M
45 | }
46 | if run(bin, ProbeRKMPPH264) {
47 | return EngineRKMPP
48 | }
49 | case "h265":
50 | if run(bin, ProbeV4L2M2MH265) {
51 | return EngineV4L2M2M
52 | }
53 | if run(bin, ProbeRKMPPH265) {
54 | return EngineRKMPP
55 | }
56 | }
57 |
58 | return EngineSoftware
59 | }
60 |
61 | return EngineSoftware
62 | }
63 |
--------------------------------------------------------------------------------
/internal/mpegts/mpegts.go:
--------------------------------------------------------------------------------
1 | package mpegts
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/api"
7 | "github.com/AlexxIT/go2rtc/internal/streams"
8 | "github.com/AlexxIT/go2rtc/pkg/mpegts"
9 | )
10 |
11 | func Init() {
12 | api.HandleFunc("api/stream.ts", apiHandle)
13 | api.HandleFunc("api/stream.aac", apiStreamAAC)
14 | }
15 |
16 | func apiHandle(w http.ResponseWriter, r *http.Request) {
17 | if r.Method != "POST" {
18 | outputMpegTS(w, r)
19 | } else {
20 | inputMpegTS(w, r)
21 | }
22 | }
23 |
24 | func outputMpegTS(w http.ResponseWriter, r *http.Request) {
25 | src := r.URL.Query().Get("src")
26 | stream := streams.Get(src)
27 | if stream == nil {
28 | http.Error(w, api.StreamNotFound, http.StatusNotFound)
29 | return
30 | }
31 |
32 | cons := mpegts.NewConsumer()
33 | cons.WithRequest(r)
34 |
35 | if err := stream.AddConsumer(cons); err != nil {
36 | http.Error(w, err.Error(), http.StatusInternalServerError)
37 | return
38 | }
39 |
40 | w.Header().Add("Content-Type", "video/mp2t")
41 |
42 | _, _ = cons.WriteTo(w)
43 |
44 | stream.RemoveConsumer(cons)
45 | }
46 |
47 | func inputMpegTS(w http.ResponseWriter, r *http.Request) {
48 | dst := r.URL.Query().Get("dst")
49 | stream := streams.Get(dst)
50 | if stream == nil {
51 | http.Error(w, api.StreamNotFound, http.StatusNotFound)
52 | return
53 | }
54 |
55 | client, err := mpegts.Open(r.Body)
56 | if err != nil {
57 | http.Error(w, err.Error(), http.StatusInternalServerError)
58 | return
59 | }
60 |
61 | stream.AddProducer(client)
62 | defer stream.RemoveProducer(client)
63 |
64 | if err = client.Start(); err != nil {
65 | http.Error(w, err.Error(), http.StatusInternalServerError)
66 | return
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/magic/producer.go:
--------------------------------------------------------------------------------
1 | package magic
2 |
3 | import (
4 | "bytes"
5 | "encoding/hex"
6 | "errors"
7 | "io"
8 |
9 | "github.com/AlexxIT/go2rtc/pkg/aac"
10 | "github.com/AlexxIT/go2rtc/pkg/core"
11 | "github.com/AlexxIT/go2rtc/pkg/flv"
12 | "github.com/AlexxIT/go2rtc/pkg/h264/annexb"
13 | "github.com/AlexxIT/go2rtc/pkg/magic/bitstream"
14 | "github.com/AlexxIT/go2rtc/pkg/magic/mjpeg"
15 | "github.com/AlexxIT/go2rtc/pkg/mpegts"
16 | "github.com/AlexxIT/go2rtc/pkg/mpjpeg"
17 | "github.com/AlexxIT/go2rtc/pkg/wav"
18 | "github.com/AlexxIT/go2rtc/pkg/y4m"
19 | )
20 |
21 | func Open(r io.Reader) (core.Producer, error) {
22 | rd := core.NewReadBuffer(r)
23 |
24 | b, err := rd.Peek(4)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | switch string(b) {
30 | case annexb.StartCode:
31 | return bitstream.Open(rd)
32 | case wav.FourCC:
33 | return wav.Open(rd)
34 | case y4m.FourCC:
35 | return y4m.Open(rd)
36 | }
37 |
38 | switch string(b[:3]) {
39 | case flv.Signature:
40 | return flv.Open(rd)
41 | }
42 |
43 | switch string(b[:2]) {
44 | case "\xFF\xD8":
45 | return mjpeg.Open(rd)
46 | case "\xFF\xF1", "\xFF\xF9":
47 | return aac.Open(rd)
48 | case "--":
49 | return mpjpeg.Open(rd)
50 | }
51 |
52 | switch b[0] {
53 | case mpegts.SyncByte:
54 | return mpegts.Open(rd)
55 | }
56 |
57 | // support MJPEG with trash on start
58 | // https://github.com/AlexxIT/go2rtc/issues/747
59 | if b, err = rd.Peek(4096); err != nil {
60 | return nil, err
61 | }
62 |
63 | if i := bytes.Index(b, []byte{0xFF, 0xD8, 0xFF, 0xDB}); i > 0 {
64 | _, _ = io.ReadFull(rd, make([]byte, i))
65 | return mjpeg.Open(rd)
66 | }
67 |
68 | return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b[:4]))
69 | }
70 |
--------------------------------------------------------------------------------
/internal/pinggy/README.md:
--------------------------------------------------------------------------------
1 | # Pinggy
2 |
3 | [Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services.
4 |
5 | **Features:**
6 |
7 | - A free account does not require registration.
8 | - It does not require downloading third-party binaries and works over the SSH protocol.
9 | - Works with HTTP, TCP and UDP protocols.
10 | - Creates HTTPS for your HTTP services.
11 |
12 | > [!IMPORTANT]
13 | > A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY.
14 |
15 | > [!CAUTION]
16 | > Public access to go2rtc without authorization puts your entire home network at risk. Use with caution.
17 |
18 | **Why:**
19 |
20 | - It's easy to set up HTTPS for testing two-way audio.
21 | - It's easy to check whether external access via WebRTC technology will work.
22 | - It's easy to share direct access to your RTSP or HTTP camera with the go2rtc developer. If such access is necessary to debug your problem.
23 |
24 | ## Configuration
25 |
26 | You will find public links in the go2rtc log after startup.
27 |
28 | **Tunnel to go2rtc WebUI.**
29 |
30 | ```yaml
31 | pinggy:
32 | tunnel: http://localhost:1984
33 | ```
34 |
35 | **Tunnel to RTSP camera.**
36 |
37 | For example, you have camera: `rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0`
38 |
39 | ```yaml
40 | pinggy:
41 | tunnel: tcp://192.168.10.91:554
42 | ```
43 |
44 | In go2rtc logs you will get similar output:
45 |
46 | ```
47 | 16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345
48 | ```
49 |
50 | Now you have working stream:
51 |
52 | ```
53 | rtsp://admin:password@abcde-123-123-123-123.a.free.pinggy.link:12345/cam/realmonitor?channel=1&subtype=0
54 | ```
55 |
--------------------------------------------------------------------------------
/internal/mjpeg/README.md:
--------------------------------------------------------------------------------
1 | ## Stream as ASCII to Terminal
2 |
3 | [](https://www.youtube.com/watch?v=sHj_3h_sX7M)
4 |
5 | **Tips**
6 |
7 | - this feature works only with MJPEG codec (use transcoding)
8 | - choose a low frame rate (FPS)
9 | - choose the width and height to fit in your terminal
10 | - different terminals support different numbers of colours (8, 256, rgb)
11 | - escape text param with urlencode
12 | - you can stream any camera or file from a disc
13 |
14 | **go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10
15 |
16 | ```yaml
17 | streams:
18 | gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10
19 | ```
20 |
21 | **API params**
22 |
23 | - `color` - foreground color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
24 | - example: `30` (black), `37` (white), `38;5;226` (yellow)
25 | - `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
26 | - example: `40` (black), `47` (white), `48;5;226` (yellow)
27 | - `text` - character set, values: empty, one char, `block`, list of chars (in order of brightness)
28 | - example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)
29 |
30 | **Examples**
31 |
32 | ```bash
33 | % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda"
34 | % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256"
35 | % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20"
36 | % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20"
37 | % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"
38 | ```
39 |
--------------------------------------------------------------------------------
/pkg/magic/mjpeg/producer.go:
--------------------------------------------------------------------------------
1 | package mjpeg
2 |
3 | import (
4 | "bytes"
5 | "io"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/pion/rtp"
9 | )
10 |
11 | type Producer struct {
12 | core.Connection
13 | rd *core.ReadBuffer
14 | }
15 |
16 | func Open(rd io.Reader) (*Producer, error) {
17 | medias := []*core.Media{
18 | {
19 | Kind: core.KindVideo,
20 | Direction: core.DirectionRecvonly,
21 | Codecs: []*core.Codec{
22 | {
23 | Name: core.CodecJPEG,
24 | ClockRate: 90000,
25 | PayloadType: core.PayloadTypeRAW,
26 | },
27 | },
28 | },
29 | }
30 | return &Producer{
31 | Connection: core.Connection{
32 | ID: core.NewID(),
33 | FormatName: "mjpeg",
34 | Medias: medias,
35 | Transport: rd,
36 | },
37 | rd: core.NewReadBuffer(rd),
38 | }, nil
39 | }
40 |
41 | func (c *Producer) Start() error {
42 | var buf []byte // total bufer
43 | b := make([]byte, core.BufferSize) // reading buffer
44 |
45 | for {
46 | // one JPEG end and next start
47 | i := bytes.Index(buf, []byte{0xFF, 0xD9, 0xFF, 0xD8})
48 | if i < 0 {
49 | n, err := c.rd.Read(b)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | c.Recv += n
55 |
56 | buf = append(buf, b[:n]...)
57 |
58 | // if we receive frame
59 | if n >= 2 && b[n-2] == 0xFF && b[n-1] == 0xD9 {
60 | i = len(buf)
61 | } else {
62 | continue
63 | }
64 | } else {
65 | i += 2
66 | }
67 |
68 | pkt := &rtp.Packet{
69 | Header: rtp.Header{Timestamp: core.Now90000()},
70 | Payload: buf[:i],
71 | }
72 | c.Receivers[0].WriteRTP(pkt)
73 |
74 | //log.Printf("[mjpeg] ts=%d size=%d", pkt.Header.Timestamp, len(pkt.Payload))
75 |
76 | buf = buf[i:]
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/tuya/helper.go:
--------------------------------------------------------------------------------
1 | package tuya
2 |
3 | import (
4 | "crypto/md5"
5 | cryptoRand "crypto/rand"
6 | "crypto/rsa"
7 | "crypto/x509"
8 | "encoding/hex"
9 | "encoding/pem"
10 | "errors"
11 | "net/http"
12 | "net/http/cookiejar"
13 | "regexp"
14 | "time"
15 |
16 | "golang.org/x/net/publicsuffix"
17 | )
18 |
19 | func EncryptPassword(password, pbKey string) (string, error) {
20 | // Hash password with MD5
21 | hasher := md5.New()
22 | hasher.Write([]byte(password))
23 | hashedPassword := hex.EncodeToString(hasher.Sum(nil))
24 |
25 | // Decode PEM public key
26 | block, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\n" + pbKey + "\n-----END PUBLIC KEY-----"))
27 | if block == nil {
28 | return "", errors.New("failed to decode PEM block")
29 | }
30 |
31 | pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
32 | if err != nil {
33 | return "", err
34 | }
35 |
36 | rsaPubKey, ok := pubKey.(*rsa.PublicKey)
37 | if !ok {
38 | return "", errors.New("not an RSA public key")
39 | }
40 |
41 | // Encrypt with RSA
42 | encrypted, err := rsa.EncryptPKCS1v15(cryptoRand.Reader, rsaPubKey, []byte(hashedPassword))
43 | if err != nil {
44 | return "", err
45 | }
46 |
47 | // Convert to hex string
48 | return hex.EncodeToString(encrypted), nil
49 | }
50 |
51 | func IsEmailAddress(input string) bool {
52 | emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
53 | return emailRegex.MatchString(input)
54 | }
55 |
56 | func CreateHTTPClientWithSession() *http.Client {
57 | jar, err := cookiejar.New(&cookiejar.Options{
58 | PublicSuffixList: publicsuffix.List,
59 | })
60 |
61 | if err != nil {
62 | return nil
63 | }
64 |
65 | return &http.Client{
66 | Timeout: 30 * time.Second,
67 | Jar: jar,
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/wav/producer.go:
--------------------------------------------------------------------------------
1 | package wav
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "io"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/pion/rtp"
10 | )
11 |
12 | const FourCC = "RIFF"
13 |
14 | func Open(r io.Reader) (*Producer, error) {
15 | // https://en.wikipedia.org/wiki/WAV
16 | // https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
17 | rd := bufio.NewReaderSize(r, core.BufferSize)
18 |
19 | codec, err := ReadHeader(r)
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | if codec.Name == "" {
25 | return nil, errors.New("waw: unsupported codec")
26 | }
27 |
28 | medias := []*core.Media{
29 | {
30 | Kind: core.KindAudio,
31 | Direction: core.DirectionRecvonly,
32 | Codecs: []*core.Codec{codec},
33 | },
34 | }
35 | return &Producer{
36 | Connection: core.Connection{
37 | ID: core.NewID(),
38 | FormatName: "wav",
39 | Medias: medias,
40 | Transport: r,
41 | },
42 | rd: rd,
43 | }, nil
44 | }
45 |
46 | type Producer struct {
47 | core.Connection
48 | rd *bufio.Reader
49 | }
50 |
51 | func (c *Producer) Start() error {
52 | var seq uint16
53 | var ts uint32
54 |
55 | const PacketSize = 0.040 * 8000 // 40ms
56 |
57 | for {
58 | payload := make([]byte, PacketSize)
59 | if _, err := io.ReadFull(c.rd, payload); err != nil {
60 | return err
61 | }
62 |
63 | c.Recv += PacketSize
64 |
65 | if len(c.Receivers) == 0 {
66 | continue
67 | }
68 |
69 | pkt := &rtp.Packet{
70 | Header: rtp.Header{
71 | Version: 2,
72 | Marker: true,
73 | SequenceNumber: seq,
74 | Timestamp: ts,
75 | },
76 | Payload: payload,
77 | }
78 | c.Receivers[0].WriteRTP(pkt)
79 |
80 | seq++
81 | ts += PacketSize
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/tcp/websocket/dial.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | cryptorand "crypto/rand"
5 | "crypto/sha1"
6 | "encoding/base64"
7 | "errors"
8 | "net"
9 | "net/http"
10 | "strings"
11 |
12 | "github.com/AlexxIT/go2rtc/pkg/tcp"
13 | )
14 |
15 | func Dial(address string) (net.Conn, error) {
16 | if strings.HasPrefix(address, "ws") {
17 | address = "http" + address[2:] // support http and https
18 | }
19 |
20 | // using custom client for support Digest Auth
21 | // https://github.com/AlexxIT/go2rtc/issues/415
22 | ctx, pconn := tcp.WithConn()
23 |
24 | req, err := http.NewRequestWithContext(ctx, "GET", address, nil)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | key, accept := GetKeyAccept()
30 |
31 | // Version, Key, Protocol important for Axis cameras
32 | req.Header.Set("Connection", "Upgrade")
33 | req.Header.Set("Upgrade", "websocket")
34 | req.Header.Set("Sec-WebSocket-Version", "13")
35 | req.Header.Set("Sec-WebSocket-Key", key)
36 | req.Header.Set("Sec-WebSocket-Protocol", "binary")
37 |
38 | res, err := tcp.Do(req)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | if res.StatusCode != http.StatusSwitchingProtocols {
44 | return nil, errors.New("wrong status: " + res.Status)
45 | }
46 |
47 | if res.Header.Get("Sec-Websocket-Accept") != accept {
48 | return nil, errors.New("wrong websocket accept")
49 | }
50 |
51 | return NewClient(*pconn), nil
52 | }
53 |
54 | func GetKeyAccept() (key, accept string) {
55 | b := make([]byte, 16)
56 | _, _ = cryptorand.Read(b)
57 | key = base64.StdEncoding.EncodeToString(b)
58 |
59 | h := sha1.New()
60 | h.Write([]byte(key))
61 | h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
62 | accept = base64.StdEncoding.EncodeToString(h.Sum(nil))
63 |
64 | return
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/pcm/backchannel.go:
--------------------------------------------------------------------------------
1 | package pcm
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | "github.com/AlexxIT/go2rtc/pkg/shell"
8 | "github.com/pion/rtp"
9 | )
10 |
11 | type Backchannel struct {
12 | core.Connection
13 | cmd *shell.Command
14 | }
15 |
16 | func NewBackchannel(cmd *shell.Command, audio string) (core.Producer, error) {
17 | var codec *core.Codec
18 |
19 | if audio == "" {
20 | // default codec
21 | codec = &core.Codec{Name: core.CodecPCML, ClockRate: 16000}
22 | } else if codec = core.ParseCodecString(audio); codec == nil {
23 | return nil, errors.New("pcm: unsupported audio format: " + audio)
24 | }
25 |
26 | medias := []*core.Media{
27 | {
28 | Kind: core.KindAudio,
29 | Direction: core.DirectionSendonly,
30 | Codecs: []*core.Codec{codec},
31 | },
32 | }
33 |
34 | return &Backchannel{
35 | Connection: core.Connection{
36 | ID: core.NewID(),
37 | FormatName: "pcm",
38 | Protocol: "pipe",
39 | Medias: medias,
40 | Transport: cmd,
41 | },
42 | cmd: cmd,
43 | }, nil
44 | }
45 |
46 | func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
47 | return nil, core.ErrCantGetTrack
48 | }
49 |
50 | func (c *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
51 | wr, err := c.cmd.StdinPipe()
52 | if err != nil {
53 | return err
54 | }
55 |
56 | sender := core.NewSender(media, track.Codec)
57 | sender.Handler = func(packet *rtp.Packet) {
58 | if n, err := wr.Write(packet.Payload); err != nil {
59 | c.Send += n
60 | }
61 | }
62 | sender.HandleRTP(track)
63 | c.Senders = append(c.Senders, sender)
64 | return nil
65 | }
66 |
67 | func (c *Backchannel) Start() error {
68 | return c.cmd.Run()
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/y4m/producer.go:
--------------------------------------------------------------------------------
1 | package y4m
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "io"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/pion/rtp"
10 | )
11 |
12 | func Open(r io.Reader) (*Producer, error) {
13 | rd := bufio.NewReaderSize(r, core.BufferSize)
14 | b, err := rd.ReadBytes('\n')
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | b = b[:len(b)-1] // remove \n
20 |
21 | fmtp := ParseHeader(b)
22 |
23 | if GetSize(fmtp) == 0 {
24 | return nil, errors.New("y4m: unsupported format: " + string(b))
25 | }
26 |
27 | medias := []*core.Media{
28 | {
29 | Kind: core.KindVideo,
30 | Direction: core.DirectionRecvonly,
31 | Codecs: []*core.Codec{
32 | {
33 | Name: core.CodecRAW,
34 | ClockRate: 90000,
35 | FmtpLine: fmtp,
36 | PayloadType: core.PayloadTypeRAW,
37 | },
38 | },
39 | },
40 | }
41 | return &Producer{
42 | Connection: core.Connection{
43 | ID: core.NewID(),
44 | FormatName: "yuv4mpegpipe",
45 | Medias: medias,
46 | SDP: string(b),
47 | Transport: r,
48 | },
49 | rd: rd,
50 | }, nil
51 | }
52 |
53 | type Producer struct {
54 | core.Connection
55 | rd *bufio.Reader
56 | }
57 |
58 | func (c *Producer) Start() error {
59 | size := GetSize(c.Medias[0].Codecs[0].FmtpLine)
60 |
61 | for {
62 | if _, err := c.rd.Discard(len(frameHdr)); err != nil {
63 | return err
64 | }
65 |
66 | frame := make([]byte, size)
67 | if _, err := io.ReadFull(c.rd, frame); err != nil {
68 | return err
69 | }
70 |
71 | c.Recv += size
72 |
73 | if len(c.Receivers) == 0 {
74 | continue
75 | }
76 |
77 | pkt := &rtp.Packet{
78 | Header: rtp.Header{Timestamp: core.Now90000()},
79 | Payload: frame,
80 | }
81 | c.Receivers[0].WriteRTP(pkt)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/creds/secrets.go:
--------------------------------------------------------------------------------
1 | package creds
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "slices"
7 | "strings"
8 | "sync"
9 | )
10 |
11 | func AddSecret(value string) {
12 | if value == "" {
13 | return
14 | }
15 |
16 | secretsMu.Lock()
17 | defer secretsMu.Unlock()
18 |
19 | if slices.Contains(secrets, value) {
20 | return
21 | }
22 |
23 | secrets = append(secrets, value)
24 | secretsReplacer = nil
25 | }
26 |
27 | var secrets []string
28 | var secretsMu sync.Mutex
29 | var secretsReplacer *strings.Replacer
30 |
31 | func getReplacer() *strings.Replacer {
32 | secretsMu.Lock()
33 | defer secretsMu.Unlock()
34 |
35 | if secretsReplacer == nil {
36 | oldnew := make([]string, 0, 2*len(secrets))
37 | for _, s := range secrets {
38 | oldnew = append(oldnew, s, "***")
39 | }
40 | secretsReplacer = strings.NewReplacer(oldnew...)
41 | }
42 |
43 | return secretsReplacer
44 | }
45 |
46 | func SecretString(s string) string {
47 | re := getReplacer()
48 | return re.Replace(s)
49 | }
50 |
51 | func SecretWriter(w io.Writer) io.Writer {
52 | return &secretWriter{w}
53 | }
54 |
55 | type secretWriter struct {
56 | w io.Writer
57 | }
58 |
59 | func (s *secretWriter) Write(b []byte) (int, error) {
60 | re := getReplacer()
61 | return re.WriteString(s.w, string(b))
62 | }
63 |
64 | type secretResponse struct {
65 | w http.ResponseWriter
66 | }
67 |
68 | func (s *secretResponse) Header() http.Header {
69 | return s.w.Header()
70 | }
71 |
72 | func (s *secretResponse) Write(b []byte) (int, error) {
73 | re := getReplacer()
74 | return re.WriteString(s.w, string(b))
75 | }
76 |
77 | func (s *secretResponse) WriteHeader(statusCode int) {
78 | s.w.WriteHeader(statusCode)
79 | }
80 |
81 | func SecretResponse(w http.ResponseWriter) http.ResponseWriter {
82 | return &secretResponse{w}
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/creds/creds.go:
--------------------------------------------------------------------------------
1 | package creds
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | type Storage interface {
12 | SetValue(name, value string) error
13 | GetValue(name string) (string, bool)
14 | }
15 |
16 | var storage Storage
17 |
18 | func SetStorage(s Storage) {
19 | storage = s
20 | }
21 |
22 | func SetValue(name, value string) error {
23 | if storage == nil {
24 | return errors.New("credentials: storage not initialized")
25 | }
26 | if err := storage.SetValue(name, value); err != nil {
27 | return err
28 | }
29 | AddSecret(value)
30 | return nil
31 | }
32 |
33 | func GetValue(name string) (value string, ok bool) {
34 | value, ok = getValue(name)
35 | AddSecret(value)
36 | return
37 | }
38 |
39 | func getValue(name string) (string, bool) {
40 | if storage != nil {
41 | if value, ok := storage.GetValue(name); ok {
42 | return value, true
43 | }
44 | }
45 |
46 | if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok {
47 | if value, _ := os.ReadFile(filepath.Join(dir, name)); value != nil {
48 | return strings.TrimSpace(string(value)), true
49 | }
50 | }
51 |
52 | return os.LookupEnv(name)
53 | }
54 |
55 | // ReplaceVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin}
56 | func ReplaceVars(data []byte) []byte {
57 | re := regexp.MustCompile(`\${([^}{]+)}`)
58 | return re.ReplaceAllFunc(data, func(match []byte) []byte {
59 | key := string(match[2 : len(match)-1])
60 |
61 | var def string
62 | var defok bool
63 |
64 | if i := strings.IndexByte(key, ':'); i > 0 {
65 | key, def = key[:i], key[i+1:]
66 | defok = true
67 | }
68 |
69 | if value, ok := GetValue(key); ok {
70 | return []byte(value)
71 | }
72 |
73 | if defok {
74 | return []byte(def)
75 | }
76 |
77 | return match
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/aac/rtp_test.go:
--------------------------------------------------------------------------------
1 | package aac
2 |
3 | import (
4 | "encoding/hex"
5 | "testing"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/pion/rtp"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestBuggy_RTSP_AAC(t *testing.T) {
13 | // https: //github.com/AlexxIT/go2rtc/issues/1328
14 | payload, _ := hex.DecodeString("fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0")
15 | packet := &rtp.Packet{
16 | Header: rtp.Header{
17 | Version: 2,
18 | Marker: true,
19 | SequenceNumber: 36944,
20 | Timestamp: 4217191328,
21 | SSRC: 12892774,
22 | },
23 | Payload: payload,
24 | }
25 |
26 | var size int
27 |
28 | RTPDepay(func(packet *core.Packet) {
29 | size = len(packet.Payload)
30 | })(packet)
31 |
32 | require.Equal(t, len(payload), size+ADTSHeaderSize)
33 | }
34 |
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | ## Versions
2 |
3 | - `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support hardware transcoding for Intel iGPU and Raspberry
4 | - `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU
5 | - `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support hardware transcoding for Rockchip RK35xx
6 | - `alexxit/go2rtc:master` - latest unstable version based on `alpine`
7 | - `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`)
8 | - `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`)
9 |
10 | ## Docker compose
11 |
12 | ```yaml
13 | services:
14 | go2rtc:
15 | image: alexxit/go2rtc
16 | network_mode: host # important for WebRTC, HomeKit, UDP cameras
17 | privileged: true # only for FFmpeg hardware transcoding
18 | restart: unless-stopped # autorestart on fail or config change from WebUI
19 | environment:
20 | - TZ=Atlantic/Bermuda # timezone in logs
21 | volumes:
22 | - "~/go2rtc:/config" # folder for go2rtc.yaml file (edit from WebUI)
23 | ```
24 |
25 | ## Basic Deployment
26 |
27 | ```bash
28 | docker run -d \
29 | --name go2rtc \
30 | --network host \
31 | --privileged \
32 | --restart unless-stopped \
33 | -e TZ=Atlantic/Bermuda \
34 | -v ~/go2rtc:/config \
35 | alexxit/go2rtc
36 | ```
37 |
38 | ## Deployment with GPU Acceleration
39 |
40 | ```bash
41 | docker run -d \
42 | --name go2rtc \
43 | --network host \
44 | --privileged \
45 | --restart unless-stopped \
46 | -e TZ=Atlantic/Bermuda \
47 | --gpus all \
48 | -v ~/go2rtc:/config \
49 | alexxit/go2rtc:latest-hardware
50 | ```
51 |
--------------------------------------------------------------------------------
/pkg/aac/producer.go:
--------------------------------------------------------------------------------
1 | package aac
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "io"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/pion/rtp"
10 | )
11 |
12 | type Producer struct {
13 | core.Connection
14 | rd *bufio.Reader
15 | }
16 |
17 | func Open(r io.Reader) (*Producer, error) {
18 | rd := bufio.NewReader(r)
19 |
20 | b, err := rd.Peek(ADTSHeaderSize)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | codec := ADTSToCodec(b)
26 | if codec == nil {
27 | return nil, errors.New("adts: wrong header")
28 | }
29 | codec.PayloadType = core.PayloadTypeRAW
30 |
31 | medias := []*core.Media{
32 | {
33 | Kind: core.KindAudio,
34 | Direction: core.DirectionRecvonly,
35 | Codecs: []*core.Codec{codec},
36 | },
37 | }
38 | return &Producer{
39 | Connection: core.Connection{
40 | ID: core.NewID(),
41 | FormatName: "adts",
42 | Medias: medias,
43 | Transport: r,
44 | },
45 | rd: rd,
46 | }, nil
47 | }
48 |
49 | func (c *Producer) Start() error {
50 | for {
51 | // read ADTS header
52 | adts := make([]byte, ADTSHeaderSize)
53 | if _, err := io.ReadFull(c.rd, adts); err != nil {
54 | return err
55 | }
56 |
57 | auSize := ReadADTSSize(adts) - ADTSHeaderSize
58 |
59 | if HasCRC(adts) {
60 | // skip CRC after header
61 | if _, err := c.rd.Discard(2); err != nil {
62 | return err
63 | }
64 | auSize -= 2
65 | }
66 |
67 | // read AAC payload after header
68 | payload := make([]byte, auSize)
69 | if _, err := io.ReadFull(c.rd, payload); err != nil {
70 | return err
71 | }
72 |
73 | c.Recv += int(auSize)
74 |
75 | if len(c.Receivers) == 0 {
76 | continue
77 | }
78 |
79 | pkt := &rtp.Packet{
80 | Header: rtp.Header{Timestamp: core.Now90000()},
81 | Payload: payload,
82 | }
83 | c.Receivers[0].WriteRTP(pkt)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/tapo/backchannel.go:
--------------------------------------------------------------------------------
1 | package tapo
2 |
3 | import (
4 | "bytes"
5 | "strconv"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/AlexxIT/go2rtc/pkg/mpegts"
9 | "github.com/pion/rtp"
10 | )
11 |
12 | func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
13 | if c.sender == nil {
14 | if err := c.SetupBackchannel(); err != nil {
15 | return err
16 | }
17 |
18 | muxer := mpegts.NewMuxer()
19 | pid := muxer.AddTrack(mpegts.StreamTypePCMATapo)
20 | if err := c.WriteBackchannel(muxer.GetHeader()); err != nil {
21 | return err
22 | }
23 |
24 | c.sender = core.NewSender(media, track.Codec)
25 | c.sender.Handler = func(packet *rtp.Packet) {
26 | b := muxer.GetPayload(pid, packet.Timestamp, packet.Payload)
27 | _ = c.WriteBackchannel(b)
28 | }
29 | }
30 |
31 | c.sender.HandleRTP(track)
32 | return nil
33 | }
34 |
35 | func (c *Client) SetupBackchannel() (err error) {
36 | // if conn1 is not used - we will use it for backchannel
37 | // or we need to start another conn for session2
38 | if c.session1 != "" {
39 | if c.conn2, err = c.newConn(); err != nil {
40 | return
41 | }
42 | } else {
43 | c.conn2 = c.conn1
44 | }
45 |
46 | c.session2, err = c.Request(c.conn2, []byte(`{"params":{"talk":{"mode":"aec"},"method":"get"},"seq":3,"type":"request"}`))
47 | return
48 | }
49 |
50 | func (c *Client) WriteBackchannel(body []byte) (err error) {
51 | // TODO: fixme (size)
52 | buf := bytes.NewBuffer(nil)
53 | buf.WriteString("----client-stream-boundary--\r\n")
54 | buf.WriteString("Content-Type: audio/mp2t\r\n")
55 | buf.WriteString("X-If-Encrypt: 0\r\n")
56 | buf.WriteString("X-Session-Id: " + c.session2 + "\r\n")
57 | buf.WriteString("Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n")
58 | buf.Write(body)
59 |
60 | _, err = buf.WriteTo(c.conn2)
61 | return
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/bubble/producer.go:
--------------------------------------------------------------------------------
1 | package bubble
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/AlexxIT/go2rtc/pkg/core"
7 | )
8 |
9 | func (c *Client) GetMedias() []*core.Media {
10 | if c.medias == nil {
11 | c.medias = []*core.Media{
12 | {
13 | Kind: core.KindVideo,
14 | Direction: core.DirectionRecvonly,
15 | Codecs: []*core.Codec{
16 | {Name: c.videoCodec, ClockRate: 90000, PayloadType: core.PayloadTypeRAW},
17 | },
18 | },
19 | {
20 | Kind: core.KindAudio,
21 | Direction: core.DirectionRecvonly,
22 | Codecs: []*core.Codec{
23 | {Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
24 | },
25 | },
26 | }
27 | }
28 |
29 | return c.medias
30 | }
31 |
32 | func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
33 | for _, track := range c.receivers {
34 | if track.Codec == codec {
35 | return track, nil
36 | }
37 | }
38 |
39 | track := core.NewReceiver(media, codec)
40 |
41 | switch media.Kind {
42 | case core.KindVideo:
43 | c.videoTrack = track
44 | case core.KindAudio:
45 | c.audioTrack = track
46 | }
47 |
48 | c.receivers = append(c.receivers, track)
49 |
50 | return track, nil
51 | }
52 |
53 | func (c *Client) Start() error {
54 | if err := c.Play(); err != nil {
55 | return err
56 | }
57 | return c.Handle()
58 | }
59 |
60 | func (c *Client) Stop() error {
61 | for _, receiver := range c.receivers {
62 | receiver.Close()
63 | }
64 | return c.Close()
65 | }
66 |
67 | func (c *Client) MarshalJSON() ([]byte, error) {
68 | info := &core.Connection{
69 | ID: core.ID(c),
70 | FormatName: "bubble",
71 | Protocol: "http",
72 | Medias: c.medias,
73 | Recv: c.recv,
74 | Receivers: c.receivers,
75 | }
76 | if c.conn != nil {
77 | info.RemoteAddr = c.conn.RemoteAddr().String()
78 | }
79 | return json.Marshal(info)
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/core/node.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/pion/rtp"
7 | )
8 |
9 | //type Packet struct {
10 | // Payload []byte
11 | // Timestamp uint32 // PTS if DTS == 0 else DTS
12 | // Composition uint32 // CTS = PTS-DTS (for support B-frames)
13 | // Sequence uint16
14 | //}
15 |
16 | type Packet = rtp.Packet
17 |
18 | // HandlerFunc - process input packets (just like http.HandlerFunc)
19 | type HandlerFunc func(packet *Packet)
20 |
21 | // Filter - a decorator for any HandlerFunc
22 | type Filter func(handler HandlerFunc) HandlerFunc
23 |
24 | // Node - Receiver or Sender or Filter (transform)
25 | type Node struct {
26 | Codec *Codec
27 | Input HandlerFunc
28 | Output HandlerFunc
29 |
30 | id uint32
31 | childs []*Node
32 | parent *Node
33 |
34 | mu sync.Mutex
35 | }
36 |
37 | func (n *Node) WithParent(parent *Node) *Node {
38 | parent.AppendChild(n)
39 | return n
40 | }
41 |
42 | func (n *Node) AppendChild(child *Node) {
43 | n.mu.Lock()
44 | n.childs = append(n.childs, child)
45 | n.mu.Unlock()
46 |
47 | child.parent = n
48 | }
49 |
50 | func (n *Node) RemoveChild(child *Node) {
51 | n.mu.Lock()
52 | for i, ch := range n.childs {
53 | if ch == child {
54 | n.childs = append(n.childs[:i], n.childs[i+1:]...)
55 | break
56 | }
57 | }
58 | n.mu.Unlock()
59 | }
60 |
61 | func (n *Node) Close() {
62 | if parent := n.parent; parent != nil {
63 | parent.RemoveChild(n)
64 |
65 | if len(parent.childs) == 0 {
66 | parent.Close()
67 | }
68 | } else {
69 | for _, childs := range n.childs {
70 | childs.Close()
71 | }
72 | }
73 | }
74 |
75 | func MoveNode(dst, src *Node) {
76 | src.mu.Lock()
77 | childs := src.childs
78 | src.childs = nil
79 | src.mu.Unlock()
80 |
81 | dst.mu.Lock()
82 | dst.childs = childs
83 | dst.mu.Unlock()
84 |
85 | for _, child := range childs {
86 | child.parent = dst
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/internal/ffmpeg/hardware/hardware_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package hardware
4 |
5 | import "github.com/AlexxIT/go2rtc/internal/api"
6 |
7 | const ProbeDXVA2H264 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -"
8 | const ProbeDXVA2H265 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -"
9 | const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -"
10 | const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
11 | const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
12 |
13 | func ProbeAll(bin string) []*api.Source {
14 | return []*api.Source{
15 | {
16 | Name: runToString(bin, ProbeDXVA2H264),
17 | URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
18 | },
19 | {
20 | Name: runToString(bin, ProbeDXVA2H265),
21 | URL: "ffmpeg:...#video=h265#hardware=" + EngineDXVA2,
22 | },
23 | {
24 | Name: runToString(bin, ProbeDXVA2JPEG),
25 | URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineDXVA2,
26 | },
27 | {
28 | Name: runToString(bin, ProbeCUDAH264),
29 | URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
30 | },
31 | {
32 | Name: runToString(bin, ProbeCUDAH265),
33 | URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
34 | },
35 | }
36 | }
37 |
38 | func ProbeHardware(bin, name string) string {
39 | switch name {
40 | case "h264":
41 | if run(bin, ProbeCUDAH264) {
42 | return EngineCUDA
43 | }
44 | if run(bin, ProbeDXVA2H264) {
45 | return EngineDXVA2
46 | }
47 |
48 | case "h265":
49 | if run(bin, ProbeCUDAH265) {
50 | return EngineCUDA
51 | }
52 | if run(bin, ProbeDXVA2H265) {
53 | return EngineDXVA2
54 | }
55 |
56 | case "mjpeg":
57 | if run(bin, ProbeDXVA2JPEG) {
58 | return EngineDXVA2
59 | }
60 | }
61 |
62 | return EngineSoftware
63 | }
64 |
--------------------------------------------------------------------------------
/internal/v4l2/README.md:
--------------------------------------------------------------------------------
1 | # V4L2
2 |
3 | What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux):
4 |
5 | - V4L2 (Video for Linux API version 2) works only in Linux
6 | - supports USB cameras and other similar devices
7 | - one device can only be connected to one software simultaneously
8 | - cameras support a fixed list of formats, resolutions and frame rates
9 | - basic cameras supports only RAW (non-compressed) pixel formats
10 | - regular cameras supports MJPEG format (series of JPEG frames)
11 | - advances cameras support H264 format (MSE/MP4, WebRTC compatible)
12 | - using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage
13 | - transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage
14 | - H265 (HEVC) format is also supported (if the camera supports it)
15 |
16 | Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%.
17 |
18 | Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**.
19 |
20 | ## RAW format
21 |
22 | Example:
23 |
24 | ```yaml
25 | streams:
26 | camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10
27 | ```
28 |
29 | Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured.
30 |
31 | ```
32 | ffplay http://localhost:1984/api/stream.mjpeg?src=camera1
33 | ```
34 |
35 | **Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth.
36 |
37 | ```
38 | ffplay http://localhost:1984/api/stream.y4m?src=camera1
39 | ```
40 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/AlexxIT/go2rtc
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/asticode/go-astits v1.14.0
7 | github.com/eclipse/paho.mqtt.golang v1.5.1
8 | github.com/expr-lang/expr v1.17.6
9 | github.com/google/uuid v1.6.0
10 | github.com/gorilla/websocket v1.5.3
11 | github.com/mattn/go-isatty v0.0.20
12 | github.com/miekg/dns v1.1.69
13 | github.com/pion/ice/v4 v4.1.0
14 | github.com/pion/interceptor v0.1.42
15 | github.com/pion/rtcp v1.2.16
16 | github.com/pion/rtp v1.8.26
17 | github.com/pion/sdp/v3 v3.0.16
18 | github.com/pion/srtp/v3 v3.0.9
19 | github.com/pion/stun/v3 v3.0.2
20 | github.com/pion/webrtc/v4 v4.1.8
21 | github.com/rs/zerolog v1.34.0
22 | github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
23 | github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
24 | github.com/stretchr/testify v1.11.1
25 | github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
26 | golang.org/x/crypto v0.46.0
27 | golang.org/x/net v0.48.0
28 | gopkg.in/yaml.v3 v3.0.1
29 | )
30 |
31 | require (
32 | github.com/asticode/go-astikit v0.57.1 // indirect
33 | github.com/davecgh/go-spew v1.1.1 // indirect
34 | github.com/kr/pretty v0.3.1 // indirect
35 | github.com/mattn/go-colorable v0.1.14 // indirect
36 | github.com/pion/datachannel v1.5.10 // indirect
37 | github.com/pion/dtls/v3 v3.0.9 // indirect
38 | github.com/pion/logging v0.2.4 // indirect
39 | github.com/pion/mdns/v2 v2.1.0 // indirect
40 | github.com/pion/randutil v0.1.0 // indirect
41 | github.com/pion/sctp v1.8.41 // indirect
42 | github.com/pion/transport/v3 v3.1.1 // indirect
43 | github.com/pion/turn/v4 v4.1.3 // indirect
44 | github.com/pmezard/go-difflib v1.0.0 // indirect
45 | github.com/wlynxg/anet v0.0.5 // indirect
46 | golang.org/x/mod v0.31.0 // indirect
47 | golang.org/x/sync v0.19.0 // indirect
48 | golang.org/x/sys v0.39.0 // indirect
49 | golang.org/x/tools v0.40.0 // indirect
50 | )
51 |
--------------------------------------------------------------------------------
/pkg/dvrip/backchannel.go:
--------------------------------------------------------------------------------
1 | package dvrip
2 |
3 | import (
4 | "encoding/binary"
5 | "time"
6 |
7 | "github.com/AlexxIT/go2rtc/pkg/core"
8 | "github.com/pion/rtp"
9 | )
10 |
11 | type Backchannel struct {
12 | core.Connection
13 | client *Client
14 | }
15 |
16 | func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
17 | return nil, core.ErrCantGetTrack
18 | }
19 |
20 | func (c *Backchannel) Start() error {
21 | if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
22 | return err
23 | }
24 |
25 | b := make([]byte, 4096)
26 | for {
27 | if _, err := c.client.rd.Read(b); err != nil {
28 | return err
29 | }
30 | }
31 | }
32 |
33 | func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
34 | if err := c.client.Talk(); err != nil {
35 | return err
36 | }
37 |
38 | const PacketSize = 320
39 |
40 | buf := make([]byte, 8+PacketSize)
41 | binary.BigEndian.PutUint32(buf, 0x1FA)
42 |
43 | switch track.Codec.Name {
44 | case core.CodecPCMU:
45 | buf[4] = 10
46 | case core.CodecPCMA:
47 | buf[4] = 14
48 | }
49 |
50 | //for i, rate := range sampleRates {
51 | // if rate == track.Codec.ClockRate {
52 | // buf[5] = byte(i) + 1
53 | // break
54 | // }
55 | //}
56 | buf[5] = 2 // ClockRate=8000
57 |
58 | binary.LittleEndian.PutUint16(buf[6:], PacketSize)
59 |
60 | var payload []byte
61 |
62 | sender := core.NewSender(media, track.Codec)
63 | sender.Handler = func(packet *rtp.Packet) {
64 | payload = append(payload, packet.Payload...)
65 |
66 | for len(payload) >= PacketSize {
67 | buf = append(buf[:8], payload[:PacketSize]...)
68 | if n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {
69 | c.Send += n
70 | }
71 |
72 | payload = payload[PacketSize:]
73 | }
74 | }
75 |
76 | sender.HandleRTP(track)
77 | c.Senders = append(c.Senders, sender)
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/internal/alsa/alsa_linux.go:
--------------------------------------------------------------------------------
1 | //go:build linux && (386 || amd64 || arm || arm64 || mipsle)
2 |
3 | package alsa
4 |
5 | import (
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/AlexxIT/go2rtc/internal/api"
13 | "github.com/AlexxIT/go2rtc/internal/streams"
14 | "github.com/AlexxIT/go2rtc/pkg/alsa"
15 | "github.com/AlexxIT/go2rtc/pkg/alsa/device"
16 | )
17 |
18 | func Init() {
19 | streams.HandleFunc("alsa", alsa.Open)
20 |
21 | api.HandleFunc("api/alsa", apiAlsa)
22 | }
23 |
24 | func apiAlsa(w http.ResponseWriter, r *http.Request) {
25 | files, err := os.ReadDir("/dev/snd/")
26 | if err != nil {
27 | return
28 | }
29 |
30 | var sources []*api.Source
31 |
32 | for _, file := range files {
33 | if !strings.HasPrefix(file.Name(), "pcm") {
34 | continue
35 | }
36 |
37 | path := "/dev/snd/" + file.Name()
38 |
39 | dev, err := device.Open(path)
40 | if err != nil {
41 | continue
42 | }
43 |
44 | info, err := dev.Info()
45 | if err == nil {
46 | formats := formatsToString(dev.ListFormats())
47 | r1, r2 := dev.RangeRates()
48 | c1, c2 := dev.RangeChannels()
49 | source := &api.Source{
50 | Name: info.ID,
51 | Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2),
52 | URL: "alsa:device?audio=" + path,
53 | }
54 | if !strings.Contains(source.Name, info.Name) {
55 | source.Name += ", " + info.Name
56 | }
57 | sources = append(sources, source)
58 | }
59 |
60 | _ = dev.Close()
61 | }
62 |
63 | api.ResponseSources(w, sources)
64 | }
65 |
66 | func formatsToString(formats []byte) string {
67 | var s string
68 | for i, format := range formats {
69 | if i > 0 {
70 | s += " "
71 | }
72 | switch format {
73 | case 2:
74 | s += "s16le"
75 | case 10:
76 | s += "s32le"
77 | default:
78 | s += strconv.Itoa(int(format))
79 | }
80 |
81 | }
82 | return s
83 | }
84 |
--------------------------------------------------------------------------------
/internal/ffmpeg/virtual/virtual.go:
--------------------------------------------------------------------------------
1 | package virtual
2 |
3 | import (
4 | "net/url"
5 | )
6 |
7 | func GetInput(src string) string {
8 | query, err := url.ParseQuery(src)
9 | if err != nil {
10 | return ""
11 | }
12 |
13 | input := "-re"
14 |
15 | for _, video := range query["video"] {
16 | // https://ffmpeg.org/ffmpeg-filters.html
17 | sep := "=" // first separator
18 |
19 | if video == "" {
20 | video = "testsrc=decimals=2" // default video
21 | sep = ":"
22 | }
23 |
24 | input += " -f lavfi -i " + video
25 |
26 | // set defaults (using Add instead of Set)
27 | query.Add("size", "1920x1080")
28 |
29 | for key, values := range query {
30 | value := values[0]
31 |
32 | // https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
33 | switch key {
34 | case "color", "rate", "duration", "sar", "decimals":
35 | case "size":
36 | switch value {
37 | case "720":
38 | value = "1280x720" // crf=1 -> 12 Mbps
39 | case "1080":
40 | value = "1920x1080" // crf=1 -> 25 Mbps
41 | case "2K":
42 | value = "2560x1440" // crf=1 -> 43 Mbps
43 | case "4K":
44 | value = "3840x2160" // crf=1 -> 103 Mbps
45 | case "8K":
46 | value = "7680x4230" // https://reolink.com/blog/8k-resolution/
47 | }
48 | default:
49 | continue
50 | }
51 |
52 | input += sep + key + "=" + value
53 | sep = ":" // next separator
54 | }
55 |
56 | if s := query.Get("format"); s != "" {
57 | input += ",format=" + s
58 | }
59 | }
60 |
61 | return input
62 | }
63 |
64 | func GetInputTTS(src string) string {
65 | query, err := url.ParseQuery(src)
66 | if err != nil {
67 | return ""
68 | }
69 |
70 | input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'`
71 |
72 | // ffmpeg -f lavfi -i flite=list_voices=1
73 | // awb, kal, kal16, rms, slt
74 | if voice := query.Get("voice"); voice != "" {
75 | input += ":voice" + voice
76 | }
77 |
78 | return input + `"`
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/bits/writer.go:
--------------------------------------------------------------------------------
1 | package bits
2 |
3 | type Writer struct {
4 | buf []byte // total buf
5 | byte *byte // pointer to current byte
6 | bits byte // bits left in byte
7 | }
8 |
9 | func NewWriter(buf []byte) *Writer {
10 | return &Writer{buf: buf}
11 | }
12 |
13 | //goland:noinspection GoStandardMethods
14 | func (w *Writer) WriteByte(b byte) {
15 | if w.bits != 0 {
16 | w.WriteBits8(b, 8)
17 | }
18 |
19 | w.buf = append(w.buf, b)
20 | }
21 |
22 | func (w *Writer) WriteBit(b byte) {
23 | if w.bits == 0 {
24 | w.buf = append(w.buf, 0)
25 | w.byte = &w.buf[len(w.buf)-1]
26 | w.bits = 7
27 | } else {
28 | w.bits--
29 | }
30 |
31 | *w.byte |= (b & 1) << w.bits
32 | }
33 |
34 | func (w *Writer) WriteBits(v uint32, n byte) {
35 | for i := n - 1; i != 255; i-- {
36 | w.WriteBit(byte(v>>i) & 0b1)
37 | }
38 | }
39 |
40 | func (w *Writer) WriteBits16(v uint16, n byte) {
41 | for i := n - 1; i != 255; i-- {
42 | w.WriteBit(byte(v>>i) & 0b1)
43 | }
44 | }
45 |
46 | func (w *Writer) WriteBits8(v, n byte) {
47 | for i := n - 1; i != 255; i-- {
48 | w.WriteBit((v >> i) & 0b1)
49 | }
50 | }
51 |
52 | func (w *Writer) WriteAllBits(bit, n byte) {
53 | for i := byte(0); i < n; i++ {
54 | w.WriteBit(bit)
55 | }
56 | }
57 |
58 | func (w *Writer) WriteBool(b bool) {
59 | if b {
60 | w.WriteBit(1)
61 | } else {
62 | w.WriteBit(0)
63 | }
64 | }
65 |
66 | func (w *Writer) WriteUint16(v uint16) {
67 | if w.bits != 0 {
68 | w.WriteBits16(v, 16)
69 | }
70 |
71 | w.buf = append(w.buf, byte(v>>8), byte(v))
72 | }
73 |
74 | func (w *Writer) WriteBytes(bytes ...byte) {
75 | if w.bits != 0 {
76 | for _, b := range bytes {
77 | w.WriteByte(b)
78 | }
79 | }
80 |
81 | w.buf = append(w.buf, bytes...)
82 | }
83 |
84 | func (w *Writer) Bytes() []byte {
85 | return w.buf
86 | }
87 |
88 | func (w *Writer) Len() int {
89 | return len(w.buf)
90 | }
91 |
92 | func (w *Writer) Reset() {
93 | w.buf = w.buf[:0]
94 | w.bits = 0
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/image/producer.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 |
8 | "github.com/AlexxIT/go2rtc/pkg/core"
9 | "github.com/AlexxIT/go2rtc/pkg/tcp"
10 | "github.com/pion/rtp"
11 | )
12 |
13 | type Producer struct {
14 | core.Connection
15 |
16 | closed bool
17 | res *http.Response
18 | }
19 |
20 | func Open(res *http.Response) (*Producer, error) {
21 | return &Producer{
22 | Connection: core.Connection{
23 | ID: core.NewID(),
24 | FormatName: "image",
25 | Protocol: "http",
26 | RemoteAddr: res.Request.URL.Host,
27 | Transport: res.Body,
28 | Medias: []*core.Media{
29 | {
30 | Kind: core.KindVideo,
31 | Direction: core.DirectionRecvonly,
32 | Codecs: []*core.Codec{
33 | {
34 | Name: core.CodecJPEG,
35 | ClockRate: 90000,
36 | PayloadType: core.PayloadTypeRAW,
37 | },
38 | },
39 | },
40 | },
41 | },
42 | res: res,
43 | }, nil
44 | }
45 |
46 | func (c *Producer) Start() error {
47 | body, err := io.ReadAll(c.res.Body)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | pkt := &rtp.Packet{
53 | Header: rtp.Header{Timestamp: core.Now90000()},
54 | Payload: body,
55 | }
56 | c.Receivers[0].WriteRTP(pkt)
57 |
58 | c.Recv += len(body)
59 |
60 | req := c.res.Request
61 |
62 | for !c.closed {
63 | res, err := tcp.Do(req)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | if res.StatusCode != http.StatusOK {
69 | return errors.New("wrong status: " + res.Status)
70 | }
71 |
72 | body, err = io.ReadAll(res.Body)
73 | if err != nil {
74 | return err
75 | }
76 |
77 | c.Recv += len(body)
78 |
79 | pkt = &rtp.Packet{
80 | Header: rtp.Header{Timestamp: core.Now90000()},
81 | Payload: body,
82 | }
83 | c.Receivers[0].WriteRTP(pkt)
84 | }
85 |
86 | return nil
87 | }
88 |
89 | func (c *Producer) Stop() error {
90 | c.closed = true
91 | return c.Connection.Stop()
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/xnet/tls/tls.go:
--------------------------------------------------------------------------------
1 | package tls
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "crypto/x509/pkix"
9 | "encoding/pem"
10 | "math/big"
11 | "net"
12 | "time"
13 | )
14 |
15 | func CreateCertificate() (*tls.Certificate, error) {
16 | // 1. Generate an RSA private key
17 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
18 | if err != nil {
19 | return nil, err
20 | }
21 |
22 | // 2. Define the certificate template
23 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
24 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | template := x509.Certificate{
30 | SerialNumber: serialNumber,
31 | Subject: pkix.Name{
32 | Organization: []string{"home"},
33 | CommonName: "localhost",
34 | },
35 | NotBefore: time.Now(),
36 | NotAfter: time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year
37 |
38 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
39 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
40 | BasicConstraintsValid: true,
41 |
42 | // Add localhost as a valid IP and DNS name
43 | IPAddresses: []net.IP{[]byte{127, 0, 0, 1}},
44 | DNSNames: []string{"localhost"},
45 | }
46 |
47 | // 3. Create a self-signed certificate
48 | // The parent is the template itself, and we use the generated public and private keys.
49 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | derBytes = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
55 | keyBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
56 |
57 | cert, err := tls.X509KeyPair(derBytes, keyBytes)
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | return &cert, nil
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/webrtc/track.go:
--------------------------------------------------------------------------------
1 | package webrtc
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/pion/rtp"
7 | "github.com/pion/webrtc/v4"
8 | )
9 |
10 | type Track struct {
11 | kind string
12 | id string
13 | streamID string
14 | sequence uint16
15 | ssrc uint32
16 | writer webrtc.TrackLocalWriter
17 | mu sync.Mutex
18 | }
19 |
20 | func NewTrack(kind string) *Track {
21 | return &Track{
22 | kind: kind,
23 | id: "go2rtc-" + kind,
24 | streamID: "go2rtc",
25 | }
26 | }
27 |
28 | func (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {
29 | t.mu.Lock()
30 | t.ssrc = uint32(context.SSRC())
31 | t.writer = context.WriteStream()
32 | t.mu.Unlock()
33 |
34 | for _, parameters := range context.CodecParameters() {
35 | // return first parameters
36 | return parameters, nil
37 | }
38 |
39 | return webrtc.RTPCodecParameters{}, nil
40 | }
41 |
42 | func (t *Track) Unbind(context webrtc.TrackLocalContext) error {
43 | t.mu.Lock()
44 | t.writer = nil
45 | t.mu.Unlock()
46 | return nil
47 | }
48 |
49 | func (t *Track) ID() string {
50 | return t.id
51 | }
52 |
53 | func (t *Track) RID() string {
54 | return "" // don't know what it is
55 | }
56 |
57 | func (t *Track) StreamID() string {
58 | return t.streamID
59 | }
60 |
61 | func (t *Track) Kind() webrtc.RTPCodecType {
62 | return webrtc.NewRTPCodecType(t.kind)
63 | }
64 |
65 | func (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) (err error) {
66 | // using mutex because Unbind https://github.com/AlexxIT/go2rtc/issues/994
67 | t.mu.Lock()
68 |
69 | // in case when we start WriteRTP before Track.Bind
70 | if t.writer != nil {
71 | // important to have internal counter if input packets from different sources
72 | t.sequence++
73 |
74 | header := packet.Header
75 | header.SSRC = t.ssrc
76 | header.PayloadType = payloadType
77 | header.SequenceNumber = t.sequence
78 | _, err = t.writer.WriteRTP(&header, packet.Payload)
79 | }
80 |
81 | t.mu.Unlock()
82 | return
83 | }
84 |
--------------------------------------------------------------------------------
/internal/mp4/ws.go:
--------------------------------------------------------------------------------
1 | package mp4
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/AlexxIT/go2rtc/internal/api"
7 | "github.com/AlexxIT/go2rtc/internal/api/ws"
8 | "github.com/AlexxIT/go2rtc/internal/streams"
9 | "github.com/AlexxIT/go2rtc/pkg/core"
10 | "github.com/AlexxIT/go2rtc/pkg/mp4"
11 | )
12 |
13 | func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
14 | stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
15 | if stream == nil {
16 | return errors.New(api.StreamNotFound)
17 | }
18 |
19 | var medias []*core.Media
20 | if codecs := msg.String(); codecs != "" {
21 | log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
22 | medias = mp4.ParseCodecs(codecs, true)
23 | }
24 |
25 | cons := mp4.NewConsumer(medias)
26 | cons.FormatName = "mse/fmp4"
27 | cons.WithRequest(tr.Request)
28 |
29 | if err := stream.AddConsumer(cons); err != nil {
30 | log.Debug().Err(err).Msg("[mp4] add consumer")
31 | return err
32 | }
33 |
34 | tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
35 |
36 | go cons.WriteTo(tr.Writer())
37 |
38 | tr.OnClose(func() {
39 | stream.RemoveConsumer(cons)
40 | })
41 |
42 | return nil
43 | }
44 |
45 | func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
46 | stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
47 | if stream == nil {
48 | return errors.New(api.StreamNotFound)
49 | }
50 |
51 | var medias []*core.Media
52 | if codecs := msg.String(); codecs != "" {
53 | log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
54 | medias = mp4.ParseCodecs(codecs, false)
55 | }
56 |
57 | cons := mp4.NewKeyframe(medias)
58 | cons.WithRequest(tr.Request)
59 |
60 | if err := stream.AddConsumer(cons); err != nil {
61 | log.Error().Err(err).Caller().Send()
62 | return err
63 | }
64 |
65 | tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
66 |
67 | go cons.WriteTo(tr.Writer())
68 |
69 | tr.OnClose(func() {
70 | stream.RemoveConsumer(cons)
71 | })
72 |
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/internal/ngrok/ngrok.go:
--------------------------------------------------------------------------------
1 | package ngrok
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "strings"
7 |
8 | "github.com/AlexxIT/go2rtc/internal/app"
9 | "github.com/AlexxIT/go2rtc/internal/webrtc"
10 | "github.com/AlexxIT/go2rtc/pkg/ngrok"
11 | "github.com/rs/zerolog"
12 | )
13 |
14 | func Init() {
15 | var cfg struct {
16 | Mod struct {
17 | Cmd string `yaml:"command"`
18 | } `yaml:"ngrok"`
19 | }
20 |
21 | app.LoadConfig(&cfg)
22 |
23 | if cfg.Mod.Cmd == "" {
24 | return
25 | }
26 |
27 | log = app.GetLogger("ngrok")
28 |
29 | ngr, err := ngrok.NewNgrok(cfg.Mod.Cmd)
30 | if err != nil {
31 | log.Error().Err(err).Msg("[ngrok] start")
32 | }
33 |
34 | ngr.Listen(func(msg any) {
35 | if msg := msg.(*ngrok.Message); msg != nil {
36 | if strings.HasPrefix(msg.Line, "ERROR:") {
37 | log.Warn().Msg("[ngrok] " + msg.Line)
38 | } else {
39 | log.Debug().Msg("[ngrok] " + msg.Line)
40 | }
41 |
42 | // Addr: "//localhost:8555", URL: "tcp://1.tcp.eu.ngrok.io:12345"
43 | if strings.HasPrefix(msg.Addr, "//localhost:") && strings.HasPrefix(msg.URL, "tcp://") {
44 | // don't know if really necessary use IP
45 | address, err := ConvertHostToIP(msg.URL[6:])
46 | if err != nil {
47 | log.Warn().Err(err).Msg("[ngrok] add candidate")
48 | return
49 | }
50 |
51 | log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
52 |
53 | webrtc.AddCandidate("tcp", address)
54 | }
55 | }
56 | })
57 |
58 | go func() {
59 | if err = ngr.Serve(); err != nil {
60 | log.Error().Err(err).Msg("[ngrok] run")
61 | }
62 | }()
63 |
64 | }
65 |
66 | var log zerolog.Logger
67 |
68 | func ConvertHostToIP(address string) (string, error) {
69 | host, port, err := net.SplitHostPort(address)
70 | if err != nil {
71 | return "", err
72 | }
73 |
74 | ip, err := net.LookupIP(host)
75 | if err != nil {
76 | return "", err
77 | }
78 |
79 | if len(ip) == 0 {
80 | return "", fmt.Errorf("can't resolve: %s", host)
81 | }
82 |
83 | return ip[0].String() + ":" + port, nil
84 | }
85 |
--------------------------------------------------------------------------------