├── 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://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](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 | --------------------------------------------------------------------------------