From 3a50b3678d132f301fe53de1be7ba8054665c4e4 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Mon, 23 Dec 2024 23:43:39 -0800 Subject: [PATCH 01/34] Extend onvif server to support Unifi Protect --- internal/onvif/init.go | 13 ++++++ pkg/onvif/server.go | 100 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index 014c5e18a..b8b4fca62 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -70,6 +70,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { // important for Hass: Media section res = onvif.GetCapabilitiesResponse(r.Host) + case onvif.ActionGetServices: + // important for Unifi: Media section + res = onvif.GetServicesResponse(r.Host) + case onvif.ActionGetSystemDateAndTime: // important for Hass res = onvif.GetSystemDateAndTimeResponse() @@ -95,8 +99,13 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { case onvif.ActionGetProfiles: // important for Hass: H264 codec, width, height + // important for Unifi: framerate, bitrate, quality res = onvif.GetProfilesResponse(streams.GetAll()) + case onvif.ActionGetVideoSources: + // important for Unifi: framerate, resolution + res = onvif.GetVideoSourcesResponse(streams.GetAll()) + case onvif.ActionGetStreamUri: host, _, err := net.SplitHostPort(r.Host) if err != nil { @@ -107,6 +116,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") res = onvif.GetStreamUriResponse(uri) + case onvif.ActionGetSnapshotUri: + uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken") + res = onvif.GetSnapshotUriResponse(uri) + default: http.Error(w, "unsupported action", http.StatusBadRequest) log.Debug().Msgf("[onvif] unsupported request:\n%s", b) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index f8f2883c3..df53dfab9 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -16,6 +16,7 @@ const ( ActionGetServiceCapabilities = "GetServiceCapabilities" ActionGetProfiles = "GetProfiles" ActionGetStreamUri = "GetStreamUri" + ActionGetSnapshotUri = "GetSnapshotUri" ActionSystemReboot = "SystemReboot" ActionGetServices = "GetServices" @@ -65,6 +66,32 @@ func GetCapabilitiesResponse(host string) string { ` } +func GetServicesResponse(host string) string { + return ` + + + + + http://www.onvif.org/ver10/device/wsdl + http://` + host + `/onvif/device_service + + 2 + 5 + + + + http://www.onvif.org/ver10/media/wsdl + http://` + host + `/onvif/media_service + + 2 + 5 + + + + +` +} + func GetSystemDateAndTimeResponse() string { loc := time.Now() utc := loc.UTC() @@ -142,7 +169,7 @@ func GetServiceCapabilitiesResponse() string { - + @@ -171,14 +198,27 @@ func GetProfilesResponse(names []string) string { for i, name := range names { buf.WriteString(` - ` + name + ` - - H264 - - 1920 - 1080 - - + ` + name + ` + + ` + name + ` + H264 + + 1920 + 1080 + + + 29.97003 + 1 + 5000 + + 4 + PT1000S + + + ` + name + ` + ` + strconv.Itoa(i) + ` + + `) } @@ -190,15 +230,55 @@ func GetProfilesResponse(names []string) string { return buf.String() } + +func GetVideoSourcesResponse(names []string) string { + buf := bytes.NewBuffer(nil) + buf.WriteString(` + + + `) + + for i, _ := range names { + buf.WriteString(` + + 29.97003 + + 1920 + 1080 + + `) + } + + buf.WriteString(` + + +`) + + return buf.String() +} + func GetStreamUriResponse(uri string) string { return ` - ` + uri + ` + ` + uri + ` ` } + +func GetSnapshotUriResponse(uri string) string { + return ` + + + + + ` + uri + ` + + + +` +} From 159d9425a732eedef06a9dd797ec6a284339a1b6 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:08:18 -0800 Subject: [PATCH 02/34] Remove non-essential fields --- pkg/onvif/server.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index df53dfab9..e2d565561 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -208,11 +208,8 @@ func GetProfilesResponse(names []string) string { 29.97003 - 1 5000 - 4 - PT1000S ` + name + ` @@ -241,7 +238,6 @@ func GetVideoSourcesResponse(names []string) string { for i, _ := range names { buf.WriteString(` - 29.97003 1920 1080 From 0d6b8fc6fc207c5c89c3cf0923b0cbd38f04ad1b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 29 Dec 2024 11:44:56 +0300 Subject: [PATCH 03/34] Fix OPUS/48000/1 for RTSP from some cameras #1506 --- pkg/rtsp/helpers.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 6b07342d7..346ecf73f 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -70,8 +70,15 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // Check buggy SDP with fmtp for H264 on another track // https://github.com/AlexxIT/WebRTC/issues/419 for _, codec := range media.Codecs { - if codec.Name == core.CodecH264 && codec.FmtpLine == "" { - codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) + switch codec.Name { + case core.CodecH264: + if codec.FmtpLine == "" { + codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) + } + case core.CodecOpus: + // fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587 + codec.ClockRate = 48000 + codec.Channels = 2 } } From a3f084dcde33b9fd341fb3ae00c0d7f6ca7210e9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 29 Dec 2024 22:37:04 +0300 Subject: [PATCH 04/34] RTMP server enhancement to support OpenIPC cameras --- pkg/flv/producer.go | 42 +++++++++++++++++++++++++++--------------- pkg/rtmp/server.go | 13 +++++++------ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 667552173..7535a8a4b 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -140,23 +140,29 @@ func (c *Producer) probe() error { // 1. Empty video/audio flag // 2. MedaData without stereo key for AAC // 3. Audio header after Video keyframe tag - waitType := []byte{TagData} - timeout := time.Now().Add(core.ProbeTimeout) - for len(waitType) != 0 && time.Now().Before(timeout) { + // OpenIPC camera sends: + // 1. Empty video/audio flag + // 2. No MetaData packet + // 3. Sends a video packet in more than 3 seconds + waitVideo := true + waitAudio := true + timeout := time.Now().Add(time.Second * 5) + + for (waitVideo || waitAudio) && time.Now().Before(timeout) { pkt, err := c.readPacket() if err != nil { return err } - if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 { - continue - } else { - waitType = append(waitType[:i], waitType[i+1:]...) - } + //log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload) switch pkt.PayloadType { case TagAudio: + if !waitAudio { + continue + } + _ = pkt.Payload[1] // bounds codecID := pkt.Payload[0] >> 4 // SoundFormat @@ -179,8 +185,13 @@ func (c *Producer) probe() error { Codecs: []*core.Codec{codec}, } c.Medias = append(c.Medias, media) + waitAudio = false case TagVideo: + if !waitVideo { + continue + } + var codec *core.Codec if isExHeader(pkt.Payload) { @@ -213,19 +224,20 @@ func (c *Producer) probe() error { Codecs: []*core.Codec{codec}, } c.Medias = append(c.Medias, media) + waitVideo = false case TagData: if !bytes.Contains(pkt.Payload, []byte("onMetaData")) { - waitType = append(waitType, TagData) + continue } // Dahua cameras doesn't send videocodecid - if bytes.Contains(pkt.Payload, []byte("videocodecid")) || - bytes.Contains(pkt.Payload, []byte("width")) || - bytes.Contains(pkt.Payload, []byte("framerate")) { - waitType = append(waitType, TagVideo) + if !bytes.Contains(pkt.Payload, []byte("videocodecid")) && + !bytes.Contains(pkt.Payload, []byte("width")) && + !bytes.Contains(pkt.Payload, []byte("framerate")) { + waitVideo = false } - if bytes.Contains(pkt.Payload, []byte("audiocodecid")) { - waitType = append(waitType, TagAudio) + if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) { + waitAudio = false } } } diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go index ed727b986..3dcd40486 100644 --- a/pkg/rtmp/server.go +++ b/pkg/rtmp/server.go @@ -117,10 +117,6 @@ func (c *Conn) acceptCommand(b []byte) error { } } - if c.App == "" { - return fmt.Errorf("rtmp: read command %x", b) - } - payload := amf.EncodeItems( "_result", tID, map[string]any{"fmsVer": "FMS/3,0,1,123"}, @@ -129,9 +125,16 @@ func (c *Conn) acceptCommand(b []byte) error { return c.writeMessage(3, TypeCommand, 0, payload) case CommandReleaseStream: + // if app is empty - will use key as app + if c.App == "" && len(items) == 4 { + c.App, _ = items[3].(string) + } + payload := amf.EncodeItems("_result", tID, nil) return c.writeMessage(3, TypeCommand, 0, payload) + case CommandFCPublish: // no response + case CommandCreateStream: payload := amf.EncodeItems("_result", tID, nil, 1) return c.writeMessage(3, TypeCommand, 0, payload) @@ -140,8 +143,6 @@ func (c *Conn) acceptCommand(b []byte) error { c.Intent = cmd c.streamID = 1 - case CommandFCPublish: // no response - default: println("rtmp: unknown command: " + cmd) } From b8303b9a22e1727b9a4db8979b6177f5e13dd35c Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:16:49 -0800 Subject: [PATCH 05/34] Remove optional fields, normalize indentation --- pkg/onvif/server.go | 66 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index e2d565561..bc3f8ffec 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -46,23 +46,23 @@ func GetRequestAction(b []byte) string { func GetCapabilitiesResponse(host string) string { return ` - - - - - http://` + host + `/onvif/device_service - - - http://` + host + `/onvif/media_service - - false - false - true - - - - - + + + + + http://` + host + `/onvif/device_service + + + http://` + host + `/onvif/media_service + + false + false + true + + + + + ` } @@ -197,31 +197,29 @@ func GetProfilesResponse(names []string) string { for i, name := range names { buf.WriteString(` - - ` + name + ` - + + ` + name + ` + ` + name + ` - H264 - - 1920 + H264 + + 1920 1080 - - 29.97003 - 5000 + - + ` + name + ` ` + strconv.Itoa(i) + ` - `) + `) } buf.WriteString(` - - + + `) return buf.String() @@ -233,11 +231,11 @@ func GetVideoSourcesResponse(names []string) string { buf.WriteString(` - `) + `) for i, _ := range names { buf.WriteString(` - + 1920 1080 @@ -246,8 +244,8 @@ func GetVideoSourcesResponse(names []string) string { } buf.WriteString(` - - + + `) return buf.String() From cf88bf9c23e7196cec60dc62644f12b2f20d8083 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:22:49 -0800 Subject: [PATCH 06/34] Remove inaccurate comments --- internal/onvif/init.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index b8b4fca62..e5ed9a7cd 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -71,7 +71,6 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { res = onvif.GetCapabilitiesResponse(r.Host) case onvif.ActionGetServices: - // important for Unifi: Media section res = onvif.GetServicesResponse(r.Host) case onvif.ActionGetSystemDateAndTime: @@ -99,11 +98,9 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { case onvif.ActionGetProfiles: // important for Hass: H264 codec, width, height - // important for Unifi: framerate, bitrate, quality res = onvif.GetProfilesResponse(streams.GetAll()) case onvif.ActionGetVideoSources: - // important for Unifi: framerate, resolution res = onvif.GetVideoSourcesResponse(streams.GetAll()) case onvif.ActionGetStreamUri: From f601c4721833eb9fbb45dba7874890cab01cf974 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 30 Dec 2024 22:34:08 +0300 Subject: [PATCH 07/34] Improve ONVIF server --- examples/onvif_client/main.go | 72 +++++ internal/onvif/README.md | 25 ++ internal/onvif/{init.go => onvif.go} | 84 +++--- pkg/onvif/README.md | 38 +++ pkg/onvif/client.go | 113 ++------ pkg/onvif/envelope.go | 79 +++++ pkg/onvif/helpers.go | 23 ++ pkg/onvif/server.go | 414 +++++++++++++-------------- 8 files changed, 504 insertions(+), 344 deletions(-) create mode 100644 examples/onvif_client/main.go create mode 100644 internal/onvif/README.md rename internal/onvif/{init.go => onvif.go} (67%) create mode 100644 pkg/onvif/README.md create mode 100644 pkg/onvif/envelope.go diff --git a/examples/onvif_client/main.go b/examples/onvif_client/main.go new file mode 100644 index 000000000..03dd12bad --- /dev/null +++ b/examples/onvif_client/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "log" + "net" + "net/url" + "os" + + "github.com/AlexxIT/go2rtc/pkg/onvif" +) + +func main() { + var rawURL = os.Args[1] + var operation = os.Args[2] + var token string + if len(os.Args) > 3 { + token = os.Args[3] + } + + client, err := onvif.NewClient(rawURL) + if err != nil { + log.Panic(err) + } + + var b []byte + + switch operation { + case onvif.ServiceGetServiceCapabilities: + b, err = client.MediaRequest(operation) + case onvif.DeviceGetCapabilities, + onvif.DeviceGetDeviceInformation, + onvif.DeviceGetDiscoveryMode, + onvif.DeviceGetDNS, + onvif.DeviceGetHostname, + onvif.DeviceGetNetworkDefaultGateway, + onvif.DeviceGetNetworkInterfaces, + onvif.DeviceGetNetworkProtocols, + onvif.DeviceGetNTP, + onvif.DeviceGetScopes, + onvif.DeviceGetServices, + onvif.DeviceGetSystemDateAndTime, + onvif.DeviceSystemReboot: + b, err = client.DeviceRequest(operation) + case onvif.MediaGetProfiles, onvif.MediaGetVideoSources: + b, err = client.MediaRequest(operation) + case onvif.MediaGetProfile: + b, err = client.GetProfile(token) + case onvif.MediaGetVideoSourceConfiguration: + b, err = client.GetVideoSourceConfiguration(token) + case onvif.MediaGetStreamUri: + b, err = client.GetStreamUri(token) + case onvif.MediaGetSnapshotUri: + b, err = client.GetSnapshotUri(token) + default: + log.Printf("unknown action\n") + } + + if err != nil { + log.Printf("%s\n", err) + } + + u, err := url.Parse(rawURL) + if err != nil { + log.Fatal(err) + } + + host, _, _ := net.SplitHostPort(u.Host) + + if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil { + log.Printf("%s\n", err) + } +} diff --git a/internal/onvif/README.md b/internal/onvif/README.md new file mode 100644 index 000000000..ee922fbf5 --- /dev/null +++ b/internal/onvif/README.md @@ -0,0 +1,25 @@ +# ONVIF + +A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`). + +Go2rtc has one video source and one profile per stream. + +## Tested clients + +Go2rtc works as ONVIF server: + +- Happytime onvif client (windows) +- Home Assistant ONVIF integration (linux) +- Onvier (android) +- ONVIF Device Manager (windows) + +PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet. + +## Tested cameras + +Go2rtc works as ONVIF client: + +- Dahua IPC-K42 +- OpenIPC +- Reolink RLC-520A +- TP-Link Tapo TC60 diff --git a/internal/onvif/init.go b/internal/onvif/onvif.go similarity index 67% rename from internal/onvif/init.go rename to internal/onvif/onvif.go index e5ed9a7cd..d332ca384 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/onvif.go @@ -55,55 +55,65 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { return } - action := onvif.GetRequestAction(b) - if action == "" { + operation := onvif.GetRequestAction(b) + if operation == "" { http.Error(w, "malformed request body", http.StatusBadRequest) return } - log.Trace().Msgf("[onvif] %s", action) - - var res string - - switch action { - case onvif.ActionGetCapabilities: + log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b) + + switch operation { + case onvif.DeviceGetNetworkInterfaces, // important for Hass + onvif.DeviceGetSystemDateAndTime, // important for Hass + onvif.DeviceGetDiscoveryMode, + onvif.DeviceGetDNS, + onvif.DeviceGetHostname, + onvif.DeviceGetNetworkDefaultGateway, + onvif.DeviceGetNetworkProtocols, + onvif.DeviceGetNTP, + onvif.DeviceGetScopes: + b = onvif.StaticResponse(operation) + + case onvif.DeviceGetCapabilities: // important for Hass: Media section - res = onvif.GetCapabilitiesResponse(r.Host) - - case onvif.ActionGetServices: - res = onvif.GetServicesResponse(r.Host) - - case onvif.ActionGetSystemDateAndTime: - // important for Hass - res = onvif.GetSystemDateAndTimeResponse() + b = onvif.GetCapabilitiesResponse(r.Host) - case onvif.ActionGetNetworkInterfaces: - // important for Hass: none - res = onvif.GetNetworkInterfacesResponse() + case onvif.DeviceGetServices: + b = onvif.GetServicesResponse(r.Host) - case onvif.ActionGetDeviceInformation: + case onvif.DeviceGetDeviceInformation: // important for Hass: SerialNumber (unique server ID) - res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host) + b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host) - case onvif.ActionGetServiceCapabilities: + case onvif.ServiceGetServiceCapabilities: // important for Hass - res = onvif.GetServiceCapabilitiesResponse() + // TODO: check path links to media + b = onvif.GetMediaServiceCapabilitiesResponse() - case onvif.ActionSystemReboot: - res = onvif.SystemRebootResponse() + case onvif.DeviceSystemReboot: + b = onvif.StaticResponse(operation) time.AfterFunc(time.Second, func() { os.Exit(0) }) - case onvif.ActionGetProfiles: + case onvif.MediaGetVideoSources: + b = onvif.GetVideoSourcesResponse(streams.GetAll()) + + case onvif.MediaGetProfiles: // important for Hass: H264 codec, width, height - res = onvif.GetProfilesResponse(streams.GetAll()) + b = onvif.GetProfilesResponse(streams.GetAll()) - case onvif.ActionGetVideoSources: - res = onvif.GetVideoSourcesResponse(streams.GetAll()) + case onvif.MediaGetProfile: + token := onvif.FindTagValue(b, "ProfileToken") + b = onvif.GetProfileResponse(token) - case onvif.ActionGetStreamUri: + case onvif.MediaGetVideoSourceConfiguration: + token := onvif.FindTagValue(b, "ConfigurationToken") + b = onvif.GetVideoSourceConfigurationResponse(token) + + case onvif.MediaGetStreamUri: host, _, err := net.SplitHostPort(r.Host) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -111,20 +121,22 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { } uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") - res = onvif.GetStreamUriResponse(uri) + b = onvif.GetStreamUriResponse(uri) - case onvif.ActionGetSnapshotUri: + case onvif.MediaGetSnapshotUri: uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken") - res = onvif.GetSnapshotUriResponse(uri) + b = onvif.GetSnapshotUriResponse(uri) default: - http.Error(w, "unsupported action", http.StatusBadRequest) + http.Error(w, "unsupported operation", http.StatusBadRequest) log.Debug().Msgf("[onvif] unsupported request:\n%s", b) return } + log.Trace().Msgf("[onvif] server response:\n%s", b) + w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8") - if _, err = w.Write([]byte(res)); err != nil { + if _, err = w.Write(b); err != nil { log.Error().Err(err).Caller().Send() } } @@ -170,7 +182,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { } if l := log.Trace(); l.Enabled() { - b, _ := client.GetProfiles() + b, _ := client.MediaRequest(onvif.MediaGetProfiles) l.Msgf("[onvif] src=%s profiles:\n%s", src, b) } diff --git a/pkg/onvif/README.md b/pkg/onvif/README.md new file mode 100644 index 000000000..732673794 --- /dev/null +++ b/pkg/onvif/README.md @@ -0,0 +1,38 @@ +## Profiles + +- Profile A - For access control configuration +- Profile C - For door control and event management +- Profile S - For basic video streaming + - Video streaming and configuration +- Profile T - For advanced video streaming + - H.264 / H.265 video compression + - Imaging settings + - Motion alarm and tampering events + - Metadata streaming + - Bi-directional audio + +## Services + +https://www.onvif.org/profiles/specifications/ + +- https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl +- https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl +- https://www.onvif.org/ver10/media/wsdl/media.wsdl + +## TMP + +| | Dahua | Reolink | TP-Link | +|------------------------|---------|---------|---------| +| GetCapabilities | no auth | no auth | no auth | +| GetServices | no auth | no auth | no auth | +| GetServiceCapabilities | no auth | no auth | auth | +| GetSystemDateAndTime | no auth | no auth | no auth | +| GetNetworkInterfaces | auth | auth | auth | +| GetDeviceInformation | auth | auth | auth | +| GetProfiles | auth | auth | auth | +| GetScopes | auth | auth | auth | + +- Dahua - onvif://192.168.10.90:80 +- Reolink - onvif://192.168.10.92:8000 +- TP-Link - onvif://192.168.10.91:2020/onvif/device_service +- \ No newline at end of file diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index 97bfd8dc8..cb6221e19 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -2,8 +2,6 @@ package onvif import ( "bytes" - "crypto/sha1" - "encoding/base64" "errors" "html" "io" @@ -12,8 +10,6 @@ import ( "regexp" "strings" "time" - - "github.com/AlexxIT/go2rtc/pkg/core" ) const PathDevice = "/onvif/device_service" @@ -41,7 +37,7 @@ func NewClient(rawURL string) (*Client, error) { client.deviceURL = baseURL + u.Path } - b, err := client.GetCapabilities() + b, err := client.DeviceRequest(DeviceGetCapabilities) if err != nil { return nil, err } @@ -95,7 +91,7 @@ func (c *Client) GetURI() (string, error) { } func (c *Client) GetName() (string, error) { - b, err := c.GetDeviceInformation() + b, err := c.DeviceRequest(DeviceGetDeviceInformation) if err != nil { return "", err } @@ -104,7 +100,7 @@ func (c *Client) GetName() (string, error) { } func (c *Client) GetProfilesTokens() ([]string, error) { - b, err := c.GetProfiles() + b, err := c.MediaRequest(MediaGetProfiles) if err != nil { return nil, err } @@ -127,86 +123,53 @@ func (c *Client) HasSnapshots() bool { return strings.Contains(string(b), `SnapshotUri="true"`) } -func (c *Client) GetCapabilities() ([]byte, error) { - return c.Request( - c.deviceURL, - ` - All -`, - ) -} - -func (c *Client) GetNetworkInterfaces() ([]byte, error) { - return c.Request( - c.deviceURL, ``, - ) -} - -func (c *Client) GetDeviceInformation() ([]byte, error) { +func (c *Client) GetProfile(token string) ([]byte, error) { return c.Request( - c.deviceURL, ``, + c.mediaURL, ``+token+``, ) } -func (c *Client) GetProfiles() ([]byte, error) { - return c.Request( - c.mediaURL, ``, - ) +func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) { + return c.Request(c.mediaURL, ` + `+token+` +`) } func (c *Client) GetStreamUri(token string) ([]byte, error) { - return c.Request( - c.mediaURL, - ` + return c.Request(c.mediaURL, ` RTP-Unicast RTSP `+token+` -`, - ) +`) } func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( - c.imaginURL, - ` - `+token+` -`, - ) -} - -func (c *Client) GetSystemDateAndTime() ([]byte, error) { - return c.Request( - c.deviceURL, ``, + c.imaginURL, ``+token+``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( - c.mediaURL, ``, - ) -} - -func (c *Client) SystemReboot() ([]byte, error) { - return c.Request( - c.deviceURL, ``, + c.mediaURL, ``, ) } -func (c *Client) GetServices() ([]byte, error) { - return c.Request( - c.deviceURL, ` - true -`, - ) +func (c *Client) DeviceRequest(operation string) ([]byte, error) { + if operation == DeviceGetServices { + operation = `true` + } else { + operation = `` + } + return c.Request(c.deviceURL, operation) } -func (c *Client) GetScopes() ([]byte, error) { - return c.Request( - c.deviceURL, ``, - ) +func (c *Client) MediaRequest(operation string) ([]byte, error) { + operation = `` + return c.Request(c.mediaURL, operation) } func (c *Client) Request(url, body string) ([]byte, error) { @@ -214,35 +177,11 @@ func (c *Client) Request(url, body string) ([]byte, error) { return nil, errors.New("onvif: unsupported service") } - buf := bytes.NewBuffer(nil) - buf.WriteString( - ``, - ) - - if user := c.url.User; user != nil { - nonce := core.RandString(16, 36) - created := time.Now().UTC().Format(time.RFC3339Nano) - pass, _ := user.Password() - - h := sha1.New() - h.Write([]byte(nonce + created + pass)) - - buf.WriteString(` - - -` + user.Username() + ` -` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + ` -` + base64.StdEncoding.EncodeToString([]byte(nonce)) + ` -` + created + ` - - -`) - } - - buf.WriteString(`` + body + ``) + e := NewEnvelopeWithUser(c.url.User) + e.Append(body) client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf) + res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes())) if err != nil { return nil, err } diff --git a/pkg/onvif/envelope.go b/pkg/onvif/envelope.go new file mode 100644 index 000000000..f0e1b29cf --- /dev/null +++ b/pkg/onvif/envelope.go @@ -0,0 +1,79 @@ +package onvif + +import ( + "crypto/sha1" + "encoding/base64" + "fmt" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" +) + +type Envelope struct { + buf []byte +} + +const ( + prefix1 = ` + +` + prefix2 = ` +` + suffix = ` + +` +) + +func NewEnvelope() *Envelope { + e := &Envelope{buf: make([]byte, 0, 1024)} + e.Append(prefix1, prefix2) + return e +} + +func NewEnvelopeWithUser(user *url.Userinfo) *Envelope { + if user == nil { + return NewEnvelope() + } + + nonce := core.RandString(16, 36) + created := time.Now().UTC().Format(time.RFC3339Nano) + pass, _ := user.Password() + + h := sha1.New() + h.Write([]byte(nonce + created + pass)) + + e := &Envelope{buf: make([]byte, 0, 1024)} + e.Append(prefix1) + e.Appendf(` + + + %s + %s + %s + %s + + + +`, + user.Username(), + base64.StdEncoding.EncodeToString(h.Sum(nil)), + base64.StdEncoding.EncodeToString([]byte(nonce)), + created) + e.Append(prefix2) + return e +} + +func (e *Envelope) Append(args ...string) { + for _, s := range args { + e.buf = append(e.buf, s...) + } +} + +func (e *Envelope) Appendf(format string, args ...any) { + e.buf = fmt.Appendf(e.buf, format, args...) +} + +func (e *Envelope) Bytes() []byte { + return append(e.buf, suffix...) +} diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index fc9c8392a..251f45790 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -1,6 +1,7 @@ package onvif import ( + "fmt" "net" "regexp" "strconv" @@ -106,3 +107,25 @@ func atoi(s string) int { } return i } + +func GetPosixTZ(current time.Time) string { + // Thanks to https://github.com/Path-Variable/go-posix-time + _, offset := current.Zone() + + if current.IsDST() { + _, end := current.ZoneBounds() + endPlus1 := end.Add(time.Hour * 25) + _, offset = endPlus1.Zone() + } + + var prefix string + if offset < 0 { + prefix = "GMT+" + offset = -offset / 60 + } else { + prefix = "GMT-" + offset = offset / 60 + } + + return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60) +} diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index bc3f8ffec..42343d375 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -2,31 +2,40 @@ package onvif import ( "bytes" - "fmt" "regexp" - "strconv" "time" ) +const ServiceGetServiceCapabilities = "GetServiceCapabilities" + const ( - ActionGetCapabilities = "GetCapabilities" - ActionGetSystemDateAndTime = "GetSystemDateAndTime" - ActionGetNetworkInterfaces = "GetNetworkInterfaces" - ActionGetDeviceInformation = "GetDeviceInformation" - ActionGetServiceCapabilities = "GetServiceCapabilities" - ActionGetProfiles = "GetProfiles" - ActionGetStreamUri = "GetStreamUri" - ActionGetSnapshotUri = "GetSnapshotUri" - ActionSystemReboot = "SystemReboot" + DeviceGetCapabilities = "GetCapabilities" + DeviceGetDeviceInformation = "GetDeviceInformation" + DeviceGetDiscoveryMode = "GetDiscoveryMode" + DeviceGetDNS = "GetDNS" + DeviceGetHostname = "GetHostname" + DeviceGetNetworkDefaultGateway = "GetNetworkDefaultGateway" + DeviceGetNetworkInterfaces = "GetNetworkInterfaces" + DeviceGetNetworkProtocols = "GetNetworkProtocols" + DeviceGetNTP = "GetNTP" + DeviceGetScopes = "GetScopes" + DeviceGetServices = "GetServices" + DeviceGetSystemDateAndTime = "GetSystemDateAndTime" + DeviceSystemReboot = "SystemReboot" +) - ActionGetServices = "GetServices" - ActionGetScopes = "GetScopes" - ActionGetVideoSources = "GetVideoSources" - ActionGetAudioSources = "GetAudioSources" - ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations" - ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations" - ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations" - ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations" +const ( + MediaGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations" + MediaGetAudioSources = "GetAudioSources" + MediaGetAudioSourceConfigurations = "GetAudioSourceConfigurations" + MediaGetProfile = "GetProfile" + MediaGetProfiles = "GetProfiles" + MediaGetSnapshotUri = "GetSnapshotUri" + MediaGetStreamUri = "GetStreamUri" + MediaGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations" + MediaGetVideoSources = "GetVideoSources" + MediaGetVideoSourceConfiguration = "GetVideoSourceConfiguration" + MediaGetVideoSourceConfigurations = "GetVideoSourceConfigurations" ) func GetRequestAction(b []byte) string { @@ -43,236 +52,199 @@ func GetRequestAction(b []byte) string { return string(m[1]) } -func GetCapabilitiesResponse(host string) string { - return ` - - - - - - http://` + host + `/onvif/device_service - - - http://` + host + `/onvif/media_service - - false - false - true - - - - - -` +func GetCapabilitiesResponse(host string) []byte { + e := NewEnvelope() + e.Append(` + + + http://`, host, `/onvif/device_service + + + http://`, host, `/onvif/media_service + + false + false + true + + + +`) + return e.Bytes() } -func GetServicesResponse(host string) string { - return ` - - - - - http://www.onvif.org/ver10/device/wsdl - http://` + host + `/onvif/device_service - - 2 - 5 - - - - http://www.onvif.org/ver10/media/wsdl - http://` + host + `/onvif/media_service - - 2 - 5 - - - - -` +func GetServicesResponse(host string) []byte { + e := NewEnvelope() + e.Append(` + + http://www.onvif.org/ver10/device/wsdl + http://`, host, `/onvif/device_service + 25 + + + http://www.onvif.org/ver10/media/wsdl + http://`, host, `/onvif/media_service + 25 + +`) + return e.Bytes() } -func GetSystemDateAndTimeResponse() string { +func GetSystemDateAndTimeResponse() []byte { loc := time.Now() utc := loc.UTC() - return fmt.Sprintf(` - - - - - NTP - false - - GMT%s - - - - %d - %d - %d - - - %d - %d - %d - - - - - %d - %d - %d - - - %d - %d - %d - - - - - -`, - loc.Format("-07:00"), + e := NewEnvelope() + e.Appendf(` + + NTP + true + + %s + + + %d%d%d + %d%d%d + + + %d%d%d + %d%d%d + + +`, + GetPosixTZ(loc), utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(), loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(), ) + return e.Bytes() } -func GetNetworkInterfacesResponse() string { - return ` - - - - -` +func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte { + e := NewEnvelope() + e.Append(` + `, manuf, ` + `, model, ` + `, firmware, ` + `, serial, ` + 1.00 +`) + return e.Bytes() } -func GetDeviceInformationResponse(manuf, model, firmware, serial string) string { - return ` - - - - ` + manuf + ` - ` + model + ` - ` + firmware + ` - ` + serial + ` - 1.00 - - -` +func GetMediaServiceCapabilitiesResponse() []byte { + e := NewEnvelope() + e.Append(` + + + +`) + return e.Bytes() } -func GetServiceCapabilitiesResponse() string { - return ` - - - - - - - - -` +func GetProfilesResponse(names []string) []byte { + e := NewEnvelope() + e.Append(` +`) + for _, name := range names { + appendProfile(e, "Profiles", name) + } + e.Append(``) + return e.Bytes() } -func SystemRebootResponse() string { - return ` - - - - system reboot in 1 second... - - -` +func GetProfileResponse(name string) []byte { + e := NewEnvelope() + e.Append(` +`) + appendProfile(e, "Profile", name) + e.Append(``) + return e.Bytes() } -func GetProfilesResponse(names []string) string { - buf := bytes.NewBuffer(nil) - buf.WriteString(` - - - `) - - for i, name := range names { - buf.WriteString(` - - ` + name + ` - - ` + name + ` - H264 - - 1920 - 1080 - - - - - - ` + name + ` - ` + strconv.Itoa(i) + ` - - - `) - } - - buf.WriteString(` - - -`) - - return buf.String() +func appendProfile(e *Envelope, tag, name string) { + e.Append(` + `, name, ` + + VSC + `, name, ` + + + + VEC + H264 + 19201080 + + +`) } +func GetVideoSourceConfigurationResponse(name string) []byte { + e := NewEnvelope() + e.Append(` + + VSC + `, name, ` + + +`) + return e.Bytes() +} -func GetVideoSourcesResponse(names []string) string { - buf := bytes.NewBuffer(nil) - buf.WriteString(` - - - `) - - for i, _ := range names { - buf.WriteString(` - - - 1920 - 1080 - - `) +func GetVideoSourcesResponse(names []string) []byte { + e := NewEnvelope() + e.Append(` +`) + for _, name := range names { + e.Append(` + 30.000000 + 19201080 + +`) } + e.Append(``) + return e.Bytes() +} - buf.WriteString(` - - -`) +func GetStreamUriResponse(uri string) []byte { + e := NewEnvelope() + e.Append(``, uri, ``) + return e.Bytes() +} - return buf.String() +func GetSnapshotUriResponse(uri string) []byte { + e := NewEnvelope() + e.Append(``, uri, ``) + return e.Bytes() } -func GetStreamUriResponse(uri string) string { - return ` - - - - - ` + uri + ` - - - -` +func StaticResponse(operation string) []byte { + switch operation { + case DeviceGetSystemDateAndTime: + return GetSystemDateAndTimeResponse() + } + + e := NewEnvelope() + e.Append(responses[operation]) + b := e.Bytes() + if operation == DeviceGetNetworkInterfaces { + println() + } + return b } -func GetSnapshotUriResponse(uri string) string { - return ` - - - - - ` + uri + ` - - - -` +var responses = map[string]string{ + DeviceGetDiscoveryMode: `Discoverable`, + DeviceGetDNS: ``, + DeviceGetHostname: ``, + DeviceGetNetworkDefaultGateway: ``, + DeviceGetNTP: ``, + DeviceSystemReboot: `OK`, + + DeviceGetNetworkInterfaces: ``, + DeviceGetNetworkProtocols: ``, + DeviceGetScopes: ` + Fixedonvif://www.onvif.org/name/go2rtc + Fixedonvif://www.onvif.org/location/github + Fixedonvif://www.onvif.org/Profile/Streaming + Fixedonvif://www.onvif.org/type/Network_Video_Transmitter +`, } From bc9194d74092fb9c4de4d9162bf7f9558fdec996 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 3 Jan 2025 13:33:12 +0300 Subject: [PATCH 08/34] Update go dependencies --- go.mod | 27 ++++++++++++++------------- go.sum | 55 ++++++++++++++++++++++++++++++++----------------------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index ecd32f3ab..5f0a193bb 100644 --- a/go.mod +++ b/go.mod @@ -8,20 +8,20 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.62 - github.com/pion/ice/v2 v2.3.36 + github.com/pion/ice/v2 v2.3.37 github.com/pion/interceptor v0.1.37 - github.com/pion/rtcp v1.2.14 - github.com/pion/rtp v1.8.9 + github.com/pion/rtcp v1.2.15 + github.com/pion/rtp v1.8.10 github.com/pion/sdp/v3 v3.0.9 github.com/pion/srtp/v2 v2.0.20 github.com/pion/stun v0.6.1 - github.com/pion/webrtc/v3 v3.3.4 + github.com/pion/webrtc/v3 v3.3.5 github.com/rs/zerolog v1.33.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.31.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -31,19 +31,20 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.33 // indirect + github.com/pion/sctp v1.8.35 // indirect github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/tools v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 804ecc435..c75ffced8 100644 --- a/go.sum +++ b/go.sum @@ -31,13 +31,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= -github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= -github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= -github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= +github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -47,13 +47,13 @@ github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYF github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= -github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= -github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= -github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= +github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU= +github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= +github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= +github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= @@ -67,11 +67,12 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= -github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= +github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg= +github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= @@ -95,8 +96,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= @@ -108,12 +110,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -122,13 +125,13 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -143,8 +146,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -165,6 +168,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= From 4035e916723e1bbde5711b88f84e01faa5b1e60c Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 3 Jan 2025 15:08:38 +0300 Subject: [PATCH 09/34] Fix ONVIF XML tag parsing in some cases --- pkg/onvif/helpers.go | 2 +- pkg/onvif/onvif_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index 251f45790..f240f2ece 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -12,7 +12,7 @@ import ( ) func FindTagValue(b []byte, tag string) string { - re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`) + re := regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`) m := re.FindSubmatch(b) if len(m) != 2 { return "" diff --git a/pkg/onvif/onvif_test.go b/pkg/onvif/onvif_test.go index cd57d60be..e9ffab048 100644 --- a/pkg/onvif/onvif_test.go +++ b/pkg/onvif/onvif_test.go @@ -84,6 +84,34 @@ func TestGetStreamUri(t *testing.T) { `, url: "rtsp://192.168.5.53:8090/profile1=r", }, + { + name: "go2rtc 1.9.4", + xml: ` + + + + rtsp://192.168.1.123:8554/rtsp-dahua1 + + + +`, + url: "rtsp://192.168.1.123:8554/rtsp-dahua1", + }, + { + name: "go2rtc 1.9.8", + xml: ` + + + + + rtsp://192.168.1.123:8554/rtsp-dahua2 + + + + +`, + url: "rtsp://192.168.1.123:8554/rtsp-dahua2", + }, } for _, test := range tests { From 199fdd6728eb932d3a34e5cebdfc00326a44ced8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 3 Jan 2025 16:24:31 +0300 Subject: [PATCH 10/34] Update version to 1.9.8 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d5c59ffc8..b8d58b270 100644 --- a/main.go +++ b/main.go @@ -37,7 +37,7 @@ import ( ) func main() { - app.Version = "1.9.7" + app.Version = "1.9.8" // 1. Core modules: app, api/ws, streams From 55af09a3502ad72b3909df637b03740321d7c404 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 5 Jan 2025 11:03:44 +0300 Subject: [PATCH 11/34] Add support fix JPEG from some MJPEG sources --- pkg/mjpeg/helpers.go | 59 +++++++++++++++++++++++++++++++++++--------- pkg/mjpeg/jpeg.go | 10 ++++++++ pkg/mjpeg/rfc2435.go | 30 +++++++++++----------- 3 files changed, 73 insertions(+), 26 deletions(-) create mode 100644 pkg/mjpeg/jpeg.go diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go index 08b4408b6..d1acbd453 100644 --- a/pkg/mjpeg/helpers.go +++ b/pkg/mjpeg/helpers.go @@ -9,24 +9,38 @@ import ( "github.com/pion/rtp" ) -// FixJPEG - reencode JPEG if it has wrong header -// -// for example, this app produce "bad" images: -// https://github.com/jacksonliam/mjpg-streamer -// -// and they can't be uploaded to the Telegram servers: -// {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"} func FixJPEG(b []byte) []byte { // skip non-JPEG - if len(b) < 10 || b[0] != 0xFF || b[1] != 0xD8 { + if len(b) < 10 || b[0] != 0xFF || b[1] != markerSOI { return b } - // skip if header OK for imghdr library - // https://docs.python.org/3/library/imghdr.html - if string(b[2:4]) == "\xFF\xDB" || string(b[6:10]) == "JFIF" || string(b[6:10]) == "Exif" { + + // skip JPEG without app marker + if b[2] == 0xFF && b[3] == markerDQT { + return b + } + + switch string(b[6:10]) { + case "JFIF", "Exif": + // skip if header OK for imghdr library + // - https://docs.python.org/3/library/imghdr.html return b + case "AVI1": + // adds DHT tables to JPEG file before SOS marker + // useful when you want to save a JPEG frame from an MJPEG stream + // - https://github.com/image-rs/jpeg-decoder/issues/76 + // - https://github.com/pion/mediadevices/pull/493 + // - https://bugzilla.mozilla.org/show_bug.cgi?id=963907#c18 + return InjectDHT(b) } + // reencode JPEG if it has wrong header + // + // for example, this app produce "bad" images: + // https://github.com/jacksonliam/mjpg-streamer + // + // and they can't be uploaded to the Telegram servers: + // {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"} img, err := jpeg.Decode(bytes.NewReader(b)) if err != nil { return b @@ -54,3 +68,26 @@ func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { handler(&clone) } } + +const dhtSize = 432 // known size for 4 default tables + +func InjectDHT(b []byte) []byte { + if bytes.Index(b, []byte{0xFF, markerDHT}) > 0 { + return b // already exist + } + + i := bytes.Index(b, []byte{0xFF, markerSOS}) + if i < 0 { + return b + } + + dht := make([]byte, 0, dhtSize) + dht = MakeHuffmanHeaders(dht) + + tmp := make([]byte, len(b)+dhtSize) + copy(tmp, b[:i]) + copy(tmp[i:], dht) + copy(tmp[i+dhtSize:], b[i:]) + + return tmp +} diff --git a/pkg/mjpeg/jpeg.go b/pkg/mjpeg/jpeg.go new file mode 100644 index 000000000..8d6d13d1e --- /dev/null +++ b/pkg/mjpeg/jpeg.go @@ -0,0 +1,10 @@ +package mjpeg + +const ( + markerSOF = 0xC0 // Start Of Frame (Baseline Sequential) + markerSOI = 0xD8 // Start Of Image + markerEOI = 0xD9 // End Of Image + markerSOS = 0xDA // Start Of Scan + markerDQT = 0xDB // Define Quantization Table + markerDHT = 0xC4 // Define Huffman Table +) diff --git a/pkg/mjpeg/rfc2435.go b/pkg/mjpeg/rfc2435.go index 443078964..aa34c2f15 100644 --- a/pkg/mjpeg/rfc2435.go +++ b/pkg/mjpeg/rfc2435.go @@ -143,9 +143,7 @@ var chm_ac_symbols = []byte{ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { // Appendix A from https://www.rfc-editor.org/rfc/rfc2435 - p = append(p, 0xFF, - 0xD8, // SOI - ) + p = append(p, 0xFF, markerSOI) p = MakeQuantHeader(p, lqt, 0) p = MakeQuantHeader(p, cqt, 1) @@ -156,8 +154,7 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { t = 0x22 // hsamp = 2, vsamp = 2 } - p = append(p, 0xFF, - 0xC0, // SOF + p = append(p, 0xFF, markerSOF, 0, 17, // size 8, // bits per component byte(h>>8), byte(h&0xFF), @@ -174,13 +171,9 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { 1, // quant table 1 ) - p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0) - p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1) - p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0) - p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1) + p = MakeHuffmanHeaders(p) - return append(p, 0xFF, - 0xDA, // SOS + return append(p, 0xFF, markerSOS, 0, 12, // size 3, // 3 components 0, // comp 0 @@ -196,16 +189,23 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { } func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte { - p = append(p, 0xFF, 0xDB, 0, 67, tableNo) + p = append(p, 0xFF, markerDQT, 0, 67, tableNo) return append(p, qt...) } func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte { - p = append(p, - 0xFF, 0xC4, 0, - byte(3+len(codelens)+len(symbols)), + p = append(p, 0xFF, markerDHT, + 0, byte(3+len(codelens)+len(symbols)), // size (tableClass<<4)|tableNo, ) p = append(p, codelens...) return append(p, symbols...) } + +func MakeHuffmanHeaders(p []byte) []byte { + p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0) + p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1) + p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0) + p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1) + return p +} From a9e1ebc0a8da09f321e5eb08973c7c7b6118d8b5 Mon Sep 17 00:00:00 2001 From: Bruno Tomassetti Couto Date: Sun, 5 Jan 2025 22:54:20 -0300 Subject: [PATCH 12/34] Improve ONVIF server by adding rate control for video encoder configuration --- pkg/onvif/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index 42343d375..66f270951 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -172,6 +172,7 @@ func appendProfile(e *Envelope, tag, name string) { VEC H264 19201080 + `) From c065db6da149c06dc7f6988848e9246ca80d6ac0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 Jan 2025 06:32:13 +0300 Subject: [PATCH 13/34] Code refactoring after #1539 --- pkg/onvif/server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index 66f270951..db0bb2fb6 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -161,6 +161,7 @@ func GetProfileResponse(name string) []byte { } func appendProfile(e *Envelope, tag, name string) { + // empty `RateControl` important for UniFi Protect e.Append(` `, name, ` @@ -172,7 +173,7 @@ func appendProfile(e *Envelope, tag, name string) { VEC H264 19201080 - + `) From df831833b1fcea9fd5e0cab0c11fea6920de47eb Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 Jan 2025 19:31:03 +0300 Subject: [PATCH 14/34] Collect list of dependency license --- scripts/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/README.md b/scripts/README.md index 36f667b2b..acc6e0c9b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -54,6 +54,41 @@ go list -deps .\cmd\go2rtc_rtsp\ - golang.org/x/tools ``` +## Licenses + +- github.com/asticode/go-astits - MIT +- github.com/expr-lang/expr - MIT +- github.com/gorilla/websocket - BSD-2 +- github.com/mattn/go-isatty - MIT +- github.com/miekg/dns - BSD-3 +- github.com/pion/ice/v2 - MIT +- github.com/pion/interceptor - MIT +- github.com/pion/rtcp - MIT +- github.com/pion/rtp - MIT +- github.com/pion/sdp/v3 - MIT +- github.com/pion/srtp/v2 - MIT +- github.com/pion/stun - MIT +- github.com/pion/webrtc/v3 - MIT +- github.com/rs/zerolog - MIT +- github.com/sigurn/crc16 - MIT +- github.com/sigurn/crc8 - MIT +- github.com/stretchr/testify - MIT +- github.com/tadglines/go-pkgs - Apache +- golang.org/x/crypto - BSD-3 +- gopkg.in/yaml.v3 - MIT and Apache +- github.com/asticode/go-astikit - MIT +- github.com/davecgh/go-spew - ISC (BSD/MIT like) +- github.com/google/uuid - BSD-3 +- github.com/kr/pretty - MIT +- github.com/mattn/go-colorable - MIT +- github.com/pmezard/go-difflib - ??? +- github.com/wlynxg/anet - BSD-3 +- golang.org/x/mod - BSD-3 +- golang.org/x/net - BSD-3 +- golang.org/x/sync - BSD-3 +- golang.org/x/sys - BSD-3 +- golang.org/x/tools - BSD-3 + ## Virus - https://go.dev/doc/faq#virus From d59139a2ab200eb064bab7be9fdc4b1d9f1227e1 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 Jan 2025 23:47:35 +0300 Subject: [PATCH 15/34] Add support v4l2 source --- examples/go2rtc_mjpeg/main.go | 21 +++ internal/v4l2/v4l2.go | 7 + internal/v4l2/v4l2_linux.go | 79 ++++++++++ main.go | 2 + pkg/v4l2/device/device.go | 244 ++++++++++++++++++++++++++++++ pkg/v4l2/device/formats.go | 40 +++++ pkg/v4l2/device/videodev2_test.go | 34 +++++ pkg/v4l2/device/videodev2_x32.go | 152 +++++++++++++++++++ pkg/v4l2/device/videodev2_x64.go | 153 +++++++++++++++++++ pkg/v4l2/producer.go | 115 ++++++++++++++ www/add.html | 12 ++ 11 files changed, 859 insertions(+) create mode 100644 examples/go2rtc_mjpeg/main.go create mode 100644 internal/v4l2/v4l2.go create mode 100644 internal/v4l2/v4l2_linux.go create mode 100644 pkg/v4l2/device/device.go create mode 100644 pkg/v4l2/device/formats.go create mode 100644 pkg/v4l2/device/videodev2_test.go create mode 100644 pkg/v4l2/device/videodev2_x32.go create mode 100644 pkg/v4l2/device/videodev2_x64.go create mode 100644 pkg/v4l2/producer.go diff --git a/examples/go2rtc_mjpeg/main.go b/examples/go2rtc_mjpeg/main.go new file mode 100644 index 000000000..a3e08ff54 --- /dev/null +++ b/examples/go2rtc_mjpeg/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/mjpeg" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/internal/v4l2" + "github.com/AlexxIT/go2rtc/pkg/shell" +) + +func main() { + app.Init() + streams.Init() + + api.Init() + mjpeg.Init() + v4l2.Init() + + shell.RunUntilSignal() +} diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go new file mode 100644 index 000000000..9cef99a59 --- /dev/null +++ b/internal/v4l2/v4l2.go @@ -0,0 +1,7 @@ +//go:build !linux + +package v4l2 + +func Init() { + // not supported +} diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go new file mode 100644 index 000000000..4a54e1e11 --- /dev/null +++ b/internal/v4l2/v4l2_linux.go @@ -0,0 +1,79 @@ +package v4l2 + +import ( + "encoding/binary" + "fmt" + "net/http" + "os" + "strings" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/v4l2" + "github.com/AlexxIT/go2rtc/pkg/v4l2/device" +) + +func Init() { + streams.HandleFunc("v4l2", func(source string) (core.Producer, error) { + return v4l2.Open(source) + }) + + api.HandleFunc("api/v4l2", apiV4L2) +} + +func apiV4L2(w http.ResponseWriter, r *http.Request) { + files, err := os.ReadDir("/dev") + if err != nil { + return + } + + var sources []*api.Source + + for _, file := range files { + if !strings.HasPrefix(file.Name(), core.KindVideo) { + continue + } + + path := "/dev/" + file.Name() + + dev, err := device.Open(path) + if err != nil { + continue + } + + formats, _ := dev.ListFormats() + for _, fourCC := range formats { + source := &api.Source{} + + for _, format := range device.Formats { + if format.FourCC == fourCC { + source.Name = format.Name + source.URL = "v4l2:device?video=" + path + "&input_format=" + format.FFmpeg + "&video_size=" + break + } + } + + if source.Name != "" { + sizes, _ := dev.ListSizes(fourCC) + for i := 0; i < len(sizes); i += 2 { + size := fmt.Sprintf("%dx%d", sizes[i], sizes[i+1]) + if i > 0 { + source.Info += " " + size + } else { + source.Info = size + source.URL += size + } + } + } else { + source.Name = string(binary.LittleEndian.AppendUint32(nil, fourCC)) + } + + sources = append(sources, source) + } + + _ = dev.Close() + } + + api.ResponseSources(w, sources) +} diff --git a/main.go b/main.go index b8d58b270..db8de9f41 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/srtp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/tapo" + "github.com/AlexxIT/go2rtc/internal/v4l2" "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/internal/webtorrent" "github.com/AlexxIT/go2rtc/pkg/shell" @@ -84,6 +85,7 @@ func main() { expr.Init() // expr source gopro.Init() // gopro source doorbird.Init() // doorbird source + v4l2.Init() // v4l2 source // 6. Helper modules diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go new file mode 100644 index 000000000..4e4d6ade2 --- /dev/null +++ b/pkg/v4l2/device/device.go @@ -0,0 +1,244 @@ +//go:build linux + +package device + +import ( + "bytes" + "errors" + "fmt" + "syscall" + "unsafe" +) + +type Device struct { + fd int + bufs [][]byte +} + +func Open(path string) (*Device, error) { + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, err + } + return &Device{fd: fd}, nil +} + +const buffersCount = 2 + +type Capability struct { + Driver string + Card string + BusInfo string + Version string +} + +func (d *Device) Capability() (*Capability, error) { + c := v4l2_capability{} + if err := ioctl(d.fd, VIDIOC_QUERYCAP, unsafe.Pointer(&c)); err != nil { + return nil, err + } + return &Capability{ + Driver: str(c.driver[:]), + Card: str(c.card[:]), + BusInfo: str(c.bus_info[:]), + Version: fmt.Sprintf("%d.%d.%d", byte(c.version>>16), byte(c.version>>8), byte(c.version)), + }, nil +} + +func (d *Device) ListFormats() ([]uint32, error) { + var items []uint32 + + for i := uint32(0); ; i++ { + fd := v4l2_fmtdesc{ + index: i, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + } + if err := ioctl(d.fd, VIDIOC_ENUM_FMT, unsafe.Pointer(&fd)); err != nil { + if !errors.Is(err, syscall.EINVAL) { + return nil, err + } + break + } + + items = append(items, fd.pixelformat) + } + + return items, nil +} + +func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) { + var items []uint32 + + for i := uint32(0); ; i++ { + fs := v4l2_frmsizeenum{ + index: i, + pixel_format: pixFmt, + } + if err := ioctl(d.fd, VIDIOC_ENUM_FRAMESIZES, unsafe.Pointer(&fs)); err != nil { + if !errors.Is(err, syscall.EINVAL) { + return nil, err + } + break + } + + if fs.typ != V4L2_FRMSIZE_TYPE_DISCRETE { + continue + } + + items = append(items, fs.discrete.width, fs.discrete.height) + } + + return items, nil +} + +func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) { + var items []uint32 + + for i := uint32(0); ; i++ { + fi := v4l2_frmivalenum{ + index: i, + pixel_format: pixFmt, + width: width, + height: height, + } + if err := ioctl(d.fd, VIDIOC_ENUM_FRAMEINTERVALS, unsafe.Pointer(&fi)); err != nil { + if !errors.Is(err, syscall.EINVAL) { + return nil, err + } + break + } + + if fi.typ != V4L2_FRMIVAL_TYPE_DISCRETE || fi.discrete.numerator != 1 { + continue + } + + items = append(items, fi.discrete.denominator) + } + + return items, nil +} + +func (d *Device) SetFormat(width, height, pixFmt uint32) error { + f := v4l2_format{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + fmt: v4l2_pix_format{ + width: width, + height: height, + pixelformat: pixFmt, + field: V4L2_FIELD_NONE, + colorspace: V4L2_COLORSPACE_DEFAULT, + }, + } + return ioctl(d.fd, VIDIOC_S_FMT, unsafe.Pointer(&f)) +} + +func (d *Device) SetParam(fps uint32) error { + p := v4l2_streamparm{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + capture: v4l2_captureparm{ + timeperframe: v4l2_fract{numerator: 1, denominator: fps}, + }, + } + return ioctl(d.fd, VIDIOC_S_PARM, unsafe.Pointer(&p)) +} + +func (d *Device) StreamOn() (err error) { + rb := v4l2_requestbuffers{ + count: buffersCount, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + if err = ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)); err != nil { + return err + } + + d.bufs = make([][]byte, buffersCount) + for i := uint32(0); i < buffersCount; i++ { + qb := v4l2_buffer{ + index: i, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + if err = ioctl(d.fd, VIDIOC_QUERYBUF, unsafe.Pointer(&qb)); err != nil { + return err + } + + if d.bufs[i], err = syscall.Mmap( + d.fd, int64(qb.offset), int(qb.length), syscall.PROT_READ, syscall.MAP_SHARED, + ); nil != err { + return err + } + + if err = ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&qb)); err != nil { + return err + } + } + + typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE) + return ioctl(d.fd, VIDIOC_STREAMON, unsafe.Pointer(&typ)) +} + +func (d *Device) StreamOff() (err error) { + typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE) + if err = ioctl(d.fd, VIDIOC_STREAMOFF, unsafe.Pointer(&typ)); err != nil { + return err + } + + for i := range d.bufs { + _ = syscall.Munmap(d.bufs[i]) + } + + rb := v4l2_requestbuffers{ + count: 0, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) +} + +func (d *Device) Capture(cositedYUV bool) ([]byte, error) { + dec := v4l2_buffer{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + if err := ioctl(d.fd, VIDIOC_DQBUF, unsafe.Pointer(&dec)); err != nil { + return nil, err + } + + buf := make([]byte, dec.bytesused) + if cositedYUV { + YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) + } else { + copy(buf, d.bufs[dec.index][:dec.bytesused]) + } + + enc := v4l2_buffer{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + index: dec.index, + } + if err := ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&enc)); err != nil { + return nil, err + } + + return buf, nil +} + +func (d *Device) Close() error { + return syscall.Close(d.fd) +} + +func ioctl(fd int, req uint, arg unsafe.Pointer) error { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) + if err != 0 { + return err + } + return nil +} + +func str(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + return string(b[:i]) + } + return string(b) +} diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go new file mode 100644 index 000000000..94d125042 --- /dev/null +++ b/pkg/v4l2/device/formats.go @@ -0,0 +1,40 @@ +package device + +const ( + V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 +) + +type Format struct { + FourCC uint32 + Name string + FFmpeg string +} + +var Formats = []Format{ + {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, +} + +// YUYV2YUV convert [Y0 Cb Y1 Cr] to cosited [Y0Y1... Cb... Cr...] +func YUYV2YUV(dst, src []byte) { + n := len(src) + i0 := 0 + iy := 0 + iu := n / 2 + iv := n / 4 * 3 + for i0 < n { + dst[iy] = src[i0] + i0++ + iy++ + dst[iu] = src[i0] + i0++ + iu++ + dst[iy] = src[i0] + i0++ + iy++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/device/videodev2_test.go b/pkg/v4l2/device/videodev2_test.go new file mode 100644 index 000000000..2556feefb --- /dev/null +++ b/pkg/v4l2/device/videodev2_test.go @@ -0,0 +1,34 @@ +package device + +import ( + "runtime" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +func TestSize(t *testing.T) { + switch runtime.GOARCH { + case "amd64", "arm64": + require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) + require.Equal(t, 208, int(unsafe.Sizeof(v4l2_format{}))) + require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) + require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) + require.Equal(t, 88, int(unsafe.Sizeof(v4l2_buffer{}))) + require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) + require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) + require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) + require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) + case "386", "arm": + require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) + require.Equal(t, 204, int(unsafe.Sizeof(v4l2_format{}))) + require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) + require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) + require.Equal(t, 68, int(unsafe.Sizeof(v4l2_buffer{}))) + require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) + require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) + require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) + require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) + } +} diff --git a/pkg/v4l2/device/videodev2_x32.go b/pkg/v4l2/device/videodev2_x32.go new file mode 100644 index 000000000..4c4db26d2 --- /dev/null +++ b/pkg/v4l2/device/videodev2_x32.go @@ -0,0 +1,152 @@ +//go:build 386 || arm + +package device + +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0445609 + + VIDIOC_QBUF = 0xc044560f + VIDIOC_DQBUF = 0xc0445611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { + driver [16]byte + card [32]byte + bus_info [32]byte + version uint32 + capabilities uint32 + device_caps uint32 + reserved [3]uint32 +} + +type v4l2_format struct { + typ uint32 + fmt v4l2_pix_format +} + +type v4l2_pix_format struct { + width uint32 // 0 + height uint32 // 4 + pixelformat uint32 // 8 + field uint32 // 12 + bytesperline uint32 // 16 + sizeimage uint32 // 20 + colorspace uint32 // 24 + priv uint32 // 28 + flags uint32 // 32 + ycbcr_enc uint32 // 36 + quantization uint32 // 40 + xfer_func uint32 // 44 + + _ [152]byte // 48 +} + +type v4l2_streamparm struct { + typ uint32 + capture v4l2_captureparm +} + +type v4l2_captureparm struct { + capability uint32 // 0 + capturemode uint32 // 4 + timeperframe v4l2_fract // 8 + extendedmode uint32 // 16 + readbuffers uint32 // 20 + + _ [176]byte // 24 +} + +type v4l2_fract struct { + numerator uint32 + denominator uint32 +} + +type v4l2_requestbuffers struct { + count uint32 + typ uint32 + memory uint32 + capabilities uint32 + flags uint8 + reserved [3]uint8 +} + +type v4l2_buffer struct { + index uint32 // 0 + typ uint32 // 4 + bytesused uint32 // 8 + flags uint32 // 12 + field uint32 // 16 + _ [8]byte // 20 + timecode v4l2_timecode // 28 + sequence uint32 // 44 + memory uint32 // 48 + offset uint32 // 52 + length uint32 // 56 + _ [8]byte // 60 +} + +type v4l2_timecode struct { + typ uint32 + flags uint32 + frames uint8 + seconds uint8 + minutes uint8 + hours uint8 + userbits [4]uint8 +} + +type v4l2_fmtdesc struct { + index uint32 + typ uint32 + flags uint32 + description [32]byte + pixelformat uint32 + mbus_code uint32 + reserved [3]uint32 +} + +type v4l2_frmsizeenum struct { + index uint32 // 0 + pixel_format uint32 // 4 + typ uint32 // 8 + discrete v4l2_frmsize_discrete // 12 + _ [24]byte +} + +type v4l2_frmsize_discrete struct { + width uint32 + height uint32 +} + +type v4l2_frmivalenum struct { + index uint32 + pixel_format uint32 + width uint32 + height uint32 + typ uint32 + discrete v4l2_fract + _ [24]byte +} diff --git a/pkg/v4l2/device/videodev2_x64.go b/pkg/v4l2/device/videodev2_x64.go new file mode 100644 index 000000000..97c3ab956 --- /dev/null +++ b/pkg/v4l2/device/videodev2_x64.go @@ -0,0 +1,153 @@ +//go:build amd64 || arm64 + +package device + +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0d05604 + VIDIOC_S_FMT = 0xc0d05605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0585609 + + VIDIOC_QBUF = 0xc058560f + VIDIOC_DQBUF = 0xc0585611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { + driver [16]byte + card [32]byte + bus_info [32]byte + version uint32 + capabilities uint32 + device_caps uint32 + reserved [3]uint32 +} + +type v4l2_format struct { + typ uint64 + fmt v4l2_pix_format +} + +type v4l2_pix_format struct { + width uint32 // 0 + height uint32 // 4 + pixelformat uint32 // 8 + field uint32 // 12 + bytesperline uint32 // 16 + sizeimage uint32 // 20 + colorspace uint32 // 24 + priv uint32 // 28 + flags uint32 // 32 + ycbcr_enc uint32 // 36 + quantization uint32 // 40 + xfer_func uint32 // 44 + + _ [152]byte // 48 +} + +type v4l2_streamparm struct { + typ uint32 + capture v4l2_captureparm +} + +type v4l2_captureparm struct { + capability uint32 // 0 + capturemode uint32 // 4 + timeperframe v4l2_fract // 8 + extendedmode uint32 // 16 + readbuffers uint32 // 20 + + _ [176]byte // 24 +} + +type v4l2_fract struct { + numerator uint32 + denominator uint32 +} + +type v4l2_requestbuffers struct { + count uint32 + typ uint32 + memory uint32 + capabilities uint32 + flags uint8 + reserved [3]uint8 +} + +type v4l2_buffer struct { + index uint32 // 0 + typ uint32 // 4 + bytesused uint32 // 8 + flags uint32 // 12 + field uint32 // 16 + _ [20]byte // 20 + timecode v4l2_timecode // 40 + sequence uint32 // 56 + memory uint32 // 60 + offset uint32 // 64 + _ [4]byte // 68 + length uint32 // 72 + _ [12]byte // 76 +} + +type v4l2_timecode struct { + typ uint32 + flags uint32 + frames uint8 + seconds uint8 + minutes uint8 + hours uint8 + userbits [4]uint8 +} + +type v4l2_fmtdesc struct { + index uint32 + typ uint32 + flags uint32 + description [32]byte + pixelformat uint32 + mbus_code uint32 + reserved [3]uint32 +} + +type v4l2_frmsizeenum struct { + index uint32 // 0 + pixel_format uint32 // 4 + typ uint32 // 8 + discrete v4l2_frmsize_discrete // 12 + _ [24]byte +} + +type v4l2_frmsize_discrete struct { + width uint32 + height uint32 +} + +type v4l2_frmivalenum struct { + index uint32 + pixel_format uint32 + width uint32 + height uint32 + typ uint32 + discrete v4l2_fract + _ [24]byte +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go new file mode 100644 index 000000000..644c5ee54 --- /dev/null +++ b/pkg/v4l2/producer.go @@ -0,0 +1,115 @@ +//go:build linux + +package v4l2 + +import ( + "errors" + "net/url" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/v4l2/device" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + dev *device.Device +} + +func Open(rawURL string) (*Producer, error) { + // Example (ffmpeg source compatible): + // v4l2:device?video=/dev/video0&input_format=mjpeg&video_size=1280x720 + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + + dev, err := device.Open(query.Get("video")) + if err != nil { + return nil, err + } + + codec := &core.Codec{ + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + + var width, height, pixFmt uint32 + + if wh := strings.Split(query.Get("video_size"), "x"); len(wh) == 2 { + codec.FmtpLine = "width=" + wh[0] + ";height=" + wh[1] + width = uint32(core.Atoi(wh[0])) + height = uint32(core.Atoi(wh[1])) + } + + switch query.Get("input_format") { + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "yuyv422": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=422" + pixFmt = device.V4L2_PIX_FMT_YUYV + default: + return nil, errors.New("v4l2: invalid input_format") + } + + if err = dev.SetFormat(width, height, pixFmt); err != nil { + return nil, err + } + + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, + }, + } + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "v4l2", + Medias: medias, + }, + dev: dev, + }, nil +} + +func (c *Producer) Start() error { + if err := c.dev.StreamOn(); err != nil { + return err + } + + cositedYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + + for { + buf, err := c.dev.Capture(cositedYUV) + if err != nil { + return err + } + + c.Recv += len(buf) + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: buf, + } + c.Receivers[0].WriteRTP(pkt) + } +} + +func (c *Producer) Stop() error { + _ = c.Connection.Stop() + return errors.Join(c.dev.StreamOff(), c.dev.Close()) +} diff --git a/www/add.html b/www/add.html index 4b40f431d..49e954d35 100644 --- a/www/add.html +++ b/www/add.html @@ -292,6 +292,18 @@ + +
+
+
+ + +
From 33e0ccdd109d3073b1fd05b00621f6955ea08845 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 00:19:53 +0300 Subject: [PATCH 16/34] Fix build for mipsle --- internal/v4l2/v4l2.go | 2 +- internal/v4l2/v4l2_linux.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go index 9cef99a59..cfef96672 100644 --- a/internal/v4l2/v4l2.go +++ b/internal/v4l2/v4l2.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !(linux && (386 || arm || amd64 || arm64)) package v4l2 diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index 4a54e1e11..4c67235d5 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -1,3 +1,5 @@ +//go:build linux && (386 || arm || amd64 || arm64) + package v4l2 import ( From e4b8d1807dee455122d2953a2f0e98de41d86bfc Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 20:08:29 +0300 Subject: [PATCH 17/34] Add support snapshot for raw image format --- pkg/magic/keyframe.go | 10 ++++++++++ pkg/mjpeg/consumer.go | 2 +- pkg/mjpeg/helpers.go | 9 ++++++++- pkg/y4m/y4m.go | 24 ++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pkg/magic/keyframe.go b/pkg/magic/keyframe.go index 8f70eec6e..9b6ef5620 100644 --- a/pkg/magic/keyframe.go +++ b/pkg/magic/keyframe.go @@ -24,6 +24,7 @@ func NewKeyframe() *Keyframe { Direction: core.DirectionSendonly, Codecs: []*core.Codec{ {Name: core.CodecJPEG}, + {Name: core.CodecRAW}, {Name: core.CodecH264}, {Name: core.CodecH265}, }, @@ -87,6 +88,15 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv if track.Codec.IsRTP() { sender.Handler = mjpeg.RTPDepay(sender.Handler) } + + case core.CodecRAW: + sender.Handler = func(packet *rtp.Packet) { + if n, err := k.wr.Write(packet.Payload); err == nil { + k.Send += n + } + } + + sender.Handler = mjpeg.Encoder(track.Codec, 5, sender.Handler) } sender.HandleRTP(track) diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go index 16edc8950..819c558ad 100644 --- a/pkg/mjpeg/consumer.go +++ b/pkg/mjpeg/consumer.go @@ -46,7 +46,7 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv if track.Codec.IsRTP() { sender.Handler = RTPDepay(sender.Handler) } else if track.Codec.Name == core.CodecRAW { - sender.Handler = Encoder(track.Codec, sender.Handler) + sender.Handler = Encoder(track.Codec, 0, sender.Handler) } sender.HandleRTP(track) diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go index d1acbd453..87f59e077 100644 --- a/pkg/mjpeg/helpers.go +++ b/pkg/mjpeg/helpers.go @@ -52,12 +52,19 @@ func FixJPEG(b []byte) []byte { return buf.Bytes() } -func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { +// Encoder convert YUV frame to Img. +// Support skipping empty frames, for example if USB cam needs time to start. +func Encoder(codec *core.Codec, skipEmpty int, handler core.HandlerFunc) core.HandlerFunc { newImage := y4m.NewImage(codec.FmtpLine) return func(packet *rtp.Packet) { img := newImage(packet.Payload) + if skipEmpty != 0 && y4m.HasSameColor(img) { + skipEmpty-- + return + } + buf := bytes.NewBuffer(nil) if err := jpeg.Encode(buf, img, nil); err != nil { return diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go index 4ac54da6a..24c431649 100644 --- a/pkg/y4m/y4m.go +++ b/pkg/y4m/y4m.go @@ -123,3 +123,27 @@ func NewImage(fmtp string) func(frame []byte) image.Image { return nil } + +// HasSameColor checks if all pixels has same color +func HasSameColor(img image.Image) bool { + var pix []byte + + switch img := img.(type) { + case *image.Gray: + pix = img.Pix + case *image.YCbCr: + pix = img.Y + } + + if len(pix) == 0 { + return false + } + + i0 := pix[0] + for _, i := range pix { + if i != i0 { + return false + } + } + return true +} From 93252fc5d271cae590ac902cfbd6185ddcd4687a Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 22:17:35 +0300 Subject: [PATCH 18/34] Change ListSizes function for V4L2 device --- internal/v4l2/v4l2_linux.go | 6 +++--- pkg/v4l2/device/device.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index 4c67235d5..a16779f45 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -58,9 +58,9 @@ func apiV4L2(w http.ResponseWriter, r *http.Request) { if source.Name != "" { sizes, _ := dev.ListSizes(fourCC) - for i := 0; i < len(sizes); i += 2 { - size := fmt.Sprintf("%dx%d", sizes[i], sizes[i+1]) - if i > 0 { + for _, wh := range sizes { + size := fmt.Sprintf("%dx%d", wh[0], wh[1]) + if source.Info != "" { source.Info += " " + size } else { source.Info = size diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 4e4d6ade2..7377aef6e 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -66,8 +66,8 @@ func (d *Device) ListFormats() ([]uint32, error) { return items, nil } -func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) { - var items []uint32 +func (d *Device) ListSizes(pixFmt uint32) ([][2]uint32, error) { + var items [][2]uint32 for i := uint32(0); ; i++ { fs := v4l2_frmsizeenum{ @@ -85,7 +85,7 @@ func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) { continue } - items = append(items, fs.discrete.width, fs.discrete.height) + items = append(items, [2]uint32{fs.discrete.width, fs.discrete.height}) } return items, nil From 59161c663b9c6d3beab9f81202782c606c1d1459 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 22:18:09 +0300 Subject: [PATCH 19/34] Add support framerate param for v4l2 source --- pkg/v4l2/producer.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 644c5ee54..b979d3460 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -65,6 +65,12 @@ func Open(rawURL string) (*Producer, error) { return nil, err } + if fps := core.Atoi(query.Get("framerate")); fps > 0 { + if err = dev.SetParam(uint32(fps)); err != nil { + return nil, err + } + } + medias := []*core.Media{ { Kind: core.KindVideo, From 7e0a163f120c98051b35b2f32690a323cb1aeb82 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 9 Jan 2025 00:20:48 +0300 Subject: [PATCH 20/34] Add support mips arch for v4l2 source --- internal/v4l2/v4l2.go | 2 +- internal/v4l2/v4l2_linux.go | 2 - pkg/v4l2/device/README.md | 19 +++ pkg/v4l2/device/arch.c | 163 ++++++++++++++++++++++++ pkg/v4l2/device/device.go | 2 +- pkg/v4l2/device/videodev2_386.go | 149 ++++++++++++++++++++++ pkg/v4l2/device/videodev2_arm.go | 149 ++++++++++++++++++++++ pkg/v4l2/device/videodev2_mipsle.go | 149 ++++++++++++++++++++++ pkg/v4l2/device/videodev2_test.go | 34 ----- pkg/v4l2/device/videodev2_x32.go | 152 ---------------------- pkg/v4l2/device/videodev2_x64.go | 190 ++++++++++++++-------------- 11 files changed, 725 insertions(+), 286 deletions(-) create mode 100644 pkg/v4l2/device/README.md create mode 100644 pkg/v4l2/device/arch.c create mode 100644 pkg/v4l2/device/videodev2_386.go create mode 100644 pkg/v4l2/device/videodev2_arm.go create mode 100644 pkg/v4l2/device/videodev2_mipsle.go delete mode 100644 pkg/v4l2/device/videodev2_test.go delete mode 100644 pkg/v4l2/device/videodev2_x32.go diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go index cfef96672..9cef99a59 100644 --- a/internal/v4l2/v4l2.go +++ b/internal/v4l2/v4l2.go @@ -1,4 +1,4 @@ -//go:build !(linux && (386 || arm || amd64 || arm64)) +//go:build !linux package v4l2 diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index a16779f45..b9cc82a05 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -1,5 +1,3 @@ -//go:build linux && (386 || arm || amd64 || arm64) - package v4l2 import ( diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md new file mode 100644 index 000000000..a816b23ed --- /dev/null +++ b/pkg/v4l2/device/README.md @@ -0,0 +1,19 @@ +Build on Ubuntu + +```bash +sudo apt install gcc-x86-64-linux-gnu +sudo apt install gcc-i686-linux-gnu +sudo apt install gcc-aarch64-linux-gnu binutils +sudo apt install gcc-arm-linux-gnueabihf +sudo apt install gcc-mipsel-linux-gnu + +x86_64-linux-gnu-gcc -w -static arch.c -o arch_x86_64 +i686-linux-gnu-gcc -w -static arch.c -o arch_i686 +aarch64-linux-gnu-gcc -w -static arch.c -o arch_aarch64 +arm-linux-gnueabihf-gcc -w -static arch.c -o arch_armhf +mipsel-linux-gnu-gcc -static arch.c -o arch_mipsel +``` + +## Useful links + +- https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h diff --git a/pkg/v4l2/device/arch.c b/pkg/v4l2/device/arch.c new file mode 100644 index 000000000..0b1195841 --- /dev/null +++ b/pkg/v4l2/device/arch.c @@ -0,0 +1,163 @@ +#include +#include +#include + +#define printconst1(con) printf("\t%s = 0x%08lx\n", #con, con) +#define printconst2(con) printf("\t%s = %d\n", #con, con) +#define printstruct(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str)) +#define printmember(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem)) +#define printunimem(str, uni, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem, typ, offsetof(struct str, uni.mem), sizeof((struct str){0}.uni.mem)) +#define printalign1(str, mem2, mem1, siz1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - siz1) +#define printfiller(str, mem1, siz1) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem1) - siz1) + +int main() { + printf("const (\n"); + printconst1(VIDIOC_QUERYCAP); + printconst1(VIDIOC_ENUM_FMT); + printconst1(VIDIOC_G_FMT); + printconst1(VIDIOC_S_FMT); + printconst1(VIDIOC_REQBUFS); + printconst1(VIDIOC_QUERYBUF); + printf("\n"); + printconst1(VIDIOC_QBUF); + printconst1(VIDIOC_DQBUF); + printconst1(VIDIOC_STREAMON); + printconst1(VIDIOC_STREAMOFF); + printconst1(VIDIOC_G_PARM); + printconst1(VIDIOC_S_PARM); + printf("\n"); + printconst1(VIDIOC_ENUM_FRAMESIZES); + printconst1(VIDIOC_ENUM_FRAMEINTERVALS); + printf(")\n\n"); + + printf("const (\n"); + printconst2(V4L2_BUF_TYPE_VIDEO_CAPTURE); + printconst2(V4L2_COLORSPACE_DEFAULT); + printconst2(V4L2_FIELD_NONE); + printconst2(V4L2_FRMIVAL_TYPE_DISCRETE); + printconst2(V4L2_FRMSIZE_TYPE_DISCRETE); + printconst2(V4L2_MEMORY_MMAP); + printf(")\n\n"); + + printstruct(v4l2_capability); + printmember(v4l2_capability, driver, "[16]byte"); + printmember(v4l2_capability, card, "[32]byte"); + printmember(v4l2_capability, bus_info, "[32]byte"); + printmember(v4l2_capability, version, "uint32"); + printmember(v4l2_capability, capabilities, "uint32"); + printmember(v4l2_capability, device_caps, "uint32"); + printmember(v4l2_capability, reserved, "[3]uint32"); + printf("}\n\n"); + + printstruct(v4l2_format); + printmember(v4l2_format, type, "uint32"); + printalign1(v4l2_format, fmt, type, 4); + printunimem(v4l2_format, fmt, pix, "v4l2_pix_format"); + printfiller(v4l2_format, fmt, sizeof(struct v4l2_pix_format)); + printf("}\n\n"); + + printstruct(v4l2_pix_format); + printmember(v4l2_pix_format, width, "uint32"); + printmember(v4l2_pix_format, height, "uint32"); + printmember(v4l2_pix_format, pixelformat, "uint32"); + printmember(v4l2_pix_format, field, "uint32"); + printmember(v4l2_pix_format, bytesperline, "uint32"); + printmember(v4l2_pix_format, sizeimage, "uint32"); + printmember(v4l2_pix_format, colorspace, "uint32"); + printmember(v4l2_pix_format, priv, "uint32"); + printmember(v4l2_pix_format, flags, "uint32"); + printmember(v4l2_pix_format, ycbcr_enc, "uint32"); + printmember(v4l2_pix_format, quantization, "uint32"); + printmember(v4l2_pix_format, xfer_func, "uint32"); + printf("}\n\n"); + + printstruct(v4l2_streamparm); + printmember(v4l2_streamparm, type, "uint32"); + printunimem(v4l2_streamparm, parm, capture, "v4l2_captureparm"); + printfiller(v4l2_streamparm, parm, sizeof(struct v4l2_captureparm)); + printf("}\n\n"); + + printstruct(v4l2_captureparm); + printmember(v4l2_captureparm, capability, "uint32"); + printmember(v4l2_captureparm, capturemode, "uint32"); + printmember(v4l2_captureparm, timeperframe, "v4l2_fract"); + printmember(v4l2_captureparm, extendedmode, "uint32"); + printmember(v4l2_captureparm, readbuffers, "uint32"); + printmember(v4l2_captureparm, reserved, "[4]uint32"); + printf("}\n\n"); + + printstruct(v4l2_fract); + printmember(v4l2_fract, numerator, "uint32"); + printmember(v4l2_fract, denominator, "uint32"); + printf("}\n\n"); + + printstruct(v4l2_requestbuffers); + printmember(v4l2_requestbuffers, count, "uint32"); + printmember(v4l2_requestbuffers, type, "uint32"); + printmember(v4l2_requestbuffers, memory, "uint32"); + printmember(v4l2_requestbuffers, capabilities, "uint32"); + printmember(v4l2_requestbuffers, flags, "uint8"); + printmember(v4l2_requestbuffers, reserved, "[3]uint8"); + printf("}\n\n"); + + printstruct(v4l2_buffer); + printmember(v4l2_buffer, index, "uint32"); + printmember(v4l2_buffer, type, "uint32"); + printmember(v4l2_buffer, bytesused, "uint32"); + printmember(v4l2_buffer, flags, "uint32"); + printmember(v4l2_buffer, field, "uint32"); + printalign1(v4l2_buffer, timecode, field, 4); + printmember(v4l2_buffer, timecode, "v4l2_timecode"); + printmember(v4l2_buffer, sequence, "uint32"); + printmember(v4l2_buffer, memory, "uint32"); + printunimem(v4l2_buffer, m, offset, "uint32"); + printalign1(v4l2_buffer, length, m, 4); + printmember(v4l2_buffer, length, "uint32"); + printfiller(v4l2_buffer, length, 4); + printf("}\n\n"); + + printstruct(v4l2_timecode); + printmember(v4l2_timecode, type, "uint32"); + printmember(v4l2_timecode, flags, "uint32"); + printmember(v4l2_timecode, frames, "uint8"); + printmember(v4l2_timecode, seconds, "uint8"); + printmember(v4l2_timecode, minutes, "uint8"); + printmember(v4l2_timecode, hours, "uint8"); + printmember(v4l2_timecode, userbits, "[4]uint8"); + printf("}\n\n"); + + printstruct(v4l2_fmtdesc); + printmember(v4l2_fmtdesc, index, "uint32"); + printmember(v4l2_fmtdesc, type, "uint32"); + printmember(v4l2_fmtdesc, flags, "uint32"); + printmember(v4l2_fmtdesc, description, "[32]byte"); + printmember(v4l2_fmtdesc, pixelformat, "uint32"); + printmember(v4l2_fmtdesc, mbus_code, "uint32"); + printmember(v4l2_fmtdesc, reserved, "[3]uint32"); + printf("}\n\n"); + + printstruct(v4l2_frmsizeenum); + printmember(v4l2_frmsizeenum, index, "uint32"); + printmember(v4l2_frmsizeenum, pixel_format, "uint32"); + printmember(v4l2_frmsizeenum, type, "uint32"); + printmember(v4l2_frmsizeenum, discrete, "v4l2_frmsize_discrete"); + printfiller(v4l2_frmsizeenum, discrete, sizeof(struct v4l2_frmsize_discrete)); + printf("}\n\n"); + + printstruct(v4l2_frmsize_discrete); + printmember(v4l2_frmsize_discrete, width, "uint32"); + printmember(v4l2_frmsize_discrete, height, "uint32"); + printf("}\n\n"); + + printstruct(v4l2_frmivalenum); + printmember(v4l2_frmivalenum, index, "uint32"); + printmember(v4l2_frmivalenum, pixel_format, "uint32"); + printmember(v4l2_frmivalenum, width, "uint32"); + printmember(v4l2_frmivalenum, height, "uint32"); + printmember(v4l2_frmivalenum, type, "uint32"); + printmember(v4l2_frmivalenum, discrete, "v4l2_fract"); + printfiller(v4l2_frmivalenum, discrete, sizeof(struct v4l2_fract)); + printf("}\n\n"); + + return 0; +} \ No newline at end of file diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7377aef6e..27bbf3508 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -121,7 +121,7 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) func (d *Device) SetFormat(width, height, pixFmt uint32) error { f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, - fmt: v4l2_pix_format{ + pix: v4l2_pix_format{ width: width, height: height, pixelformat: pixFmt, diff --git a/pkg/v4l2/device/videodev2_386.go b/pkg/v4l2/device/videodev2_386.go new file mode 100644 index 000000000..8737ca9dd --- /dev/null +++ b/pkg/v4l2/device/videodev2_386.go @@ -0,0 +1,149 @@ +package device + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0445609 + + VIDIOC_QBUF = 0xc044560f + VIDIOC_DQBUF = 0xc0445611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 +} + +type v4l2_format struct { // size 204 + typ uint32 // offset 0, size 4 + _ [0]byte // align + pix v4l2_pix_format // offset 4, size 48 + _ [152]byte // filler +} + +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 +} + +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler +} + +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 +} + +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 +} + +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 +} + +type v4l2_buffer struct { // size 68 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [8]byte // align + timecode v4l2_timecode // offset 28, size 16 + sequence uint32 // offset 44, size 4 + memory uint32 // offset 48, size 4 + offset uint32 // offset 52, size 4 + _ [0]byte // align + length uint32 // offset 56, size 4 + _ [8]byte // filler +} + +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 +} + +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 +} + +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler +} + +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 +} + +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler +} diff --git a/pkg/v4l2/device/videodev2_arm.go b/pkg/v4l2/device/videodev2_arm.go new file mode 100644 index 000000000..098ca5a35 --- /dev/null +++ b/pkg/v4l2/device/videodev2_arm.go @@ -0,0 +1,149 @@ +package device + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0505609 + + VIDIOC_QBUF = 0xc050560f + VIDIOC_DQBUF = 0xc0505611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 +} + +type v4l2_format struct { // size 204 + typ uint32 // offset 0, size 4 + _ [0]byte // align + pix v4l2_pix_format // offset 4, size 48 + _ [152]byte // filler +} + +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 +} + +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler +} + +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 +} + +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 +} + +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 +} + +type v4l2_buffer struct { // size 80 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [20]byte // align + timecode v4l2_timecode // offset 40, size 16 + sequence uint32 // offset 56, size 4 + memory uint32 // offset 60, size 4 + offset uint32 // offset 64, size 4 + _ [0]byte // align + length uint32 // offset 68, size 4 + _ [8]byte // filler +} + +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 +} + +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 +} + +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler +} + +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 +} + +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler +} diff --git a/pkg/v4l2/device/videodev2_mipsle.go b/pkg/v4l2/device/videodev2_mipsle.go new file mode 100644 index 000000000..19e8164f4 --- /dev/null +++ b/pkg/v4l2/device/videodev2_mipsle.go @@ -0,0 +1,149 @@ +package device + +const ( + VIDIOC_QUERYCAP = 0x40685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0505609 + + VIDIOC_QBUF = 0xc050560f + VIDIOC_DQBUF = 0xc0505611 + VIDIOC_STREAMON = 0x80045612 + VIDIOC_STREAMOFF = 0x80045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 +} + +type v4l2_format struct { // size 204 + typ uint32 // offset 0, size 4 + _ [0]byte // align + pix v4l2_pix_format // offset 4, size 48 + _ [152]byte // filler +} + +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 +} + +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler +} + +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 +} + +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 +} + +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 +} + +type v4l2_buffer struct { // size 80 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [20]byte // align + timecode v4l2_timecode // offset 40, size 16 + sequence uint32 // offset 56, size 4 + memory uint32 // offset 60, size 4 + offset uint32 // offset 64, size 4 + _ [0]byte // align + length uint32 // offset 68, size 4 + _ [8]byte // filler +} + +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 +} + +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 +} + +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler +} + +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 +} + +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler +} diff --git a/pkg/v4l2/device/videodev2_test.go b/pkg/v4l2/device/videodev2_test.go deleted file mode 100644 index 2556feefb..000000000 --- a/pkg/v4l2/device/videodev2_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package device - -import ( - "runtime" - "testing" - "unsafe" - - "github.com/stretchr/testify/require" -) - -func TestSize(t *testing.T) { - switch runtime.GOARCH { - case "amd64", "arm64": - require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) - require.Equal(t, 208, int(unsafe.Sizeof(v4l2_format{}))) - require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) - require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) - require.Equal(t, 88, int(unsafe.Sizeof(v4l2_buffer{}))) - require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) - require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) - require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) - require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) - case "386", "arm": - require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) - require.Equal(t, 204, int(unsafe.Sizeof(v4l2_format{}))) - require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) - require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) - require.Equal(t, 68, int(unsafe.Sizeof(v4l2_buffer{}))) - require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) - require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) - require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) - require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) - } -} diff --git a/pkg/v4l2/device/videodev2_x32.go b/pkg/v4l2/device/videodev2_x32.go deleted file mode 100644 index 4c4db26d2..000000000 --- a/pkg/v4l2/device/videodev2_x32.go +++ /dev/null @@ -1,152 +0,0 @@ -//go:build 386 || arm - -package device - -// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h - -const ( - VIDIOC_QUERYCAP = 0x80685600 - VIDIOC_ENUM_FMT = 0xc0405602 - VIDIOC_G_FMT = 0xc0cc5604 - VIDIOC_S_FMT = 0xc0cc5605 - VIDIOC_REQBUFS = 0xc0145608 - VIDIOC_QUERYBUF = 0xc0445609 - - VIDIOC_QBUF = 0xc044560f - VIDIOC_DQBUF = 0xc0445611 - VIDIOC_STREAMON = 0x40045612 - VIDIOC_STREAMOFF = 0x40045613 - VIDIOC_G_PARM = 0xc0cc5615 - VIDIOC_S_PARM = 0xc0cc5616 - - VIDIOC_ENUM_FRAMESIZES = 0xc02c564a - VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b -) - -const ( - V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 - V4L2_COLORSPACE_DEFAULT = 0 - V4L2_FIELD_NONE = 1 - V4L2_FRMIVAL_TYPE_DISCRETE = 1 - V4L2_FRMSIZE_TYPE_DISCRETE = 1 - V4L2_MEMORY_MMAP = 1 -) - -type v4l2_capability struct { - driver [16]byte - card [32]byte - bus_info [32]byte - version uint32 - capabilities uint32 - device_caps uint32 - reserved [3]uint32 -} - -type v4l2_format struct { - typ uint32 - fmt v4l2_pix_format -} - -type v4l2_pix_format struct { - width uint32 // 0 - height uint32 // 4 - pixelformat uint32 // 8 - field uint32 // 12 - bytesperline uint32 // 16 - sizeimage uint32 // 20 - colorspace uint32 // 24 - priv uint32 // 28 - flags uint32 // 32 - ycbcr_enc uint32 // 36 - quantization uint32 // 40 - xfer_func uint32 // 44 - - _ [152]byte // 48 -} - -type v4l2_streamparm struct { - typ uint32 - capture v4l2_captureparm -} - -type v4l2_captureparm struct { - capability uint32 // 0 - capturemode uint32 // 4 - timeperframe v4l2_fract // 8 - extendedmode uint32 // 16 - readbuffers uint32 // 20 - - _ [176]byte // 24 -} - -type v4l2_fract struct { - numerator uint32 - denominator uint32 -} - -type v4l2_requestbuffers struct { - count uint32 - typ uint32 - memory uint32 - capabilities uint32 - flags uint8 - reserved [3]uint8 -} - -type v4l2_buffer struct { - index uint32 // 0 - typ uint32 // 4 - bytesused uint32 // 8 - flags uint32 // 12 - field uint32 // 16 - _ [8]byte // 20 - timecode v4l2_timecode // 28 - sequence uint32 // 44 - memory uint32 // 48 - offset uint32 // 52 - length uint32 // 56 - _ [8]byte // 60 -} - -type v4l2_timecode struct { - typ uint32 - flags uint32 - frames uint8 - seconds uint8 - minutes uint8 - hours uint8 - userbits [4]uint8 -} - -type v4l2_fmtdesc struct { - index uint32 - typ uint32 - flags uint32 - description [32]byte - pixelformat uint32 - mbus_code uint32 - reserved [3]uint32 -} - -type v4l2_frmsizeenum struct { - index uint32 // 0 - pixel_format uint32 // 4 - typ uint32 // 8 - discrete v4l2_frmsize_discrete // 12 - _ [24]byte -} - -type v4l2_frmsize_discrete struct { - width uint32 - height uint32 -} - -type v4l2_frmivalenum struct { - index uint32 - pixel_format uint32 - width uint32 - height uint32 - typ uint32 - discrete v4l2_fract - _ [24]byte -} diff --git a/pkg/v4l2/device/videodev2_x64.go b/pkg/v4l2/device/videodev2_x64.go index 97c3ab956..6e1018e00 100644 --- a/pkg/v4l2/device/videodev2_x64.go +++ b/pkg/v4l2/device/videodev2_x64.go @@ -2,8 +2,6 @@ package device -// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h - const ( VIDIOC_QUERYCAP = 0x80685600 VIDIOC_ENUM_FMT = 0xc0405602 @@ -32,122 +30,122 @@ const ( V4L2_MEMORY_MMAP = 1 ) -type v4l2_capability struct { - driver [16]byte - card [32]byte - bus_info [32]byte - version uint32 - capabilities uint32 - device_caps uint32 - reserved [3]uint32 +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 } -type v4l2_format struct { - typ uint64 - fmt v4l2_pix_format +type v4l2_format struct { // size 208 + typ uint32 // offset 0, size 4 + _ [4]byte // align + pix v4l2_pix_format // offset 8, size 48 + _ [152]byte // filler } -type v4l2_pix_format struct { - width uint32 // 0 - height uint32 // 4 - pixelformat uint32 // 8 - field uint32 // 12 - bytesperline uint32 // 16 - sizeimage uint32 // 20 - colorspace uint32 // 24 - priv uint32 // 28 - flags uint32 // 32 - ycbcr_enc uint32 // 36 - quantization uint32 // 40 - xfer_func uint32 // 44 - - _ [152]byte // 48 +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 } -type v4l2_streamparm struct { - typ uint32 - capture v4l2_captureparm +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler } -type v4l2_captureparm struct { - capability uint32 // 0 - capturemode uint32 // 4 - timeperframe v4l2_fract // 8 - extendedmode uint32 // 16 - readbuffers uint32 // 20 - - _ [176]byte // 24 +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 } -type v4l2_fract struct { - numerator uint32 - denominator uint32 +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 } -type v4l2_requestbuffers struct { - count uint32 - typ uint32 - memory uint32 - capabilities uint32 - flags uint8 - reserved [3]uint8 +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 } -type v4l2_buffer struct { - index uint32 // 0 - typ uint32 // 4 - bytesused uint32 // 8 - flags uint32 // 12 - field uint32 // 16 - _ [20]byte // 20 - timecode v4l2_timecode // 40 - sequence uint32 // 56 - memory uint32 // 60 - offset uint32 // 64 - _ [4]byte // 68 - length uint32 // 72 - _ [12]byte // 76 +type v4l2_buffer struct { // size 88 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [20]byte // align + timecode v4l2_timecode // offset 40, size 16 + sequence uint32 // offset 56, size 4 + memory uint32 // offset 60, size 4 + offset uint32 // offset 64, size 4 + _ [4]byte // align + length uint32 // offset 72, size 4 + _ [12]byte // filler } -type v4l2_timecode struct { - typ uint32 - flags uint32 - frames uint8 - seconds uint8 - minutes uint8 - hours uint8 - userbits [4]uint8 +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 } -type v4l2_fmtdesc struct { - index uint32 - typ uint32 - flags uint32 - description [32]byte - pixelformat uint32 - mbus_code uint32 - reserved [3]uint32 +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 } -type v4l2_frmsizeenum struct { - index uint32 // 0 - pixel_format uint32 // 4 - typ uint32 // 8 - discrete v4l2_frmsize_discrete // 12 - _ [24]byte +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler } -type v4l2_frmsize_discrete struct { - width uint32 - height uint32 +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 } -type v4l2_frmivalenum struct { - index uint32 - pixel_format uint32 - width uint32 - height uint32 - typ uint32 - discrete v4l2_fract - _ [24]byte +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler } From 879ef603fe2c4ec80ee56e0e5243f7c623db91b5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 9 Jan 2025 00:30:01 +0300 Subject: [PATCH 21/34] Update v4l2 discovery --- internal/v4l2/v4l2_linux.go | 48 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index b9cc82a05..2cd606921 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -44,32 +44,33 @@ func apiV4L2(w http.ResponseWriter, r *http.Request) { formats, _ := dev.ListFormats() for _, fourCC := range formats { - source := &api.Source{} + name, ffmpeg := findFormat(fourCC) + source := &api.Source{Name: name} - for _, format := range device.Formats { - if format.FourCC == fourCC { - source.Name = format.Name - source.URL = "v4l2:device?video=" + path + "&input_format=" + format.FFmpeg + "&video_size=" - break + sizes, _ := dev.ListSizes(fourCC) + for _, wh := range sizes { + if source.Info != "" { + source.Info += " " } - } - if source.Name != "" { - sizes, _ := dev.ListSizes(fourCC) - for _, wh := range sizes { - size := fmt.Sprintf("%dx%d", wh[0], wh[1]) - if source.Info != "" { - source.Info += " " + size - } else { - source.Info = size - source.URL += size + source.Info += fmt.Sprintf("%dx%d", wh[0], wh[1]) + + frameRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1]) + for _, fr := range frameRates { + source.Info += fmt.Sprintf("@%d", fr) + + if source.URL == "" && ffmpeg != "" { + source.URL = fmt.Sprintf( + "v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d", + path, ffmpeg, wh[0], wh[1], fr, + ) } } - } else { - source.Name = string(binary.LittleEndian.AppendUint32(nil, fourCC)) } - sources = append(sources, source) + if source.Info != "" { + sources = append(sources, source) + } } _ = dev.Close() @@ -77,3 +78,12 @@ func apiV4L2(w http.ResponseWriter, r *http.Request) { api.ResponseSources(w, sources) } + +func findFormat(fourCC uint32) (name, ffmpeg string) { + for _, format := range device.Formats { + if format.FourCC == fourCC { + return format.Name, format.FFmpeg + } + } + return string(binary.LittleEndian.AppendUint32(nil, fourCC)), "" +} From 773e415dff51a471a83e6a4e3bd227b3c445eb99 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 9 Jan 2025 07:18:36 +0300 Subject: [PATCH 22/34] Code refactoring for v4l2 device --- pkg/v4l2/device/README.md | 12 +++++++----- pkg/v4l2/device/{arch.c => videodev2_arch.c} | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) rename pkg/v4l2/device/{arch.c => videodev2_arch.c} (88%) diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md index a816b23ed..802eca93b 100644 --- a/pkg/v4l2/device/README.md +++ b/pkg/v4l2/device/README.md @@ -1,3 +1,5 @@ +# Video For Linux Two + Build on Ubuntu ```bash @@ -7,11 +9,11 @@ sudo apt install gcc-aarch64-linux-gnu binutils sudo apt install gcc-arm-linux-gnueabihf sudo apt install gcc-mipsel-linux-gnu -x86_64-linux-gnu-gcc -w -static arch.c -o arch_x86_64 -i686-linux-gnu-gcc -w -static arch.c -o arch_i686 -aarch64-linux-gnu-gcc -w -static arch.c -o arch_aarch64 -arm-linux-gnueabihf-gcc -w -static arch.c -o arch_armhf -mipsel-linux-gnu-gcc -static arch.c -o arch_mipsel +x86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64 +i686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686 +aarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64 +arm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf +mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel ``` ## Useful links diff --git a/pkg/v4l2/device/arch.c b/pkg/v4l2/device/videodev2_arch.c similarity index 88% rename from pkg/v4l2/device/arch.c rename to pkg/v4l2/device/videodev2_arch.c index 0b1195841..1053a088b 100644 --- a/pkg/v4l2/device/arch.c +++ b/pkg/v4l2/device/videodev2_arch.c @@ -7,8 +7,8 @@ #define printstruct(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str)) #define printmember(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem)) #define printunimem(str, uni, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem, typ, offsetof(struct str, uni.mem), sizeof((struct str){0}.uni.mem)) -#define printalign1(str, mem2, mem1, siz1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - siz1) -#define printfiller(str, mem1, siz1) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem1) - siz1) +#define printalign1(str, mem2, mem1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - sizeof((struct str){0}.mem1)) +#define printfiller(str, mem) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem) - sizeof((struct str){0}.mem)) int main() { printf("const (\n"); @@ -51,9 +51,9 @@ int main() { printstruct(v4l2_format); printmember(v4l2_format, type, "uint32"); - printalign1(v4l2_format, fmt, type, 4); + printalign1(v4l2_format, fmt, type); printunimem(v4l2_format, fmt, pix, "v4l2_pix_format"); - printfiller(v4l2_format, fmt, sizeof(struct v4l2_pix_format)); + printfiller(v4l2_format, fmt.pix); printf("}\n\n"); printstruct(v4l2_pix_format); @@ -74,7 +74,7 @@ int main() { printstruct(v4l2_streamparm); printmember(v4l2_streamparm, type, "uint32"); printunimem(v4l2_streamparm, parm, capture, "v4l2_captureparm"); - printfiller(v4l2_streamparm, parm, sizeof(struct v4l2_captureparm)); + printfiller(v4l2_streamparm, parm.capture); printf("}\n\n"); printstruct(v4l2_captureparm); @@ -106,14 +106,14 @@ int main() { printmember(v4l2_buffer, bytesused, "uint32"); printmember(v4l2_buffer, flags, "uint32"); printmember(v4l2_buffer, field, "uint32"); - printalign1(v4l2_buffer, timecode, field, 4); + printalign1(v4l2_buffer, timecode, field); printmember(v4l2_buffer, timecode, "v4l2_timecode"); printmember(v4l2_buffer, sequence, "uint32"); printmember(v4l2_buffer, memory, "uint32"); printunimem(v4l2_buffer, m, offset, "uint32"); - printalign1(v4l2_buffer, length, m, 4); + printalign1(v4l2_buffer, length, m.offset); printmember(v4l2_buffer, length, "uint32"); - printfiller(v4l2_buffer, length, 4); + printfiller(v4l2_buffer, length); printf("}\n\n"); printstruct(v4l2_timecode); @@ -141,7 +141,7 @@ int main() { printmember(v4l2_frmsizeenum, pixel_format, "uint32"); printmember(v4l2_frmsizeenum, type, "uint32"); printmember(v4l2_frmsizeenum, discrete, "v4l2_frmsize_discrete"); - printfiller(v4l2_frmsizeenum, discrete, sizeof(struct v4l2_frmsize_discrete)); + printfiller(v4l2_frmsizeenum, discrete); printf("}\n\n"); printstruct(v4l2_frmsize_discrete); @@ -156,7 +156,7 @@ int main() { printmember(v4l2_frmivalenum, height, "uint32"); printmember(v4l2_frmivalenum, type, "uint32"); printmember(v4l2_frmivalenum, discrete, "v4l2_fract"); - printfiller(v4l2_frmivalenum, discrete, sizeof(struct v4l2_fract)); + printfiller(v4l2_frmivalenum, discrete); printf("}\n\n"); return 0; From 0664e46a4bc45107e478619a2c1bdca215dca27a Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 12:57:37 +0300 Subject: [PATCH 23/34] Fix v4l2 source for MIPS --- pkg/v4l2/device/README.md | 2 +- pkg/v4l2/device/videodev2_mipsle.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md index 802eca93b..de686ea08 100644 --- a/pkg/v4l2/device/README.md +++ b/pkg/v4l2/device/README.md @@ -13,7 +13,7 @@ x86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64 i686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686 aarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64 arm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf -mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel +mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel -D_TIME_BITS=32 ``` ## Useful links diff --git a/pkg/v4l2/device/videodev2_mipsle.go b/pkg/v4l2/device/videodev2_mipsle.go index 19e8164f4..cecc54c4d 100644 --- a/pkg/v4l2/device/videodev2_mipsle.go +++ b/pkg/v4l2/device/videodev2_mipsle.go @@ -6,10 +6,10 @@ const ( VIDIOC_G_FMT = 0xc0cc5604 VIDIOC_S_FMT = 0xc0cc5605 VIDIOC_REQBUFS = 0xc0145608 - VIDIOC_QUERYBUF = 0xc0505609 + VIDIOC_QUERYBUF = 0xc0445609 - VIDIOC_QBUF = 0xc050560f - VIDIOC_DQBUF = 0xc0505611 + VIDIOC_QBUF = 0xc044560f + VIDIOC_DQBUF = 0xc0445611 VIDIOC_STREAMON = 0x80045612 VIDIOC_STREAMOFF = 0x80045613 VIDIOC_G_PARM = 0xc0cc5615 @@ -89,19 +89,19 @@ type v4l2_requestbuffers struct { // size 20 reserved [3]uint8 // offset 17, size 3 } -type v4l2_buffer struct { // size 80 +type v4l2_buffer struct { // size 68 index uint32 // offset 0, size 4 typ uint32 // offset 4, size 4 bytesused uint32 // offset 8, size 4 flags uint32 // offset 12, size 4 field uint32 // offset 16, size 4 - _ [20]byte // align - timecode v4l2_timecode // offset 40, size 16 - sequence uint32 // offset 56, size 4 - memory uint32 // offset 60, size 4 - offset uint32 // offset 64, size 4 + _ [8]byte // align + timecode v4l2_timecode // offset 28, size 16 + sequence uint32 // offset 44, size 4 + memory uint32 // offset 48, size 4 + offset uint32 // offset 52, size 4 _ [0]byte // align - length uint32 // offset 68, size 4 + length uint32 // offset 56, size 4 _ [8]byte // filler } From 7dc9beb1712ac1074657ca052f7df3ee1230c954 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 14:56:42 +0300 Subject: [PATCH 24/34] Add ws and ffmpeg modules to go2rtc_mjpeg --- examples/go2rtc_mjpeg/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/go2rtc_mjpeg/main.go b/examples/go2rtc_mjpeg/main.go index a3e08ff54..3c915b3c1 100644 --- a/examples/go2rtc_mjpeg/main.go +++ b/examples/go2rtc_mjpeg/main.go @@ -2,7 +2,9 @@ package main import ( "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/ffmpeg" "github.com/AlexxIT/go2rtc/internal/mjpeg" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/v4l2" @@ -14,6 +16,9 @@ func main() { streams.Init() api.Init() + ws.Init() + + ffmpeg.Init() mjpeg.Init() v4l2.Init() From 83907132b57466e9d094675a2f1c972f8e5bceb8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 15:01:01 +0300 Subject: [PATCH 25/34] Update about packed and planar YUV formats --- pkg/v4l2/device/device.go | 4 ++-- pkg/v4l2/device/formats.go | 2 +- pkg/v4l2/producer.go | 4 ++-- pkg/y4m/README.md | 14 ++++++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 27bbf3508..7f16fd238 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -196,7 +196,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(cositedYUV bool) ([]byte, error) { +func (d *Device) Capture(planarYUV bool) ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -206,7 +206,7 @@ func (d *Device) Capture(cositedYUV bool) ([]byte, error) { } buf := make([]byte, dec.bytesused) - if cositedYUV { + if planarYUV { YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) } else { copy(buf, d.bufs[dec.index][:dec.bytesused]) diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index 94d125042..fb54bbd1e 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -16,7 +16,7 @@ var Formats = []Format{ {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, } -// YUYV2YUV convert [Y0 Cb Y1 Cr] to cosited [Y0Y1... Cb... Cr...] +// YUYV2YUV convert packed YUV to planar YUV func YUYV2YUV(dst, src []byte) { n := len(src) i0 := 0 diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index b979d3460..87199762d 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -93,10 +93,10 @@ func (c *Producer) Start() error { return err } - cositedYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW for { - buf, err := c.dev.Capture(cositedYUV) + buf, err := c.dev.Capture(planarYUV) if err != nil { return err } diff --git a/pkg/y4m/README.md b/pkg/y4m/README.md index 6f4d863e0..ff97813b4 100644 --- a/pkg/y4m/README.md +++ b/pkg/y4m/README.md @@ -1,5 +1,19 @@ +## Planar YUV formats + +Packed YUV - yuyv422 - YUYV 4:2:2 +Semi-Planar - nv12 - Y/CbCr 4:2:0 +Planar YUV - yuv420p - Planar YUV 4:2:0 - aka. [cosited](https://manned.org/yuv4mpeg.5) + +``` +[video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuyv422 : YUYV 4:2:2 : 1920x1080 +[video4linux2,v4l2 @ 0x55fddc42a940] Raw : nv12 : Y/CbCr 4:2:0 : 1920x1080 +[video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuv420p : Planar YUV 4:2:0 : 1920x1080 +``` + ## Useful links - https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering - https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts - https://fourcc.org/yuv.php#YV12 +- https://docs.kernel.org/userspace-api/media/v4l/pixfmt-yuv-planar.html +- https://gist.github.com/Jim-Bar/3cbba684a71d1a9d468a6711a6eddbeb From 22e63a7367c66f579ca80f314307a3dae51b90d6 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 19:57:10 +0300 Subject: [PATCH 26/34] Fix comment about OpenIPC --- pkg/flv/producer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 7535a8a4b..33762d209 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -141,7 +141,7 @@ func (c *Producer) probe() error { // 2. MedaData without stereo key for AAC // 3. Audio header after Video keyframe tag - // OpenIPC camera sends: + // OpenIPC camera (on old firmwares) sends: // 1. Empty video/audio flag // 2. No MetaData packet // 3. Sends a video packet in more than 3 seconds From 485448cbc7c0a3d0fc25cae2b7e24fed6f2b58f2 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 12:38:45 +0100 Subject: [PATCH 27/34] initial ring implementation --- internal/ring/init.go | 47 ++++ main.go | 2 + pkg/ring/api.go | 416 +++++++++++++++++++++++++++++++ pkg/ring/client.go | 551 ++++++++++++++++++++++++++++++++++++++++++ www/add.html | 25 +- 5 files changed, 1040 insertions(+), 1 deletion(-) create mode 100644 internal/ring/init.go create mode 100644 pkg/ring/api.go create mode 100644 pkg/ring/client.go diff --git a/internal/ring/init.go b/internal/ring/init.go new file mode 100644 index 000000000..24a91ac62 --- /dev/null +++ b/internal/ring/init.go @@ -0,0 +1,47 @@ +package ring + +import ( + "net/http" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/ring" +) + +func Init() { + streams.HandleFunc("ring", func(source string) (core.Producer, error) { + return ring.Dial(source) + }) + + api.HandleFunc("api/ring", apiRing) +} + +func apiRing(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + refreshToken := query.Get("refresh_token") + + ringAPI, err := ring.NewRingRestClient(ring.RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + devices, err := ringAPI.FetchRingDevices() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var items []*api.Source + + for _, camera := range devices.AllCameras { + query.Set("device_id", camera.DeviceID) + + items = append(items, &api.Source{ + Name: camera.Description, URL: "ring:?" + query.Encode(), + }) + } + + api.ResponseSources(w, items) +} diff --git a/main.go b/main.go index db8de9f41..db3983ccb 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/nest" "github.com/AlexxIT/go2rtc/internal/ngrok" "github.com/AlexxIT/go2rtc/internal/onvif" + "github.com/AlexxIT/go2rtc/internal/ring" "github.com/AlexxIT/go2rtc/internal/roborock" "github.com/AlexxIT/go2rtc/internal/rtmp" "github.com/AlexxIT/go2rtc/internal/rtsp" @@ -80,6 +81,7 @@ func main() { mpegts.Init() // mpegts passive source roborock.Init() // roborock source homekit.Init() // homekit source + ring.Init() // ring source nest.Init() // nest source bubble.Init() // bubble source expr.Init() // expr source diff --git a/pkg/ring/api.go b/pkg/ring/api.go new file mode 100644 index 000000000..faebf6b90 --- /dev/null +++ b/pkg/ring/api.go @@ -0,0 +1,416 @@ +package ring + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "time" +) + +type RefreshTokenAuth struct { + RefreshToken string +} + +// AuthConfig represents the decoded refresh token data +type AuthConfig struct { + RT string `json:"rt"` // Refresh Token + HID string `json:"hid"` // Hardware ID +} + +// AuthTokenResponse represents the response from the authentication endpoint +type AuthTokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` // Always "client" + TokenType string `json:"token_type"` // Always "Bearer" +} + +// SocketTicketRequest represents the request to get a socket ticket +type SocketTicketResponse struct { + Ticket string `json:"ticket"` + ResponseTimestamp int64 `json:"response_timestamp"` +} + +// RingRestClient handles authentication and requests to Ring API +type RingRestClient struct { + httpClient *http.Client + authConfig *AuthConfig + hardwareID string + authToken *AuthTokenResponse + auth RefreshTokenAuth + onTokenRefresh func(string) // Callback when refresh token is updated +} + +// CameraKind represents the different types of Ring cameras +type CameraKind string + +const ( + Doorbot CameraKind = "doorbot" + Doorbell CameraKind = "doorbell" + DoorbellV3 CameraKind = "doorbell_v3" + DoorbellV4 CameraKind = "doorbell_v4" + DoorbellV5 CameraKind = "doorbell_v5" + DoorbellOyster CameraKind = "doorbell_oyster" + DoorbellPortal CameraKind = "doorbell_portal" + DoorbellScallop CameraKind = "doorbell_scallop" + DoorbellScallopLite CameraKind = "doorbell_scallop_lite" + DoorbellGraham CameraKind = "doorbell_graham_cracker" + LpdV1 CameraKind = "lpd_v1" + LpdV2 CameraKind = "lpd_v2" + LpdV4 CameraKind = "lpd_v4" + JboxV1 CameraKind = "jbox_v1" + StickupCam CameraKind = "stickup_cam" + StickupCamV3 CameraKind = "stickup_cam_v3" + StickupCamElite CameraKind = "stickup_cam_elite" + StickupCamLongfin CameraKind = "stickup_cam_longfin" + StickupCamLunar CameraKind = "stickup_cam_lunar" + SpotlightV2 CameraKind = "spotlightw_v2" + HpCamV1 CameraKind = "hp_cam_v1" + HpCamV2 CameraKind = "hp_cam_v2" + StickupCamV4 CameraKind = "stickup_cam_v4" + FloodlightV1 CameraKind = "floodlight_v1" + FloodlightV2 CameraKind = "floodlight_v2" + FloodlightPro CameraKind = "floodlight_pro" + CocoaCamera CameraKind = "cocoa_camera" + CocoaDoorbell CameraKind = "cocoa_doorbell" + CocoaFloodlight CameraKind = "cocoa_floodlight" + CocoaSpotlight CameraKind = "cocoa_spotlight" + StickupCamMini CameraKind = "stickup_cam_mini" + OnvifCamera CameraKind = "onvif_camera" +) + +// RingDeviceType represents different types of Ring devices +type RingDeviceType string + +const ( + IntercomHandsetAudio RingDeviceType = "intercom_handset_audio" + OnvifCameraType RingDeviceType = "onvif_camera" +) + +// CameraData contains common fields for all camera types +type CameraData struct { + ID float64 `json:"id"` + Description string `json:"description"` + DeviceID string `json:"device_id"` + Kind string `json:"kind"` + LocationID string `json:"location_id"` +} + +// RingDevicesResponse represents the response from the Ring API +type RingDevicesResponse struct { + Doorbots []CameraData `json:"doorbots"` + AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` + StickupCams []CameraData `json:"stickup_cams"` + AllCameras []CameraData `json:"all_cameras"` + Chimes []CameraData `json:"chimes"` + Other []map[string]interface{} `json:"other"` +} + +const ( + clientAPIBaseURL = "https://api.ring.com/clients_api/" + deviceAPIBaseURL = "https://api.ring.com/devices/v1/" + commandsAPIBaseURL = "https://api.ring.com/commands/v1/" + appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/" + oauthURL = "https://oauth.ring.com/oauth/token" + apiVersion = 11 + defaultTimeout = 20 * time.Second + maxRetries = 3 +) + +// NewRingRestClient creates a new Ring client instance +func NewRingRestClient(auth RefreshTokenAuth, onTokenRefresh func(string)) (*RingRestClient, error) { + client := &RingRestClient{ + httpClient: &http.Client{ + Timeout: defaultTimeout, + }, + auth: auth, + onTokenRefresh: onTokenRefresh, + hardwareID: generateHardwareID(), + } + + // check if refresh token is provided + if auth.RefreshToken == "" { + return nil, fmt.Errorf("refresh token is required") + } + + if config, err := parseAuthConfig(auth.RefreshToken); err == nil { + client.authConfig = config + client.hardwareID = config.HID + } + + return client, nil +} + +// Request makes an authenticated request to the Ring API +func (c *RingRestClient) Request(method, url string, body interface{}) ([]byte, error) { + // Ensure we have a valid auth token + if err := c.ensureAuth(); err != nil { + return nil, fmt.Errorf("authentication failed: %w", err) + } + + var bodyReader io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewReader(jsonBody) + } + + // Create request + req, err := http.NewRequest(method, url, bodyReader) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Authorization", "Bearer "+c.authToken.AccessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + + // Make request with retries + var resp *http.Response + var responseBody []byte + + for attempt := 0; attempt <= maxRetries; attempt++ { + resp, err = c.httpClient.Do(req) + if err != nil { + if attempt == maxRetries { + return nil, fmt.Errorf("request failed after %d retries: %w", maxRetries, err) + } + time.Sleep(5 * time.Second) + continue + } + defer resp.Body.Close() + + responseBody, err = io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Handle 401 by refreshing auth and retrying + if resp.StatusCode == http.StatusUnauthorized { + c.authToken = nil // Force token refresh + if attempt == maxRetries { + return nil, fmt.Errorf("authentication failed after %d retries", maxRetries) + } + if err := c.ensureAuth(); err != nil { + return nil, fmt.Errorf("failed to refresh authentication: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.authToken.AccessToken) + continue + } + + // Handle other error status codes + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(responseBody)) + } + + break + } + + return responseBody, nil +} + +// ensureAuth ensures we have a valid auth token +func (c *RingRestClient) ensureAuth() error { + if c.authToken != nil { + return nil + } + + var grantData = map[string]string{ + "grant_type": "refresh_token", + "refresh_token": c.authConfig.RT, + } + + // Add common fields + grantData["client_id"] = "ring_official_android" + grantData["scope"] = "client" + + // Make auth request + body, err := json.Marshal(grantData) + if err != nil { + return fmt.Errorf("failed to marshal auth request: %w", err) + } + + req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create auth request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + req.Header.Set("2fa-support", "true") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("auth request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusPreconditionFailed { + return fmt.Errorf("2FA required. Please see documentation for handling 2FA") + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var authResp AuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return fmt.Errorf("failed to decode auth response: %w", err) + } + + // Update auth config and refresh token + c.authToken = &authResp + c.authConfig = &AuthConfig{ + RT: authResp.RefreshToken, + HID: c.hardwareID, + } + + // Encode and notify about new refresh token + if c.onTokenRefresh != nil { + newRefreshToken := encodeAuthConfig(c.authConfig) + c.onTokenRefresh(newRefreshToken) + } + + return nil +} + +// Helper functions for auth config encoding/decoding +func parseAuthConfig(refreshToken string) (*AuthConfig, error) { + decoded, err := base64.StdEncoding.DecodeString(refreshToken) + if err != nil { + return nil, err + } + + var config AuthConfig + if err := json.Unmarshal(decoded, &config); err != nil { + // Handle legacy format where refresh token is the raw token + return &AuthConfig{RT: refreshToken}, nil + } + + return &config, nil +} + +func encodeAuthConfig(config *AuthConfig) string { + jsonBytes, _ := json.Marshal(config) + return base64.StdEncoding.EncodeToString(jsonBytes) +} + +// API URL helpers +func ClientAPI(path string) string { + return clientAPIBaseURL + path +} + +func DeviceAPI(path string) string { + return deviceAPIBaseURL + path +} + +func CommandsAPI(path string) string { + return commandsAPIBaseURL + path +} + +func AppAPI(path string) string { + return appAPIBaseURL + path +} + +// FetchRingDevices gets all Ring devices and categorizes them +func (c *RingRestClient) FetchRingDevices() (*RingDevicesResponse, error) { + response, err := c.Request("GET", ClientAPI("ring_devices"), nil) + if err != nil { + return nil, fmt.Errorf("failed to fetch ring devices: %w", err) + } + + var devices RingDevicesResponse + if err := json.Unmarshal(response, &devices); err != nil { + return nil, fmt.Errorf("failed to unmarshal devices response: %w", err) + } + + // Process "other" devices + var onvifCameras []CameraData + var intercoms []CameraData + + for _, device := range devices.Other { + kind, ok := device["kind"].(string) + if !ok { + continue + } + + switch RingDeviceType(kind) { + case OnvifCameraType: + var camera CameraData + if deviceJson, err := json.Marshal(device); err == nil { + if err := json.Unmarshal(deviceJson, &camera); err == nil { + onvifCameras = append(onvifCameras, camera) + } + } + case IntercomHandsetAudio: + var intercom CameraData + if deviceJson, err := json.Marshal(device); err == nil { + if err := json.Unmarshal(deviceJson, &intercom); err == nil { + intercoms = append(intercoms, intercom) + } + } + } + } + + // Combine all cameras into AllCameras slice + allCameras := make([]CameraData, 0) + allCameras = append(allCameras, interfaceSlice(devices.Doorbots)...) + allCameras = append(allCameras, interfaceSlice(devices.StickupCams)...) + allCameras = append(allCameras, interfaceSlice(devices.AuthorizedDoorbots)...) + allCameras = append(allCameras, interfaceSlice(onvifCameras)...) + allCameras = append(allCameras, interfaceSlice(intercoms)...) + + devices.AllCameras = allCameras + + return &devices, nil +} + +func (c *RingRestClient) GetSocketTicket() (*SocketTicketResponse, error) { + response, err := c.Request("POST", AppAPI("clap/ticket/request/signalsocket"), nil) + if err != nil { + return nil, fmt.Errorf("failed to fetch socket ticket: %w", err) + } + + var ticket SocketTicketResponse + if err := json.Unmarshal(response, &ticket); err != nil { + return nil, fmt.Errorf("failed to unmarshal socket ticket response: %w", err) + } + + return &ticket, nil +} + +func generateHardwareID() string { + h := sha256.New() + h.Write([]byte("ring-client-go2rtc")) + return hex.EncodeToString(h.Sum(nil)[:16]) +} + +func interfaceSlice(slice interface{}) []CameraData { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + return nil + } + + ret := make([]CameraData, s.Len()) + for i := 0; i < s.Len(); i++ { + if camera, ok := s.Index(i).Interface().(CameraData); ok { + ret[i] = camera + } + } + return ret +} \ No newline at end of file diff --git a/pkg/ring/client.go b/pkg/ring/client.go new file mode 100644 index 000000000..db8e2eaa0 --- /dev/null +++ b/pkg/ring/client.go @@ -0,0 +1,551 @@ +package ring + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" + "github.com/google/uuid" + "github.com/gorilla/websocket" + pion "github.com/pion/webrtc/v3" + "github.com/rs/zerolog/log" +) + +type Client struct { + conn *webrtc.Conn + ws *websocket.Conn + api *RingRestClient + camera *CameraData + dialogID string + sessionID string + done chan struct{} +} + +type SessionBody struct { + DoorbotID int `json:"doorbot_id"` + SessionID string `json:"session_id"` +} + +type AnswerMessage struct { + Method string `json:"method"` // "sdp" + Body struct { + SessionBody + SDP string `json:"sdp"` + Type string `json:"type"` // "answer" + } `json:"body"` +} + +type IceCandidateMessage struct { + Method string `json:"method"` // "ice" + Body struct { + SessionBody + Ice string `json:"ice"` + MLineIndex int `json:"mlineindex"` + } `json:"body"` +} + +type SessionMessage struct { + Method string `json:"method"` // "session_created" or "session_started" + Body SessionBody `json:"body"` +} + +type PongMessage struct { + Method string `json:"method"` // "pong" + Body SessionBody `json:"body"` +} + +type NotificationMessage struct { + Method string `json:"method"` // "notification" + Body struct { + SessionBody + IsOK bool `json:"is_ok"` + Text string `json:"text"` + } `json:"body"` +} + +type StreamInfoMessage struct { + Method string `json:"method"` // "stream_info" + Body struct { + SessionBody + Transcoding bool `json:"transcoding"` + TranscodingReason string `json:"transcoding_reason"` + } `json:"body"` +} + +type CloseMessage struct { + Method string `json:"method"` // "close" + Body struct { + SessionBody + Reason struct { + Code int `json:"code"` + Text string `json:"text"` + } `json:"reason"` + } `json:"body"` +} + +type BaseMessage struct { + Method string `json:"method"` + Body map[string]any `json:"body"` +} + +// Close reason codes +const ( + CloseReasonNormalClose = 0 + CloseReasonAuthenticationFailed = 5 + CloseReasonTimeout = 6 +) + +func Dial(rawURL string) (*Client, error) { + // 1. Create Ring Rest API client + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + encodedToken := query.Get("refresh_token") + deviceID := query.Get("device_id") + + if encodedToken == "" || deviceID == "" { + return nil, errors.New("ring: wrong query") + } + + // URL-decode the refresh token + refreshToken, err := url.QueryUnescape(encodedToken) + if err != nil { + return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) + } + + println("Connecting to Ring WebSocket") + println("Refresh Token: ", refreshToken) + println("Device ID: ", deviceID) + + // Initialize Ring API client + ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + return nil, err + } + + // Get camera details + devices, err := ringAPI.FetchRingDevices() + if err != nil { + return nil, err + } + + var camera *CameraData + for _, cam := range devices.AllCameras { + if fmt.Sprint(cam.DeviceID) == deviceID { + camera = &cam + break + } + } + if camera == nil { + return nil, errors.New("ring: camera not found") + } + + // 2. Connect to signaling server + ticket, err := ringAPI.GetSocketTicket() + if err != nil { + return nil, err + } + + println("WebSocket Ticket: ", ticket.Ticket) + println("WebSocket ResponseTimestamp: ", ticket.ResponseTimestamp) + + // Create WebSocket connection + wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", + uuid.NewString(), url.QueryEscape(ticket.Ticket)) + + println("WebSocket URL: ", wsURL) + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + "User-Agent": {"android:com.ringapp"}, + }) + if err != nil { + return nil, err + } + + println("WebSocket handshake completed successfully") + + // 3. Create Peer Connection + println("Creating Peer Connection") + + conf := pion.Configuration{ + ICEServers: []pion.ICEServer{ + {URLs: []string{ + "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", + "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", + "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + }}, + }, + ICETransportPolicy: pion.ICETransportPolicyAll, + BundlePolicy: pion.BundlePolicyBalanced, + } + + api, err := webrtc.NewAPI() + if err != nil { + println("Failed to create WebRTC API") + conn.Close() + return nil, err + } + + pc, err := api.NewPeerConnection(conf) + if err != nil { + println("Failed to create Peer Connection") + conn.Close() + return nil, err + } + + println("Peer Connection created") + + // protect from sending ICE candidate before Offer + var sendOffer core.Waiter + + // protect from blocking on errors + defer sendOffer.Done(nil) + + // waiter will wait PC error or WS error or nil (connection OK) + var connState core.Waiter + + prod := webrtc.NewConn(pc) + prod.FormatName = "ring/webrtc" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL + + client := &Client{ + ws: conn, + api: ringAPI, + camera: camera, + dialogID: uuid.NewString(), + conn: prod, + done: make(chan struct{}), + } + + prod.Listen(func(msg any) { + switch msg := msg.(type) { + case *pion.ICECandidate: + _ = sendOffer.Wait() + + iceCandidate := msg.ToJSON() + + icePayload := map[string]interface{}{ + "ice": iceCandidate.Candidate, + "mlineindex": iceCandidate.SDPMLineIndex, + } + + if err = client.sendSessionMessage("ice", icePayload); err != nil { + connState.Done(err) + return + } + + case pion.PeerConnectionState: + switch msg { + case pion.PeerConnectionStateConnecting: + case pion.PeerConnectionStateConnected: + connState.Done(nil) + default: + connState.Done(errors.New("ring: " + msg.String())) + } + } + }) + + // Setup media configuration + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendRecv, + Codecs: []*core.Codec{ + { + Name: "opus", + ClockRate: 48000, + Channels: 2, + }, + }, + }, + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: "H264", + ClockRate: 90000, + }, + }, + }, + } + + // 4. Create offer + offer, err := prod.CreateOffer(medias) + if err != nil { + println("Failed to create offer") + client.Stop() + return nil, err + } + + println("Offer created") + println(offer) + + // 5. Send offer + offerPayload := map[string]interface{}{ + "stream_options": map[string]bool{ + "audio_enabled": true, + "video_enabled": true, + }, + "sdp": offer, + } + + if err = client.sendSessionMessage("live_view", offerPayload); err != nil { + println("Failed to send live_view message") + client.Stop() + return nil, err + } + + sendOffer.Done(nil) + + // Ring expects a ping message every 5 seconds + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-client.done: + return + case <-ticker.C: + if pc.ConnectionState() == pion.PeerConnectionStateConnected { + if err := client.sendSessionMessage("ping", nil); err != nil { + println("Failed to send ping:", err) + return + } + } + } + } + }() + + go func() { + var err error + + // will be closed when conn will be closed + defer func() { + connState.Done(err) + }() + + for { + select { + case <-client.done: + return + default: + var res BaseMessage + if err = conn.ReadJSON(&res); err != nil { + select { + case <-client.done: + return + default: + } + + if websocket.IsCloseError(err, websocket.CloseNormalClosure) { + println("WebSocket closed normally") + } else { + println("Failed to read JSON message:", err) + client.Stop() + } + return + } + + body, _ := json.Marshal(res.Body) + bodyStr := string(body) + + println("Received message:", res.Method) + println("Message body:", bodyStr) + + // check if "doorbot_id" is present and matches the camera ID + if _, ok := res.Body["doorbot_id"]; !ok { + println("Received message without doorbot_id") + continue + } + + doorbotID := res.Body["doorbot_id"].(float64) + if doorbotID != float64(client.camera.ID) { + println("Received message from unknown doorbot:", doorbotID) + continue + } + + if res.Method == "session_created" || res.Method == "session_started" { + if _, ok := res.Body["session_id"]; ok && client.sessionID == "" { + client.sessionID = res.Body["session_id"].(string) + println("Session established:", client.sessionID) + } + } + + if _, ok := res.Body["session_id"]; ok { + if res.Body["session_id"].(string) != client.sessionID { + println("Received message with wrong session ID") + continue + } + } + + rawMsg, _ := json.Marshal(res) + + switch res.Method { + case "sdp": + // 6. Get answer + var msg AnswerMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + println("Failed to parse SDP message:", err) + client.Stop() + return + } + if err = prod.SetAnswer(msg.Body.SDP); err != nil { + println("Failed to set answer:", err) + client.Stop() + return + } + if err = client.activateSession(); err != nil { + println("Failed to activate session:", err) + client.Stop() + return + } + + case "ice": + // 7. Continue to receiving candidates + var msg IceCandidateMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + println("Failed to parse ICE message:", err) + client.Stop() + return + } + + if err = prod.AddCandidate(msg.Body.Ice); err != nil { + client.Stop() + return + } + + case "close": + client.Stop() + return + + case "pong": + // Ignore + continue + } + } + } + }() + + if err = connState.Wait(); err != nil { + return nil, err + } + + return client, nil +} + +func (c *Client) activateSession() error { + println("Activating session") + + if err := c.sendSessionMessage("activate_session", nil); err != nil { + return err + } + + streamPayload := map[string]interface{}{ + "audio_enabled": true, + "video_enabled": true, + } + + if err := c.sendSessionMessage("stream_options", streamPayload); err != nil { + return err + } + + println("Session activated") + + return nil +} + +func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error { + if body == nil { + body = make(map[string]interface{}) + } + + body["doorbot_id"] = c.camera.ID + if c.sessionID != "" { + body["session_id"] = c.sessionID + } + + msg := map[string]interface{}{ + "method": method, + "dialog_id": c.dialogID, + "body": body, + } + + println("Sending session message:", method) + + if err := c.ws.WriteJSON(msg); err != nil { + log.Error().Err(err).Msg("Failed to send JSON message") + return err + } + + return nil +} + +func (c *Client) GetMedias() []*core.Media { + println("Getting medias") + return c.conn.GetMedias() +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + println("Getting track") + return c.conn.GetTrack(media, codec) +} + +func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + println("Adding track") + return c.conn.AddTrack(media, codec, track) +} + +func (c *Client) Start() error { + println("Starting client") + return c.conn.Start() +} + +func (c *Client) Stop() error { + select { + case <-c.done: + return nil + default: + println("Stopping client") + close(c.done) + } + + if c.conn != nil { + _ = c.conn.Stop() + c.conn = nil + } + + if c.ws != nil { + closePayload := map[string]interface{}{ + "reason": map[string]interface{}{ + "code": CloseReasonNormalClose, + "text": "", + }, + } + + _ = c.sendSessionMessage("close", closePayload) + _ = c.ws.Close() + c.ws = nil + } + + return nil +} + +func (c *Client) MarshalJSON() ([]byte, error) { + return c.conn.MarshalJSON() +} \ No newline at end of file diff --git a/www/add.html b/www/add.html index 49e954d35..1190f07e9 100644 --- a/www/add.html +++ b/www/add.html @@ -35,7 +35,7 @@ function drawTable(table, data) { const cols = ['id', 'name', 'info', 'url', 'location']; const th = (row) => cols.reduce((html, k) => k in row ? `${html}${k}` : html, '') + ''; - const td = (row) => cols.reduce((html, k) => k in row ? `${html}${row[k]}` : html, '') + ''; + const td = (row) => cols.reduce((html, k) => k in row ? `${html}${row[k]}` : html, '') + ''; const thead = th(data.sources[0]); const tbody = data.sources.reduce((html, source) => `${html}${td(source)}`, ''); @@ -218,6 +218,29 @@ }); + +
+
+ + +
+
+
+
From 17bba4d4a28644fb99bdffc5e6bef62d690ea3fd Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 12:47:25 +0100 Subject: [PATCH 28/34] skip empty ICE candidates --- pkg/ring/client.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index db8e2eaa0..47790664c 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -238,6 +238,11 @@ func Dial(rawURL string) (*Client, error) { iceCandidate := msg.ToJSON() + // skip empty ICE candidates + if iceCandidate.Candidate == "" { + return + } + icePayload := map[string]interface{}{ "ice": iceCandidate.Candidate, "mlineindex": iceCandidate.SDPMLineIndex, @@ -425,6 +430,12 @@ func Dial(rawURL string) (*Client, error) { return } + // check for empty ICE candidate + if msg.Body.Ice == "" { + println("Received empty ICE candidate") + continue + } + if err = prod.AddCandidate(msg.Body.Ice); err != nil { client.Stop() return From bceb024588813aa8f8a809ffcd99d9684b8dec57 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 17:37:50 +0100 Subject: [PATCH 29/34] enable speaker for two way audio --- pkg/ring/client.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index 47790664c..8fa27bd5a 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -16,9 +16,9 @@ import ( ) type Client struct { - conn *webrtc.Conn - ws *websocket.Conn api *RingRestClient + ws *websocket.Conn + prod *webrtc.Conn camera *CameraData dialogID string sessionID string @@ -162,7 +162,7 @@ func Dial(rawURL string) (*Client, error) { println("WebSocket URL: ", wsURL) - conn, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ "User-Agent": {"android:com.ringapp"}, }) if err != nil { @@ -194,14 +194,14 @@ func Dial(rawURL string) (*Client, error) { api, err := webrtc.NewAPI() if err != nil { println("Failed to create WebRTC API") - conn.Close() + ws.Close() return nil, err } pc, err := api.NewPeerConnection(conf) if err != nil { println("Failed to create Peer Connection") - conn.Close() + ws.Close() return nil, err } @@ -223,11 +223,11 @@ func Dial(rawURL string) (*Client, error) { prod.URL = rawURL client := &Client{ - ws: conn, api: ringAPI, + ws: ws, + prod: prod, camera: camera, dialogID: uuid.NewString(), - conn: prod, done: make(chan struct{}), } @@ -351,7 +351,7 @@ func Dial(rawURL string) (*Client, error) { return default: var res BaseMessage - if err = conn.ReadJSON(&res); err != nil { + if err = ws.ReadJSON(&res); err != nil { select { case <-client.done: return @@ -509,22 +509,28 @@ func (c *Client) sendSessionMessage(method string, body map[string]interface{}) func (c *Client) GetMedias() []*core.Media { println("Getting medias") - return c.conn.GetMedias() + return c.prod.GetMedias() } func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { println("Getting track") - return c.conn.GetTrack(media, codec) + return c.prod.GetTrack(media, codec) } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - println("Adding track") - return c.conn.AddTrack(media, codec, track) + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + + _ = c.sendSessionMessage("camera_options", speakerPayload); + + return c.prod.AddTrack(media, codec, track) } func (c *Client) Start() error { println("Starting client") - return c.conn.Start() + return c.prod.Start() } func (c *Client) Stop() error { @@ -536,9 +542,9 @@ func (c *Client) Stop() error { close(c.done) } - if c.conn != nil { - _ = c.conn.Stop() - c.conn = nil + if c.prod != nil { + _ = c.prod.Stop() + c.prod = nil } if c.ws != nil { @@ -558,5 +564,5 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - return c.conn.MarshalJSON() + return c.prod.MarshalJSON() } \ No newline at end of file From c9682ca64da755000a6c12b32db3df00cad5525c Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 18:02:47 +0100 Subject: [PATCH 30/34] remove unnecessary prints and use mutex for ws --- pkg/ring/client.go | 83 ++++++++++------------------------------------ 1 file changed, 18 insertions(+), 65 deletions(-) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index 8fa27bd5a..b48727d9f 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/url" + "sync" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -12,7 +13,6 @@ import ( "github.com/google/uuid" "github.com/gorilla/websocket" pion "github.com/pion/webrtc/v3" - "github.com/rs/zerolog/log" ) type Client struct { @@ -22,6 +22,7 @@ type Client struct { camera *CameraData dialogID string sessionID string + wsMutex sync.Mutex done chan struct{} } @@ -120,10 +121,6 @@ func Dial(rawURL string) (*Client, error) { return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) } - println("Connecting to Ring WebSocket") - println("Refresh Token: ", refreshToken) - println("Device ID: ", deviceID) - // Initialize Ring API client ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) if err != nil { @@ -153,15 +150,10 @@ func Dial(rawURL string) (*Client, error) { return nil, err } - println("WebSocket Ticket: ", ticket.Ticket) - println("WebSocket ResponseTimestamp: ", ticket.ResponseTimestamp) - // Create WebSocket connection wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", uuid.NewString(), url.QueryEscape(ticket.Ticket)) - println("WebSocket URL: ", wsURL) - ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ "User-Agent": {"android:com.ringapp"}, }) @@ -169,11 +161,7 @@ func Dial(rawURL string) (*Client, error) { return nil, err } - println("WebSocket handshake completed successfully") - // 3. Create Peer Connection - println("Creating Peer Connection") - conf := pion.Configuration{ ICEServers: []pion.ICEServer{ {URLs: []string{ @@ -193,20 +181,16 @@ func Dial(rawURL string) (*Client, error) { api, err := webrtc.NewAPI() if err != nil { - println("Failed to create WebRTC API") ws.Close() return nil, err } pc, err := api.NewPeerConnection(conf) if err != nil { - println("Failed to create Peer Connection") ws.Close() return nil, err } - println("Peer Connection created") - // protect from sending ICE candidate before Offer var sendOffer core.Waiter @@ -292,14 +276,10 @@ func Dial(rawURL string) (*Client, error) { // 4. Create offer offer, err := prod.CreateOffer(medias) if err != nil { - println("Failed to create offer") client.Stop() return nil, err } - println("Offer created") - println(offer) - // 5. Send offer offerPayload := map[string]interface{}{ "stream_options": map[string]bool{ @@ -310,7 +290,6 @@ func Dial(rawURL string) (*Client, error) { } if err = client.sendSessionMessage("live_view", offerPayload); err != nil { - println("Failed to send live_view message") client.Stop() return nil, err } @@ -329,7 +308,6 @@ func Dial(rawURL string) (*Client, error) { case <-ticker.C: if pc.ConnectionState() == pion.PeerConnectionStateConnected { if err := client.sendSessionMessage("ping", nil); err != nil { - println("Failed to send ping:", err) return } } @@ -358,43 +336,30 @@ func Dial(rawURL string) (*Client, error) { default: } - if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - println("WebSocket closed normally") - } else { - println("Failed to read JSON message:", err) - client.Stop() - } + client.Stop() return } - body, _ := json.Marshal(res.Body) - bodyStr := string(body) - - println("Received message:", res.Method) - println("Message body:", bodyStr) - - // check if "doorbot_id" is present and matches the camera ID + // check if "doorbot_id" is present if _, ok := res.Body["doorbot_id"]; !ok { - println("Received message without doorbot_id") continue } + // check if the message is from the correct doorbot doorbotID := res.Body["doorbot_id"].(float64) if doorbotID != float64(client.camera.ID) { - println("Received message from unknown doorbot:", doorbotID) continue } + // check if the message is from the correct session if res.Method == "session_created" || res.Method == "session_started" { if _, ok := res.Body["session_id"]; ok && client.sessionID == "" { client.sessionID = res.Body["session_id"].(string) - println("Session established:", client.sessionID) } } if _, ok := res.Body["session_id"]; ok { if res.Body["session_id"].(string) != client.sessionID { - println("Received message with wrong session ID") continue } } @@ -406,17 +371,14 @@ func Dial(rawURL string) (*Client, error) { // 6. Get answer var msg AnswerMessage if err = json.Unmarshal(rawMsg, &msg); err != nil { - println("Failed to parse SDP message:", err) client.Stop() return } if err = prod.SetAnswer(msg.Body.SDP); err != nil { - println("Failed to set answer:", err) client.Stop() return } if err = client.activateSession(); err != nil { - println("Failed to activate session:", err) client.Stop() return } @@ -425,15 +387,12 @@ func Dial(rawURL string) (*Client, error) { // 7. Continue to receiving candidates var msg IceCandidateMessage if err = json.Unmarshal(rawMsg, &msg); err != nil { - println("Failed to parse ICE message:", err) - client.Stop() - return + break } // check for empty ICE candidate if msg.Body.Ice == "" { - println("Received empty ICE candidate") - continue + break } if err = prod.AddCandidate(msg.Body.Ice); err != nil { @@ -461,8 +420,6 @@ func Dial(rawURL string) (*Client, error) { } func (c *Client) activateSession() error { - println("Activating session") - if err := c.sendSessionMessage("activate_session", nil); err != nil { return err } @@ -476,12 +433,13 @@ func (c *Client) activateSession() error { return err } - println("Session activated") - return nil } func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error { + c.wsMutex.Lock() + defer c.wsMutex.Unlock() + if body == nil { body = make(map[string]interface{}) } @@ -497,10 +455,7 @@ func (c *Client) sendSessionMessage(method string, body map[string]interface{}) "body": body, } - println("Sending session message:", method) - if err := c.ws.WriteJSON(msg); err != nil { - log.Error().Err(err).Msg("Failed to send JSON message") return err } @@ -508,28 +463,27 @@ func (c *Client) sendSessionMessage(method string, body map[string]interface{}) } func (c *Client) GetMedias() []*core.Media { - println("Getting medias") return c.prod.GetMedias() } func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - println("Getting track") return c.prod.GetTrack(media, codec) } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - // Enable speaker - speakerPayload := map[string]interface{}{ - "stealth_mode": false, + if media.Kind == core.KindAudio { + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + + _ = c.sendSessionMessage("camera_options", speakerPayload); } - _ = c.sendSessionMessage("camera_options", speakerPayload); - return c.prod.AddTrack(media, codec, track) } func (c *Client) Start() error { - println("Starting client") return c.prod.Start() } @@ -538,7 +492,6 @@ func (c *Client) Stop() error { case <-c.done: return nil default: - println("Stopping client") close(c.done) } From 2c5f1e0417b01d495a70b8d1f709afb9636a01bb Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 19:37:17 +0100 Subject: [PATCH 31/34] add 2fa --- internal/ring/init.go | 87 ++++++++++++---- pkg/ring/api.go | 223 +++++++++++++++++++++++++++++++++--------- www/add.html | 36 ++++++- 3 files changed, 274 insertions(+), 72 deletions(-) diff --git a/internal/ring/init.go b/internal/ring/init.go index 24a91ac62..521c137a8 100644 --- a/internal/ring/init.go +++ b/internal/ring/init.go @@ -1,7 +1,9 @@ package ring import ( + "encoding/json" "net/http" + "net/url" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" @@ -18,30 +20,75 @@ func Init() { } func apiRing(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - refreshToken := query.Get("refresh_token") + query := r.URL.Query() + var ringAPI *ring.RingRestClient + var err error - ringAPI, err := ring.NewRingRestClient(ring.RefreshTokenAuth{RefreshToken: refreshToken}, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + // Check auth method + if email := query.Get("email"); email != "" { + // Email/Password Flow + password := query.Get("password") + code := query.Get("code") - devices, err := ringAPI.FetchRingDevices() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{ + Email: email, + Password: password, + }, nil) - var items []*api.Source + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } - for _, camera := range devices.AllCameras { - query.Set("device_id", camera.DeviceID) + // Try authentication (this will trigger 2FA if needed) + if _, err = ringAPI.GetAuth(code); err != nil { + if ringAPI.Using2FA { + // Return 2FA prompt + json.NewEncoder(w).Encode(map[string]interface{}{ + "needs_2fa": true, + "prompt": ringAPI.PromptFor2FA, + }) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + // Refresh Token Flow + refreshToken := query.Get("refresh_token") + if refreshToken == "" { + http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest) + return + } - items = append(items, &api.Source{ - Name: camera.Description, URL: "ring:?" + query.Encode(), - }) - } + ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{ + RefreshToken: refreshToken, + }, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } - api.ResponseSources(w, items) + // Fetch devices + devices, err := ringAPI.FetchRingDevices() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create clean query with only required parameters + cleanQuery := url.Values{} + cleanQuery.Set("refresh_token", ringAPI.RefreshToken) + + var items []*api.Source + for _, camera := range devices.AllCameras { + cleanQuery.Set("device_id", camera.DeviceID) + items = append(items, &api.Source{ + Name: camera.Description, + URL: "ring:?" + cleanQuery.Encode(), + }) + } + + api.ResponseSources(w, items) } diff --git a/pkg/ring/api.go b/pkg/ring/api.go index faebf6b90..e025e031d 100644 --- a/pkg/ring/api.go +++ b/pkg/ring/api.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "reflect" + "strings" "time" ) @@ -17,6 +18,11 @@ type RefreshTokenAuth struct { RefreshToken string } +type EmailAuth struct { + Email string + Password string +} + // AuthConfig represents the decoded refresh token data type AuthConfig struct { RT string `json:"rt"` // Refresh Token @@ -32,6 +38,14 @@ type AuthTokenResponse struct { TokenType string `json:"token_type"` // Always "Bearer" } +type Auth2faResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + TSVState string `json:"tsv_state"` + Phone string `json:"phone"` + NextTimeInSecs int `json:"next_time_in_secs"` +} + // SocketTicketRequest represents the request to get a socket ticket type SocketTicketResponse struct { Ticket string `json:"ticket"` @@ -40,17 +54,42 @@ type SocketTicketResponse struct { // RingRestClient handles authentication and requests to Ring API type RingRestClient struct { - httpClient *http.Client - authConfig *AuthConfig - hardwareID string - authToken *AuthTokenResponse - auth RefreshTokenAuth - onTokenRefresh func(string) // Callback when refresh token is updated + httpClient *http.Client + authConfig *AuthConfig + hardwareID string + authToken *AuthTokenResponse + Using2FA bool + PromptFor2FA string + RefreshToken string + auth interface{} // EmailAuth or RefreshTokenAuth + onTokenRefresh func(string) } // CameraKind represents the different types of Ring cameras type CameraKind string +// CameraData contains common fields for all camera types +type CameraData struct { + ID float64 `json:"id"` + Description string `json:"description"` + DeviceID string `json:"device_id"` + Kind string `json:"kind"` + LocationID string `json:"location_id"` +} + +// RingDeviceType represents different types of Ring devices +type RingDeviceType string + +// RingDevicesResponse represents the response from the Ring API +type RingDevicesResponse struct { + Doorbots []CameraData `json:"doorbots"` + AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` + StickupCams []CameraData `json:"stickup_cams"` + AllCameras []CameraData `json:"all_cameras"` + Chimes []CameraData `json:"chimes"` + Other []map[string]interface{} `json:"other"` +} + const ( Doorbot CameraKind = "doorbot" Doorbell CameraKind = "doorbell" @@ -86,33 +125,11 @@ const ( OnvifCamera CameraKind = "onvif_camera" ) -// RingDeviceType represents different types of Ring devices -type RingDeviceType string - const ( IntercomHandsetAudio RingDeviceType = "intercom_handset_audio" OnvifCameraType RingDeviceType = "onvif_camera" ) -// CameraData contains common fields for all camera types -type CameraData struct { - ID float64 `json:"id"` - Description string `json:"description"` - DeviceID string `json:"device_id"` - Kind string `json:"kind"` - LocationID string `json:"location_id"` -} - -// RingDevicesResponse represents the response from the Ring API -type RingDevicesResponse struct { - Doorbots []CameraData `json:"doorbots"` - AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` - StickupCams []CameraData `json:"stickup_cams"` - AllCameras []CameraData `json:"all_cameras"` - Chimes []CameraData `json:"chimes"` - Other []map[string]interface{} `json:"other"` -} - const ( clientAPIBaseURL = "https://api.ring.com/clients_api/" deviceAPIBaseURL = "https://api.ring.com/devices/v1/" @@ -125,27 +142,37 @@ const ( ) // NewRingRestClient creates a new Ring client instance -func NewRingRestClient(auth RefreshTokenAuth, onTokenRefresh func(string)) (*RingRestClient, error) { - client := &RingRestClient{ - httpClient: &http.Client{ - Timeout: defaultTimeout, - }, - auth: auth, - onTokenRefresh: onTokenRefresh, - hardwareID: generateHardwareID(), - } +func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) { + client := &RingRestClient{ + httpClient: &http.Client{Timeout: defaultTimeout}, + onTokenRefresh: onTokenRefresh, + hardwareID: generateHardwareID(), + auth: auth, + } + + switch a := auth.(type) { + case RefreshTokenAuth: + if a.RefreshToken == "" { + return nil, fmt.Errorf("refresh token is required") + } + + config, err := parseAuthConfig(a.RefreshToken) + if err != nil { + return nil, fmt.Errorf("failed to parse refresh token: %w", err) + } - // check if refresh token is provided - if auth.RefreshToken == "" { - return nil, fmt.Errorf("refresh token is required") - } - - if config, err := parseAuthConfig(auth.RefreshToken); err == nil { client.authConfig = config - client.hardwareID = config.HID - } - - return client, nil + client.hardwareID = config.HID + client.RefreshToken = a.RefreshToken + case EmailAuth: + if a.Email == "" || a.Password == "" { + return nil, fmt.Errorf("email and password are required") + } + default: + return nil, fmt.Errorf("invalid auth type") + } + + return client, nil } // Request makes an authenticated request to the Ring API @@ -289,6 +316,108 @@ func (c *RingRestClient) ensureAuth() error { return nil } +// getAuth makes an authentication request to the Ring API +func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) { + var grantData map[string]string + + if c.authConfig != nil && twoFactorAuthCode == "" { + grantData = map[string]string{ + "grant_type": "refresh_token", + "refresh_token": c.authConfig.RT, + } + } else { + authEmail, ok := c.auth.(EmailAuth) + if !ok { + return nil, fmt.Errorf("invalid auth type for email authentication") + } + grantData = map[string]string{ + "grant_type": "password", + "username": authEmail.Email, + "password": authEmail.Password, + } + } + + grantData["client_id"] = "ring_official_android" + grantData["scope"] = "client" + + body, err := json.Marshal(grantData) + if err != nil { + return nil, fmt.Errorf("failed to marshal auth request: %w", err) + } + + req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + req.Header.Set("2fa-support", "true") + if twoFactorAuthCode != "" { + req.Header.Set("2fa-code", twoFactorAuthCode) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Handle 2FA Responses + if resp.StatusCode == http.StatusPreconditionFailed || + (resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) { + + var tfaResp Auth2faResponse + if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil { + return nil, err + } + + c.Using2FA = true + if resp.StatusCode == http.StatusBadRequest { + c.PromptFor2FA = "Invalid 2fa code entered. Please try again." + return nil, fmt.Errorf("invalid 2FA code") + } + + if tfaResp.TSVState != "" { + prompt := "from your authenticator app" + if tfaResp.TSVState != "totp" { + prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState) + } + c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt) + } else { + c.PromptFor2FA = "Please enter the code sent to your text/email" + } + + return nil, fmt.Errorf("2FA required") + } + + // Handle errors + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var authResp AuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return nil, fmt.Errorf("failed to decode auth response: %w", err) + } + + c.authToken = &authResp + c.authConfig = &AuthConfig{ + RT: authResp.RefreshToken, + HID: c.hardwareID, + } + + c.RefreshToken = encodeAuthConfig(c.authConfig) + if c.onTokenRefresh != nil { + c.onTokenRefresh(c.RefreshToken) + } + + return c.authToken, nil +} + // Helper functions for auth config encoding/decoding func parseAuthConfig(refreshToken string) (*AuthConfig, error) { decoded, err := base64.StdEncoding.DecodeString(refreshToken) diff --git a/www/add.html b/www/add.html index 1190f07e9..7dae63d49 100644 --- a/www/add.html +++ b/www/add.html @@ -220,7 +220,16 @@
-
+ + + + + +
+
@@ -231,15 +240,32 @@ ev.target.nextElementSibling.style.display = 'block'; }); - document.getElementById('ring-form').addEventListener('submit', async ev => { + async function handleRingAuth(ev) { ev.preventDefault(); - const query = new URLSearchParams(new FormData(ev.target)); const url = new URL('api/ring?' + query.toString(), location.href); const r = await fetch(url, {cache: 'no-cache'}); - await getSources('ring-table', r); - }); + const data = await r.json(); + + if (data.needs_2fa) { + document.getElementById('tfa-field').style.display = 'block'; + document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code'; + return; + } + + if (!r.ok) { + const table = document.getElementById('ring-table'); + table.innerText = data.error || 'Unknown error'; + return; + } + + const table = document.getElementById('ring-table'); + drawTable(table, data); + } + + document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth); + document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth); From 0651a09a3c0f19250dcc2ff0845195450b6c8684 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 22:35:04 +0100 Subject: [PATCH 32/34] add snapshot producer --- internal/ring/init.go | 8 + pkg/ring/client.go | 671 ++++++++++++++++++++++-------------------- pkg/ring/snapshot.go | 64 ++++ 3 files changed, 418 insertions(+), 325 deletions(-) create mode 100644 pkg/ring/snapshot.go diff --git a/internal/ring/init.go b/internal/ring/init.go index 521c137a8..bc49178b2 100644 --- a/internal/ring/init.go +++ b/internal/ring/init.go @@ -84,10 +84,18 @@ func apiRing(w http.ResponseWriter, r *http.Request) { var items []*api.Source for _, camera := range devices.AllCameras { cleanQuery.Set("device_id", camera.DeviceID) + + // Stream source items = append(items, &api.Source{ Name: camera.Description, URL: "ring:?" + cleanQuery.Encode(), }) + + // Snapshot source + items = append(items, &api.Source{ + Name: camera.Description + " Snapshot", + URL: "ring:?" + cleanQuery.Encode() + "&snapshot", + }) } api.ResponseSources(w, items) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index b48727d9f..c432ecf97 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -18,7 +18,7 @@ import ( type Client struct { api *RingRestClient ws *websocket.Conn - prod *webrtc.Conn + prod core.Producer camera *CameraData dialogID string sessionID string @@ -101,322 +101,337 @@ const ( ) func Dial(rawURL string) (*Client, error) { - // 1. Create Ring Rest API client - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } - - query := u.Query() - encodedToken := query.Get("refresh_token") - deviceID := query.Get("device_id") - - if encodedToken == "" || deviceID == "" { - return nil, errors.New("ring: wrong query") - } - - // URL-decode the refresh token - refreshToken, err := url.QueryUnescape(encodedToken) - if err != nil { - return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) - } - - // Initialize Ring API client - ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) - if err != nil { - return nil, err - } - - // Get camera details - devices, err := ringAPI.FetchRingDevices() - if err != nil { - return nil, err - } - - var camera *CameraData - for _, cam := range devices.AllCameras { - if fmt.Sprint(cam.DeviceID) == deviceID { - camera = &cam - break - } - } - if camera == nil { - return nil, errors.New("ring: camera not found") - } - - // 2. Connect to signaling server - ticket, err := ringAPI.GetSocketTicket() - if err != nil { - return nil, err - } - - // Create WebSocket connection - wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", - uuid.NewString(), url.QueryEscape(ticket.Ticket)) - - ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ - "User-Agent": {"android:com.ringapp"}, - }) - if err != nil { - return nil, err - } - - // 3. Create Peer Connection - conf := pion.Configuration{ - ICEServers: []pion.ICEServer{ - {URLs: []string{ - "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", - "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", - "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302", - "stun:stun3.l.google.com:19302", - "stun:stun4.l.google.com:19302", - }}, - }, - ICETransportPolicy: pion.ICETransportPolicyAll, + // 1. Parse URL and validate basic params + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + encodedToken := query.Get("refresh_token") + deviceID := query.Get("device_id") + _, isSnapshot := query["snapshot"] + + if encodedToken == "" || deviceID == "" { + return nil, errors.New("ring: wrong query") + } + + // URL-decode the refresh token + refreshToken, err := url.QueryUnescape(encodedToken) + if err != nil { + return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) + } + + // Initialize Ring API client + ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + return nil, err + } + + // Get camera details + devices, err := ringAPI.FetchRingDevices() + if err != nil { + return nil, err + } + + var camera *CameraData + for _, cam := range devices.AllCameras { + if fmt.Sprint(cam.DeviceID) == deviceID { + camera = &cam + break + } + } + if camera == nil { + return nil, errors.New("ring: camera not found") + } + + // Create base client + client := &Client{ + api: ringAPI, + camera: camera, + dialogID: uuid.NewString(), + done: make(chan struct{}), + } + + // Check if snapshot request + if isSnapshot { + client.prod = NewSnapshotProducer(ringAPI, camera) + return client, nil + } + + // If not snapshot, continue with WebRTC setup + ticket, err := ringAPI.GetSocketTicket() + if err != nil { + return nil, err + } + + // Create WebSocket connection + wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", + uuid.NewString(), url.QueryEscape(ticket.Ticket)) + + client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + "User-Agent": {"android:com.ringapp"}, + }) + if err != nil { + return nil, err + } + + // Create Peer Connection + conf := pion.Configuration{ + ICEServers: []pion.ICEServer{ + {URLs: []string{ + "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", + "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", + "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + }}, + }, + ICETransportPolicy: pion.ICETransportPolicyAll, BundlePolicy: pion.BundlePolicyBalanced, - } - - api, err := webrtc.NewAPI() - if err != nil { - ws.Close() - return nil, err - } - - pc, err := api.NewPeerConnection(conf) - if err != nil { - ws.Close() - return nil, err - } - - // protect from sending ICE candidate before Offer - var sendOffer core.Waiter - - // protect from blocking on errors - defer sendOffer.Done(nil) - - // waiter will wait PC error or WS error or nil (connection OK) - var connState core.Waiter - - prod := webrtc.NewConn(pc) - prod.FormatName = "ring/webrtc" - prod.Mode = core.ModeActiveProducer - prod.Protocol = "ws" - prod.URL = rawURL - - client := &Client{ - api: ringAPI, - ws: ws, - prod: prod, - camera: camera, - dialogID: uuid.NewString(), - done: make(chan struct{}), - } - - prod.Listen(func(msg any) { - switch msg := msg.(type) { - case *pion.ICECandidate: - _ = sendOffer.Wait() - - iceCandidate := msg.ToJSON() - - // skip empty ICE candidates - if iceCandidate.Candidate == "" { - return - } - - icePayload := map[string]interface{}{ - "ice": iceCandidate.Candidate, - "mlineindex": iceCandidate.SDPMLineIndex, - } - - if err = client.sendSessionMessage("ice", icePayload); err != nil { - connState.Done(err) - return - } - - case pion.PeerConnectionState: - switch msg { - case pion.PeerConnectionStateConnecting: - case pion.PeerConnectionStateConnected: - connState.Done(nil) - default: - connState.Done(errors.New("ring: " + msg.String())) - } - } - }) - - // Setup media configuration - medias := []*core.Media{ - { - Kind: core.KindAudio, - Direction: core.DirectionSendRecv, - Codecs: []*core.Codec{ - { - Name: "opus", - ClockRate: 48000, - Channels: 2, - }, - }, - }, - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: "H264", - ClockRate: 90000, - }, - }, - }, - } - - // 4. Create offer - offer, err := prod.CreateOffer(medias) - if err != nil { - client.Stop() - return nil, err - } - - // 5. Send offer - offerPayload := map[string]interface{}{ - "stream_options": map[string]bool{ - "audio_enabled": true, - "video_enabled": true, - }, - "sdp": offer, - } - - if err = client.sendSessionMessage("live_view", offerPayload); err != nil { - client.Stop() - return nil, err - } - - sendOffer.Done(nil) - - // Ring expects a ping message every 5 seconds - go func() { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-client.done: - return - case <-ticker.C: - if pc.ConnectionState() == pion.PeerConnectionStateConnected { - if err := client.sendSessionMessage("ping", nil); err != nil { - return - } - } - } - } - }() - - go func() { - var err error - - // will be closed when conn will be closed - defer func() { - connState.Done(err) - }() - - for { - select { - case <-client.done: - return - default: - var res BaseMessage - if err = ws.ReadJSON(&res); err != nil { - select { - case <-client.done: - return - default: - } - - client.Stop() - return - } - - // check if "doorbot_id" is present - if _, ok := res.Body["doorbot_id"]; !ok { - continue - } - - // check if the message is from the correct doorbot - doorbotID := res.Body["doorbot_id"].(float64) - if doorbotID != float64(client.camera.ID) { - continue - } - - // check if the message is from the correct session - if res.Method == "session_created" || res.Method == "session_started" { - if _, ok := res.Body["session_id"]; ok && client.sessionID == "" { - client.sessionID = res.Body["session_id"].(string) - } - } - - if _, ok := res.Body["session_id"]; ok { - if res.Body["session_id"].(string) != client.sessionID { - continue - } - } - - rawMsg, _ := json.Marshal(res) - - switch res.Method { - case "sdp": - // 6. Get answer - var msg AnswerMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - client.Stop() - return - } - if err = prod.SetAnswer(msg.Body.SDP); err != nil { - client.Stop() - return - } - if err = client.activateSession(); err != nil { - client.Stop() - return - } - - case "ice": - // 7. Continue to receiving candidates - var msg IceCandidateMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - break - } - - // check for empty ICE candidate - if msg.Body.Ice == "" { - break - } - - if err = prod.AddCandidate(msg.Body.Ice); err != nil { - client.Stop() - return - } - - case "close": - client.Stop() - return - - case "pong": - // Ignore - continue - } - } - } - }() + } + + api, err := webrtc.NewAPI() + if err != nil { + client.ws.Close() + return nil, err + } + + pc, err := api.NewPeerConnection(conf) + if err != nil { + client.ws.Close() + return nil, err + } + + // protect from sending ICE candidate before Offer + var sendOffer core.Waiter + + // protect from blocking on errors + defer sendOffer.Done(nil) + + // waiter will wait PC error or WS error or nil (connection OK) + var connState core.Waiter + + prod := webrtc.NewConn(pc) + prod.FormatName = "ring/webrtc" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL + + client.prod = prod + + prod.Listen(func(msg any) { + switch msg := msg.(type) { + case *pion.ICECandidate: + _ = sendOffer.Wait() + + iceCandidate := msg.ToJSON() + + // skip empty ICE candidates + if iceCandidate.Candidate == "" { + return + } + + icePayload := map[string]interface{}{ + "ice": iceCandidate.Candidate, + "mlineindex": iceCandidate.SDPMLineIndex, + } + + if err = client.sendSessionMessage("ice", icePayload); err != nil { + connState.Done(err) + return + } + + case pion.PeerConnectionState: + switch msg { + case pion.PeerConnectionStateConnecting: + case pion.PeerConnectionStateConnected: + connState.Done(nil) + default: + connState.Done(errors.New("ring: " + msg.String())) + } + } + }) + + // Setup media configuration + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendRecv, + Codecs: []*core.Codec{ + { + Name: "opus", + ClockRate: 48000, + Channels: 2, + }, + }, + }, + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: "H264", + ClockRate: 90000, + }, + }, + }, + } + + // Create offer + offer, err := prod.CreateOffer(medias) + if err != nil { + client.Stop() + return nil, err + } + + // Send offer + offerPayload := map[string]interface{}{ + "stream_options": map[string]bool{ + "audio_enabled": true, + "video_enabled": true, + }, + "sdp": offer, + } + + if err = client.sendSessionMessage("live_view", offerPayload); err != nil { + client.Stop() + return nil, err + } + + sendOffer.Done(nil) + + // Ring expects a ping message every 5 seconds + go client.startPingLoop(pc) + go client.startMessageLoop(&connState) + + if err = connState.Wait(); err != nil { + return nil, err + } + + return client, nil +} - if err = connState.Wait(); err != nil { - return nil, err - } +func (c *Client) startPingLoop(pc *pion.PeerConnection) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-c.done: + return + case <-ticker.C: + if pc.ConnectionState() == pion.PeerConnectionStateConnected { + if err := c.sendSessionMessage("ping", nil); err != nil { + return + } + } + } + } +} - return client, nil +func (c *Client) startMessageLoop(connState *core.Waiter) { + var err error + + // will be closed when conn will be closed + defer func() { + connState.Done(err) + }() + + for { + select { + case <-c.done: + return + default: + var res BaseMessage + if err = c.ws.ReadJSON(&res); err != nil { + select { + case <-c.done: + return + default: + } + + c.Stop() + return + } + + // check if "doorbot_id" is present + if _, ok := res.Body["doorbot_id"]; !ok { + continue + } + + // check if the message is from the correct doorbot + doorbotID := res.Body["doorbot_id"].(float64) + if doorbotID != float64(c.camera.ID) { + continue + } + + // check if the message is from the correct session + if res.Method == "session_created" || res.Method == "session_started" { + if _, ok := res.Body["session_id"]; ok && c.sessionID == "" { + c.sessionID = res.Body["session_id"].(string) + } + } + + if _, ok := res.Body["session_id"]; ok { + if res.Body["session_id"].(string) != c.sessionID { + continue + } + } + + rawMsg, _ := json.Marshal(res) + + switch res.Method { + case "sdp": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Get answer + var msg AnswerMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + c.Stop() + return + } + if err = prod.SetAnswer(msg.Body.SDP); err != nil { + c.Stop() + return + } + if err = c.activateSession(); err != nil { + c.Stop() + return + } + } + + case "ice": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Continue to receiving candidates + var msg IceCandidateMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + break + } + + // check for empty ICE candidate + if msg.Body.Ice == "" { + break + } + + if err = prod.AddCandidate(msg.Body.Ice); err != nil { + c.Stop() + return + } + } + + case "close": + c.Stop() + return + + case "pong": + // Ignore + continue + } + } + } } func (c *Client) activateSession() error { @@ -471,16 +486,18 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - if media.Kind == core.KindAudio { - // Enable speaker - speakerPayload := map[string]interface{}{ - "stealth_mode": false, - } - - _ = c.sendSessionMessage("camera_options", speakerPayload); - } - - return c.prod.AddTrack(media, codec, track) + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + if media.Kind == core.KindAudio { + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + _ = c.sendSessionMessage("camera_options", speakerPayload) + } + return webrtcProd.AddTrack(media, codec, track) + } + + return fmt.Errorf("add track not supported for snapshot") } func (c *Client) Start() error { @@ -517,5 +534,9 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - return c.prod.MarshalJSON() + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + return webrtcProd.MarshalJSON() + } + + return nil, errors.New("ring: can't marshal") } \ No newline at end of file diff --git a/pkg/ring/snapshot.go b/pkg/ring/snapshot.go new file mode 100644 index 000000000..bbf86e284 --- /dev/null +++ b/pkg/ring/snapshot.go @@ -0,0 +1,64 @@ +package ring + +import ( + "fmt" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type SnapshotProducer struct { + core.Connection + + client *RingRestClient + camera *CameraData +} + +func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer { + return &SnapshotProducer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "ring/snapshot", + Protocol: "https", + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + client: client, + camera: camera, + } +} + +func (p *SnapshotProducer) Start() error { + // Fetch snapshot + response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) + if err != nil { + return fmt.Errorf("failed to get snapshot: %w", err) + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: response, + } + + // Send to all receivers + for _, receiver := range p.Receivers { + receiver.WriteRTP(pkt) + } + + return nil +} + +func (p *SnapshotProducer) Stop() error { + return p.Connection.Stop() +} \ No newline at end of file From f072dab07bf3cf0eaeedd06659351cd03c44924e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 25 Jan 2025 11:18:36 +0300 Subject: [PATCH 33/34] Correcting code formatting after #1567 --- internal/ring/init.go | 102 ------ internal/ring/ring.go | 102 ++++++ pkg/ring/api.go | 336 +++++++++--------- pkg/ring/client.go | 784 +++++++++++++++++++++--------------------- pkg/ring/snapshot.go | 80 ++--- www/add.html | 2 +- 6 files changed, 703 insertions(+), 703 deletions(-) delete mode 100644 internal/ring/init.go create mode 100644 internal/ring/ring.go diff --git a/internal/ring/init.go b/internal/ring/init.go deleted file mode 100644 index bc49178b2..000000000 --- a/internal/ring/init.go +++ /dev/null @@ -1,102 +0,0 @@ -package ring - -import ( - "encoding/json" - "net/http" - "net/url" - - "github.com/AlexxIT/go2rtc/internal/api" - "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/ring" -) - -func Init() { - streams.HandleFunc("ring", func(source string) (core.Producer, error) { - return ring.Dial(source) - }) - - api.HandleFunc("api/ring", apiRing) -} - -func apiRing(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - var ringAPI *ring.RingRestClient - var err error - - // Check auth method - if email := query.Get("email"); email != "" { - // Email/Password Flow - password := query.Get("password") - code := query.Get("code") - - ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{ - Email: email, - Password: password, - }, nil) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Try authentication (this will trigger 2FA if needed) - if _, err = ringAPI.GetAuth(code); err != nil { - if ringAPI.Using2FA { - // Return 2FA prompt - json.NewEncoder(w).Encode(map[string]interface{}{ - "needs_2fa": true, - "prompt": ringAPI.PromptFor2FA, - }) - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } else { - // Refresh Token Flow - refreshToken := query.Get("refresh_token") - if refreshToken == "" { - http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest) - return - } - - ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{ - RefreshToken: refreshToken, - }, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - - // Fetch devices - devices, err := ringAPI.FetchRingDevices() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Create clean query with only required parameters - cleanQuery := url.Values{} - cleanQuery.Set("refresh_token", ringAPI.RefreshToken) - - var items []*api.Source - for _, camera := range devices.AllCameras { - cleanQuery.Set("device_id", camera.DeviceID) - - // Stream source - items = append(items, &api.Source{ - Name: camera.Description, - URL: "ring:?" + cleanQuery.Encode(), - }) - - // Snapshot source - items = append(items, &api.Source{ - Name: camera.Description + " Snapshot", - URL: "ring:?" + cleanQuery.Encode() + "&snapshot", - }) - } - - api.ResponseSources(w, items) -} diff --git a/internal/ring/ring.go b/internal/ring/ring.go new file mode 100644 index 000000000..673ea480e --- /dev/null +++ b/internal/ring/ring.go @@ -0,0 +1,102 @@ +package ring + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/ring" +) + +func Init() { + streams.HandleFunc("ring", func(source string) (core.Producer, error) { + return ring.Dial(source) + }) + + api.HandleFunc("api/ring", apiRing) +} + +func apiRing(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + var ringAPI *ring.RingRestClient + var err error + + // Check auth method + if email := query.Get("email"); email != "" { + // Email/Password Flow + password := query.Get("password") + code := query.Get("code") + + ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{ + Email: email, + Password: password, + }, nil) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Try authentication (this will trigger 2FA if needed) + if _, err = ringAPI.GetAuth(code); err != nil { + if ringAPI.Using2FA { + // Return 2FA prompt + json.NewEncoder(w).Encode(map[string]interface{}{ + "needs_2fa": true, + "prompt": ringAPI.PromptFor2FA, + }) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + // Refresh Token Flow + refreshToken := query.Get("refresh_token") + if refreshToken == "" { + http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest) + return + } + + ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{ + RefreshToken: refreshToken, + }, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + // Fetch devices + devices, err := ringAPI.FetchRingDevices() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create clean query with only required parameters + cleanQuery := url.Values{} + cleanQuery.Set("refresh_token", ringAPI.RefreshToken) + + var items []*api.Source + for _, camera := range devices.AllCameras { + cleanQuery.Set("device_id", camera.DeviceID) + + // Stream source + items = append(items, &api.Source{ + Name: camera.Description, + URL: "ring:?" + cleanQuery.Encode(), + }) + + // Snapshot source + items = append(items, &api.Source{ + Name: camera.Description + " Snapshot", + URL: "ring:?" + cleanQuery.Encode() + "&snapshot", + }) + } + + api.ResponseSources(w, items) +} diff --git a/pkg/ring/api.go b/pkg/ring/api.go index e025e031d..ed69465fb 100644 --- a/pkg/ring/api.go +++ b/pkg/ring/api.go @@ -19,8 +19,8 @@ type RefreshTokenAuth struct { } type EmailAuth struct { - Email string - Password string + Email string + Password string } // AuthConfig represents the decoded refresh token data @@ -31,38 +31,38 @@ type AuthConfig struct { // AuthTokenResponse represents the response from the authentication endpoint type AuthTokenResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` // Always "client" - TokenType string `json:"token_type"` // Always "Bearer" + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` // Always "client" + TokenType string `json:"token_type"` // Always "Bearer" } type Auth2faResponse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` - TSVState string `json:"tsv_state"` - Phone string `json:"phone"` - NextTimeInSecs int `json:"next_time_in_secs"` + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + TSVState string `json:"tsv_state"` + Phone string `json:"phone"` + NextTimeInSecs int `json:"next_time_in_secs"` } // SocketTicketRequest represents the request to get a socket ticket type SocketTicketResponse struct { - Ticket string `json:"ticket"` - ResponseTimestamp int64 `json:"response_timestamp"` + Ticket string `json:"ticket"` + ResponseTimestamp int64 `json:"response_timestamp"` } // RingRestClient handles authentication and requests to Ring API type RingRestClient struct { - httpClient *http.Client - authConfig *AuthConfig - hardwareID string - authToken *AuthTokenResponse - Using2FA bool - PromptFor2FA string - RefreshToken string - auth interface{} // EmailAuth or RefreshTokenAuth - onTokenRefresh func(string) + httpClient *http.Client + authConfig *AuthConfig + hardwareID string + authToken *AuthTokenResponse + Using2FA bool + PromptFor2FA string + RefreshToken string + auth interface{} // EmailAuth or RefreshTokenAuth + onTokenRefresh func(string) } // CameraKind represents the different types of Ring cameras @@ -70,11 +70,11 @@ type CameraKind string // CameraData contains common fields for all camera types type CameraData struct { - ID float64 `json:"id"` - Description string `json:"description"` - DeviceID string `json:"device_id"` - Kind string `json:"kind"` - LocationID string `json:"location_id"` + ID float64 `json:"id"` + Description string `json:"description"` + DeviceID string `json:"device_id"` + Kind string `json:"kind"` + LocationID string `json:"location_id"` } // RingDeviceType represents different types of Ring devices @@ -82,12 +82,12 @@ type RingDeviceType string // RingDevicesResponse represents the response from the Ring API type RingDevicesResponse struct { - Doorbots []CameraData `json:"doorbots"` - AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` - StickupCams []CameraData `json:"stickup_cams"` - AllCameras []CameraData `json:"all_cameras"` - Chimes []CameraData `json:"chimes"` - Other []map[string]interface{} `json:"other"` + Doorbots []CameraData `json:"doorbots"` + AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` + StickupCams []CameraData `json:"stickup_cams"` + AllCameras []CameraData `json:"all_cameras"` + Chimes []CameraData `json:"chimes"` + Other []map[string]interface{} `json:"other"` } const ( @@ -131,48 +131,48 @@ const ( ) const ( - clientAPIBaseURL = "https://api.ring.com/clients_api/" - deviceAPIBaseURL = "https://api.ring.com/devices/v1/" - commandsAPIBaseURL = "https://api.ring.com/commands/v1/" - appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/" - oauthURL = "https://oauth.ring.com/oauth/token" - apiVersion = 11 - defaultTimeout = 20 * time.Second - maxRetries = 3 + clientAPIBaseURL = "https://api.ring.com/clients_api/" + deviceAPIBaseURL = "https://api.ring.com/devices/v1/" + commandsAPIBaseURL = "https://api.ring.com/commands/v1/" + appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/" + oauthURL = "https://oauth.ring.com/oauth/token" + apiVersion = 11 + defaultTimeout = 20 * time.Second + maxRetries = 3 ) // NewRingRestClient creates a new Ring client instance func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) { - client := &RingRestClient{ - httpClient: &http.Client{Timeout: defaultTimeout}, - onTokenRefresh: onTokenRefresh, - hardwareID: generateHardwareID(), - auth: auth, - } - - switch a := auth.(type) { - case RefreshTokenAuth: - if a.RefreshToken == "" { - return nil, fmt.Errorf("refresh token is required") - } - + client := &RingRestClient{ + httpClient: &http.Client{Timeout: defaultTimeout}, + onTokenRefresh: onTokenRefresh, + hardwareID: generateHardwareID(), + auth: auth, + } + + switch a := auth.(type) { + case RefreshTokenAuth: + if a.RefreshToken == "" { + return nil, fmt.Errorf("refresh token is required") + } + config, err := parseAuthConfig(a.RefreshToken) - if err != nil { - return nil, fmt.Errorf("failed to parse refresh token: %w", err) - } + if err != nil { + return nil, fmt.Errorf("failed to parse refresh token: %w", err) + } client.authConfig = config - client.hardwareID = config.HID + client.hardwareID = config.HID client.RefreshToken = a.RefreshToken - case EmailAuth: - if a.Email == "" || a.Password == "" { - return nil, fmt.Errorf("email and password are required") - } - default: - return nil, fmt.Errorf("invalid auth type") - } - - return client, nil + case EmailAuth: + if a.Email == "" || a.Password == "" { + return nil, fmt.Errorf("email and password are required") + } + default: + return nil, fmt.Errorf("invalid auth type") + } + + return client, nil } // Request makes an authenticated request to the Ring API @@ -207,7 +207,7 @@ func (c *RingRestClient) Request(method, url string, body interface{}) ([]byte, // Make request with retries var resp *http.Response var responseBody []byte - + for attempt := 0; attempt <= maxRetries; attempt++ { resp, err = c.httpClient.Do(req) if err != nil { @@ -318,104 +318,104 @@ func (c *RingRestClient) ensureAuth() error { // getAuth makes an authentication request to the Ring API func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) { - var grantData map[string]string - - if c.authConfig != nil && twoFactorAuthCode == "" { - grantData = map[string]string{ - "grant_type": "refresh_token", - "refresh_token": c.authConfig.RT, - } - } else { - authEmail, ok := c.auth.(EmailAuth) - if !ok { - return nil, fmt.Errorf("invalid auth type for email authentication") - } - grantData = map[string]string{ - "grant_type": "password", - "username": authEmail.Email, - "password": authEmail.Password, - } - } - - grantData["client_id"] = "ring_official_android" - grantData["scope"] = "client" - - body, err := json.Marshal(grantData) - if err != nil { - return nil, fmt.Errorf("failed to marshal auth request: %w", err) - } - - req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - req.Header.Set("hardware_id", c.hardwareID) - req.Header.Set("User-Agent", "android:com.ringapp") - req.Header.Set("2fa-support", "true") - if twoFactorAuthCode != "" { - req.Header.Set("2fa-code", twoFactorAuthCode) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // Handle 2FA Responses - if resp.StatusCode == http.StatusPreconditionFailed || - (resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) { - - var tfaResp Auth2faResponse - if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil { - return nil, err - } - - c.Using2FA = true - if resp.StatusCode == http.StatusBadRequest { - c.PromptFor2FA = "Invalid 2fa code entered. Please try again." - return nil, fmt.Errorf("invalid 2FA code") - } - - if tfaResp.TSVState != "" { - prompt := "from your authenticator app" - if tfaResp.TSVState != "totp" { - prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState) - } - c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt) - } else { - c.PromptFor2FA = "Please enter the code sent to your text/email" - } - - return nil, fmt.Errorf("2FA required") - } - - // Handle errors - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) - } - - var authResp AuthTokenResponse - if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { - return nil, fmt.Errorf("failed to decode auth response: %w", err) - } - - c.authToken = &authResp - c.authConfig = &AuthConfig{ - RT: authResp.RefreshToken, - HID: c.hardwareID, - } - - c.RefreshToken = encodeAuthConfig(c.authConfig) - if c.onTokenRefresh != nil { - c.onTokenRefresh(c.RefreshToken) - } - - return c.authToken, nil + var grantData map[string]string + + if c.authConfig != nil && twoFactorAuthCode == "" { + grantData = map[string]string{ + "grant_type": "refresh_token", + "refresh_token": c.authConfig.RT, + } + } else { + authEmail, ok := c.auth.(EmailAuth) + if !ok { + return nil, fmt.Errorf("invalid auth type for email authentication") + } + grantData = map[string]string{ + "grant_type": "password", + "username": authEmail.Email, + "password": authEmail.Password, + } + } + + grantData["client_id"] = "ring_official_android" + grantData["scope"] = "client" + + body, err := json.Marshal(grantData) + if err != nil { + return nil, fmt.Errorf("failed to marshal auth request: %w", err) + } + + req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + req.Header.Set("2fa-support", "true") + if twoFactorAuthCode != "" { + req.Header.Set("2fa-code", twoFactorAuthCode) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Handle 2FA Responses + if resp.StatusCode == http.StatusPreconditionFailed || + (resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) { + + var tfaResp Auth2faResponse + if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil { + return nil, err + } + + c.Using2FA = true + if resp.StatusCode == http.StatusBadRequest { + c.PromptFor2FA = "Invalid 2fa code entered. Please try again." + return nil, fmt.Errorf("invalid 2FA code") + } + + if tfaResp.TSVState != "" { + prompt := "from your authenticator app" + if tfaResp.TSVState != "totp" { + prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState) + } + c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt) + } else { + c.PromptFor2FA = "Please enter the code sent to your text/email" + } + + return nil, fmt.Errorf("2FA required") + } + + // Handle errors + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var authResp AuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return nil, fmt.Errorf("failed to decode auth response: %w", err) + } + + c.authToken = &authResp + c.authConfig = &AuthConfig{ + RT: authResp.RefreshToken, + HID: c.hardwareID, + } + + c.RefreshToken = encodeAuthConfig(c.authConfig) + if c.onTokenRefresh != nil { + c.onTokenRefresh(c.RefreshToken) + } + + return c.authToken, nil } // Helper functions for auth config encoding/decoding @@ -542,4 +542,4 @@ func interfaceSlice(slice interface{}) []CameraData { } } return ret -} \ No newline at end of file +} diff --git a/pkg/ring/client.go b/pkg/ring/client.go index c432ecf97..7014213d7 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -16,422 +16,422 @@ import ( ) type Client struct { - api *RingRestClient - ws *websocket.Conn - prod core.Producer - camera *CameraData - dialogID string - sessionID string - wsMutex sync.Mutex - done chan struct{} + api *RingRestClient + ws *websocket.Conn + prod core.Producer + camera *CameraData + dialogID string + sessionID string + wsMutex sync.Mutex + done chan struct{} } type SessionBody struct { - DoorbotID int `json:"doorbot_id"` - SessionID string `json:"session_id"` + DoorbotID int `json:"doorbot_id"` + SessionID string `json:"session_id"` } type AnswerMessage struct { - Method string `json:"method"` // "sdp" - Body struct { - SessionBody - SDP string `json:"sdp"` - Type string `json:"type"` // "answer" - } `json:"body"` + Method string `json:"method"` // "sdp" + Body struct { + SessionBody + SDP string `json:"sdp"` + Type string `json:"type"` // "answer" + } `json:"body"` } type IceCandidateMessage struct { - Method string `json:"method"` // "ice" - Body struct { - SessionBody - Ice string `json:"ice"` - MLineIndex int `json:"mlineindex"` - } `json:"body"` + Method string `json:"method"` // "ice" + Body struct { + SessionBody + Ice string `json:"ice"` + MLineIndex int `json:"mlineindex"` + } `json:"body"` } type SessionMessage struct { - Method string `json:"method"` // "session_created" or "session_started" - Body SessionBody `json:"body"` + Method string `json:"method"` // "session_created" or "session_started" + Body SessionBody `json:"body"` } type PongMessage struct { - Method string `json:"method"` // "pong" - Body SessionBody `json:"body"` + Method string `json:"method"` // "pong" + Body SessionBody `json:"body"` } type NotificationMessage struct { - Method string `json:"method"` // "notification" - Body struct { - SessionBody - IsOK bool `json:"is_ok"` - Text string `json:"text"` - } `json:"body"` + Method string `json:"method"` // "notification" + Body struct { + SessionBody + IsOK bool `json:"is_ok"` + Text string `json:"text"` + } `json:"body"` } type StreamInfoMessage struct { - Method string `json:"method"` // "stream_info" - Body struct { - SessionBody - Transcoding bool `json:"transcoding"` - TranscodingReason string `json:"transcoding_reason"` - } `json:"body"` + Method string `json:"method"` // "stream_info" + Body struct { + SessionBody + Transcoding bool `json:"transcoding"` + TranscodingReason string `json:"transcoding_reason"` + } `json:"body"` } type CloseMessage struct { - Method string `json:"method"` // "close" - Body struct { - SessionBody - Reason struct { - Code int `json:"code"` - Text string `json:"text"` - } `json:"reason"` - } `json:"body"` + Method string `json:"method"` // "close" + Body struct { + SessionBody + Reason struct { + Code int `json:"code"` + Text string `json:"text"` + } `json:"reason"` + } `json:"body"` } type BaseMessage struct { - Method string `json:"method"` - Body map[string]any `json:"body"` + Method string `json:"method"` + Body map[string]any `json:"body"` } // Close reason codes const ( - CloseReasonNormalClose = 0 - CloseReasonAuthenticationFailed = 5 - CloseReasonTimeout = 6 + CloseReasonNormalClose = 0 + CloseReasonAuthenticationFailed = 5 + CloseReasonTimeout = 6 ) func Dial(rawURL string) (*Client, error) { - // 1. Parse URL and validate basic params - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } - - query := u.Query() - encodedToken := query.Get("refresh_token") - deviceID := query.Get("device_id") + // 1. Parse URL and validate basic params + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + encodedToken := query.Get("refresh_token") + deviceID := query.Get("device_id") _, isSnapshot := query["snapshot"] - if encodedToken == "" || deviceID == "" { - return nil, errors.New("ring: wrong query") - } - - // URL-decode the refresh token - refreshToken, err := url.QueryUnescape(encodedToken) - if err != nil { - return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) - } - - // Initialize Ring API client - ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) - if err != nil { - return nil, err - } - - // Get camera details - devices, err := ringAPI.FetchRingDevices() - if err != nil { - return nil, err - } - - var camera *CameraData - for _, cam := range devices.AllCameras { - if fmt.Sprint(cam.DeviceID) == deviceID { - camera = &cam - break - } - } - if camera == nil { - return nil, errors.New("ring: camera not found") - } - - // Create base client - client := &Client{ - api: ringAPI, - camera: camera, - dialogID: uuid.NewString(), - done: make(chan struct{}), - } - - // Check if snapshot request - if isSnapshot { - client.prod = NewSnapshotProducer(ringAPI, camera) - return client, nil - } - - // If not snapshot, continue with WebRTC setup - ticket, err := ringAPI.GetSocketTicket() - if err != nil { - return nil, err - } - - // Create WebSocket connection - wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", - uuid.NewString(), url.QueryEscape(ticket.Ticket)) - - client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{ - "User-Agent": {"android:com.ringapp"}, - }) - if err != nil { - return nil, err - } - - // Create Peer Connection - conf := pion.Configuration{ - ICEServers: []pion.ICEServer{ - {URLs: []string{ - "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", - "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", - "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302", - "stun:stun3.l.google.com:19302", - "stun:stun4.l.google.com:19302", - }}, - }, - ICETransportPolicy: pion.ICETransportPolicyAll, - BundlePolicy: pion.BundlePolicyBalanced, - } - - api, err := webrtc.NewAPI() - if err != nil { - client.ws.Close() - return nil, err - } - - pc, err := api.NewPeerConnection(conf) - if err != nil { - client.ws.Close() - return nil, err - } - - // protect from sending ICE candidate before Offer - var sendOffer core.Waiter - - // protect from blocking on errors - defer sendOffer.Done(nil) - - // waiter will wait PC error or WS error or nil (connection OK) - var connState core.Waiter - - prod := webrtc.NewConn(pc) - prod.FormatName = "ring/webrtc" - prod.Mode = core.ModeActiveProducer - prod.Protocol = "ws" - prod.URL = rawURL - - client.prod = prod - - prod.Listen(func(msg any) { - switch msg := msg.(type) { - case *pion.ICECandidate: - _ = sendOffer.Wait() - - iceCandidate := msg.ToJSON() - - // skip empty ICE candidates - if iceCandidate.Candidate == "" { - return - } - - icePayload := map[string]interface{}{ - "ice": iceCandidate.Candidate, - "mlineindex": iceCandidate.SDPMLineIndex, - } - - if err = client.sendSessionMessage("ice", icePayload); err != nil { - connState.Done(err) - return - } - - case pion.PeerConnectionState: - switch msg { - case pion.PeerConnectionStateConnecting: - case pion.PeerConnectionStateConnected: - connState.Done(nil) - default: - connState.Done(errors.New("ring: " + msg.String())) - } - } - }) - - // Setup media configuration - medias := []*core.Media{ - { - Kind: core.KindAudio, - Direction: core.DirectionSendRecv, - Codecs: []*core.Codec{ - { - Name: "opus", - ClockRate: 48000, - Channels: 2, - }, - }, - }, - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: "H264", - ClockRate: 90000, - }, - }, - }, - } - - // Create offer - offer, err := prod.CreateOffer(medias) - if err != nil { - client.Stop() - return nil, err - } - - // Send offer - offerPayload := map[string]interface{}{ - "stream_options": map[string]bool{ - "audio_enabled": true, - "video_enabled": true, - }, - "sdp": offer, - } - - if err = client.sendSessionMessage("live_view", offerPayload); err != nil { - client.Stop() - return nil, err - } - - sendOffer.Done(nil) - - // Ring expects a ping message every 5 seconds - go client.startPingLoop(pc) - go client.startMessageLoop(&connState) - - if err = connState.Wait(); err != nil { - return nil, err - } - - return client, nil + if encodedToken == "" || deviceID == "" { + return nil, errors.New("ring: wrong query") + } + + // URL-decode the refresh token + refreshToken, err := url.QueryUnescape(encodedToken) + if err != nil { + return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) + } + + // Initialize Ring API client + ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + return nil, err + } + + // Get camera details + devices, err := ringAPI.FetchRingDevices() + if err != nil { + return nil, err + } + + var camera *CameraData + for _, cam := range devices.AllCameras { + if fmt.Sprint(cam.DeviceID) == deviceID { + camera = &cam + break + } + } + if camera == nil { + return nil, errors.New("ring: camera not found") + } + + // Create base client + client := &Client{ + api: ringAPI, + camera: camera, + dialogID: uuid.NewString(), + done: make(chan struct{}), + } + + // Check if snapshot request + if isSnapshot { + client.prod = NewSnapshotProducer(ringAPI, camera) + return client, nil + } + + // If not snapshot, continue with WebRTC setup + ticket, err := ringAPI.GetSocketTicket() + if err != nil { + return nil, err + } + + // Create WebSocket connection + wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", + uuid.NewString(), url.QueryEscape(ticket.Ticket)) + + client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + "User-Agent": {"android:com.ringapp"}, + }) + if err != nil { + return nil, err + } + + // Create Peer Connection + conf := pion.Configuration{ + ICEServers: []pion.ICEServer{ + {URLs: []string{ + "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", + "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", + "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + }}, + }, + ICETransportPolicy: pion.ICETransportPolicyAll, + BundlePolicy: pion.BundlePolicyBalanced, + } + + api, err := webrtc.NewAPI() + if err != nil { + client.ws.Close() + return nil, err + } + + pc, err := api.NewPeerConnection(conf) + if err != nil { + client.ws.Close() + return nil, err + } + + // protect from sending ICE candidate before Offer + var sendOffer core.Waiter + + // protect from blocking on errors + defer sendOffer.Done(nil) + + // waiter will wait PC error or WS error or nil (connection OK) + var connState core.Waiter + + prod := webrtc.NewConn(pc) + prod.FormatName = "ring/webrtc" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL + + client.prod = prod + + prod.Listen(func(msg any) { + switch msg := msg.(type) { + case *pion.ICECandidate: + _ = sendOffer.Wait() + + iceCandidate := msg.ToJSON() + + // skip empty ICE candidates + if iceCandidate.Candidate == "" { + return + } + + icePayload := map[string]interface{}{ + "ice": iceCandidate.Candidate, + "mlineindex": iceCandidate.SDPMLineIndex, + } + + if err = client.sendSessionMessage("ice", icePayload); err != nil { + connState.Done(err) + return + } + + case pion.PeerConnectionState: + switch msg { + case pion.PeerConnectionStateConnecting: + case pion.PeerConnectionStateConnected: + connState.Done(nil) + default: + connState.Done(errors.New("ring: " + msg.String())) + } + } + }) + + // Setup media configuration + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendRecv, + Codecs: []*core.Codec{ + { + Name: "opus", + ClockRate: 48000, + Channels: 2, + }, + }, + }, + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: "H264", + ClockRate: 90000, + }, + }, + }, + } + + // Create offer + offer, err := prod.CreateOffer(medias) + if err != nil { + client.Stop() + return nil, err + } + + // Send offer + offerPayload := map[string]interface{}{ + "stream_options": map[string]bool{ + "audio_enabled": true, + "video_enabled": true, + }, + "sdp": offer, + } + + if err = client.sendSessionMessage("live_view", offerPayload); err != nil { + client.Stop() + return nil, err + } + + sendOffer.Done(nil) + + // Ring expects a ping message every 5 seconds + go client.startPingLoop(pc) + go client.startMessageLoop(&connState) + + if err = connState.Wait(); err != nil { + return nil, err + } + + return client, nil } func (c *Client) startPingLoop(pc *pion.PeerConnection) { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-c.done: - return - case <-ticker.C: - if pc.ConnectionState() == pion.PeerConnectionStateConnected { - if err := c.sendSessionMessage("ping", nil); err != nil { - return - } - } - } - } + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-c.done: + return + case <-ticker.C: + if pc.ConnectionState() == pion.PeerConnectionStateConnected { + if err := c.sendSessionMessage("ping", nil); err != nil { + return + } + } + } + } } func (c *Client) startMessageLoop(connState *core.Waiter) { - var err error - - // will be closed when conn will be closed - defer func() { - connState.Done(err) - }() - - for { - select { - case <-c.done: - return - default: - var res BaseMessage - if err = c.ws.ReadJSON(&res); err != nil { - select { - case <-c.done: - return - default: - } - - c.Stop() - return - } - - // check if "doorbot_id" is present - if _, ok := res.Body["doorbot_id"]; !ok { - continue - } - - // check if the message is from the correct doorbot - doorbotID := res.Body["doorbot_id"].(float64) - if doorbotID != float64(c.camera.ID) { - continue - } - - // check if the message is from the correct session - if res.Method == "session_created" || res.Method == "session_started" { - if _, ok := res.Body["session_id"]; ok && c.sessionID == "" { - c.sessionID = res.Body["session_id"].(string) - } - } - - if _, ok := res.Body["session_id"]; ok { - if res.Body["session_id"].(string) != c.sessionID { - continue - } - } - - rawMsg, _ := json.Marshal(res) - - switch res.Method { - case "sdp": - if prod, ok := c.prod.(*webrtc.Conn); ok { - // Get answer - var msg AnswerMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - c.Stop() - return - } - if err = prod.SetAnswer(msg.Body.SDP); err != nil { - c.Stop() - return - } - if err = c.activateSession(); err != nil { - c.Stop() - return - } - } - - case "ice": - if prod, ok := c.prod.(*webrtc.Conn); ok { - // Continue to receiving candidates - var msg IceCandidateMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - break - } - - // check for empty ICE candidate - if msg.Body.Ice == "" { - break - } - - if err = prod.AddCandidate(msg.Body.Ice); err != nil { - c.Stop() - return - } - } - - case "close": - c.Stop() - return - - case "pong": - // Ignore - continue - } - } - } + var err error + + // will be closed when conn will be closed + defer func() { + connState.Done(err) + }() + + for { + select { + case <-c.done: + return + default: + var res BaseMessage + if err = c.ws.ReadJSON(&res); err != nil { + select { + case <-c.done: + return + default: + } + + c.Stop() + return + } + + // check if "doorbot_id" is present + if _, ok := res.Body["doorbot_id"]; !ok { + continue + } + + // check if the message is from the correct doorbot + doorbotID := res.Body["doorbot_id"].(float64) + if doorbotID != float64(c.camera.ID) { + continue + } + + // check if the message is from the correct session + if res.Method == "session_created" || res.Method == "session_started" { + if _, ok := res.Body["session_id"]; ok && c.sessionID == "" { + c.sessionID = res.Body["session_id"].(string) + } + } + + if _, ok := res.Body["session_id"]; ok { + if res.Body["session_id"].(string) != c.sessionID { + continue + } + } + + rawMsg, _ := json.Marshal(res) + + switch res.Method { + case "sdp": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Get answer + var msg AnswerMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + c.Stop() + return + } + if err = prod.SetAnswer(msg.Body.SDP); err != nil { + c.Stop() + return + } + if err = c.activateSession(); err != nil { + c.Stop() + return + } + } + + case "ice": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Continue to receiving candidates + var msg IceCandidateMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + break + } + + // check for empty ICE candidate + if msg.Body.Ice == "" { + break + } + + if err = prod.AddCandidate(msg.Body.Ice); err != nil { + c.Stop() + return + } + } + + case "close": + c.Stop() + return + + case "pong": + // Ignore + continue + } + } + } } func (c *Client) activateSession() error { @@ -453,7 +453,7 @@ func (c *Client) activateSession() error { func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error { c.wsMutex.Lock() - defer c.wsMutex.Unlock() + defer c.wsMutex.Unlock() if body == nil { body = make(map[string]interface{}) @@ -486,18 +486,18 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { - if media.Kind == core.KindAudio { - // Enable speaker - speakerPayload := map[string]interface{}{ - "stealth_mode": false, - } - _ = c.sendSessionMessage("camera_options", speakerPayload) - } - return webrtcProd.AddTrack(media, codec, track) - } - - return fmt.Errorf("add track not supported for snapshot") + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + if media.Kind == core.KindAudio { + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + _ = c.sendSessionMessage("camera_options", speakerPayload) + } + return webrtcProd.AddTrack(media, codec, track) + } + + return fmt.Errorf("add track not supported for snapshot") } func (c *Client) Start() error { @@ -534,9 +534,9 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { - return webrtcProd.MarshalJSON() - } - + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + return webrtcProd.MarshalJSON() + } + return nil, errors.New("ring: can't marshal") -} \ No newline at end of file +} diff --git a/pkg/ring/snapshot.go b/pkg/ring/snapshot.go index bbf86e284..84da0fd32 100644 --- a/pkg/ring/snapshot.go +++ b/pkg/ring/snapshot.go @@ -8,57 +8,57 @@ import ( ) type SnapshotProducer struct { - core.Connection + core.Connection - client *RingRestClient - camera *CameraData + client *RingRestClient + camera *CameraData } func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer { - return &SnapshotProducer{ - Connection: core.Connection{ - ID: core.NewID(), - FormatName: "ring/snapshot", - Protocol: "https", - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - }, - }, - }, - client: client, - camera: camera, - } + return &SnapshotProducer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "ring/snapshot", + Protocol: "https", + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + client: client, + camera: camera, + } } func (p *SnapshotProducer) Start() error { - // Fetch snapshot - response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) - if err != nil { - return fmt.Errorf("failed to get snapshot: %w", err) - } + // Fetch snapshot + response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) + if err != nil { + return fmt.Errorf("failed to get snapshot: %w", err) + } - pkt := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: response, - } + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: response, + } - // Send to all receivers + // Send to all receivers for _, receiver := range p.Receivers { - receiver.WriteRTP(pkt) - } + receiver.WriteRTP(pkt) + } - return nil + return nil } func (p *SnapshotProducer) Stop() error { - return p.Connection.Stop() -} \ No newline at end of file + return p.Connection.Stop() +} diff --git a/www/add.html b/www/add.html index 7dae63d49..cec8ed369 100644 --- a/www/add.html +++ b/www/add.html @@ -247,7 +247,7 @@ const r = await fetch(url, {cache: 'no-cache'}); const data = await r.json(); - + if (data.needs_2fa) { document.getElementById('tfa-field').style.display = 'block'; document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code'; From 82f6c2c550ce2a004a1600821519a338ae624e26 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 Jan 2025 16:09:50 +0300 Subject: [PATCH 34/34] Add support H264, H265, NV12 for V4L2 source #1546 --- pkg/h264/annexb/annexb_test.go | 12 ++++++++++++ pkg/v4l2/device/device.go | 26 +++++++++++++++++--------- pkg/v4l2/device/formats.go | 26 ++++++++++++++++++++++++-- pkg/v4l2/producer.go | 33 +++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go index 7220f570a..cbc382feb 100644 --- a/pkg/h264/annexb/annexb_test.go +++ b/pkg/h264/annexb/annexb_test.go @@ -83,3 +83,15 @@ func TestDahua(t *testing.T) { n := naluTypes(b) require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) } + +func TestUSB(t *testing.T) { + s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) + + s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C" + b = EncodeToAVCC(decode(s)) + n = naluTypes(b) + require.Equal(t, []byte{0x41}, n) +} diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7f16fd238..c77d60f5b 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -11,8 +11,9 @@ import ( ) type Device struct { - fd int - bufs [][]byte + fd int + bufs [][]byte + pixFmt uint32 } func Open(path string) (*Device, error) { @@ -119,6 +120,8 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) } func (d *Device) SetFormat(width, height, pixFmt uint32) error { + d.pixFmt = pixFmt + f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, pix: v4l2_pix_format{ @@ -196,7 +199,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(planarYUV bool) ([]byte, error) { +func (d *Device) Capture() ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -205,11 +208,16 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - buf := make([]byte, dec.bytesused) - if planarYUV { - YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) - } else { - copy(buf, d.bufs[dec.index][:dec.bytesused]) + src := d.bufs[dec.index][:dec.bytesused] + dst := make([]byte, dec.bytesused) + + switch d.pixFmt { + case V4L2_PIX_FMT_YUYV: + YUYVtoYUV(dst, src) + case V4L2_PIX_FMT_NV12: + NV12toYUV(dst, src) + default: + copy(dst, d.bufs[dec.index][:dec.bytesused]) } enc := v4l2_buffer{ @@ -221,7 +229,7 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - return buf, nil + return dst, nil } func (d *Device) Close() error { diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index fb54bbd1e..a0b410827 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -2,7 +2,10 @@ package device const ( V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 + V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24 + V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24 ) type Format struct { @@ -13,11 +16,13 @@ type Format struct { var Formats = []Format{ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"}, {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, + {V4L2_PIX_FMT_H264, "H.264", "h264"}, + {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"}, } -// YUYV2YUV convert packed YUV to planar YUV -func YUYV2YUV(dst, src []byte) { +func YUYVtoYUV(dst, src []byte) { n := len(src) i0 := 0 iy := 0 @@ -38,3 +43,20 @@ func YUYV2YUV(dst, src []byte) { iv++ } } + +func NV12toYUV(dst, src []byte) { + n := len(src) + k := n / 6 + i0 := k * 4 + iu := i0 + iv := i0 + k + copy(dst, src[:i0]) // copy Y + for i0 < n { + dst[iu] = src[i0] + i0++ + iu++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 87199762d..663d0a9e3 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" "github.com/AlexxIT/go2rtc/pkg/v4l2/device" "github.com/pion/rtp" ) @@ -46,17 +47,29 @@ func Open(rawURL string) (*Producer, error) { } switch query.Get("input_format") { - case "mjpeg": - codec.Name = core.CodecJPEG - pixFmt = device.V4L2_PIX_FMT_MJPEG case "yuyv422": if codec.FmtpLine == "" { return nil, errors.New("v4l2: invalid video_size") } - codec.Name = core.CodecRAW codec.FmtpLine += ";colorspace=422" pixFmt = device.V4L2_PIX_FMT_YUYV + case "nv12": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=420mpeg2" // maybe 420jpeg + pixFmt = device.V4L2_PIX_FMT_NV12 + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "h264": + codec.Name = core.CodecH264 + pixFmt = device.V4L2_PIX_FMT_H264 + case "hevc": + codec.Name = core.CodecH265 + pixFmt = device.V4L2_PIX_FMT_HEVC default: return nil, errors.New("v4l2: invalid input_format") } @@ -93,10 +106,14 @@ func (c *Producer) Start() error { return err } - planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + var bitstream bool + switch c.Medias[0].Codecs[0].Name { + case core.CodecH264, core.CodecH265: + bitstream = true + } for { - buf, err := c.dev.Capture(planarYUV) + buf, err := c.dev.Capture() if err != nil { return err } @@ -107,6 +124,10 @@ func (c *Producer) Start() error { continue } + if bitstream { + buf = annexb.EncodeToAVCC(buf) + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: buf,