-
+
@@ -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,