Files
Strix/.claude/skills/add_protocol_strix/SKILL.md
T
eduard256 568e41e72b Update add_protocol_strix skill with B1/B2 split and handoffs
Split Type B into B1 (pairing-based like homekit) and B2 (cloud-auth like
xiaomi/tapo/nest/ring/roborock/tuya). Document the xiaomi copy-from-go2rtc
template with stateless token flow. Add version note about Frigate stable
shipping go2rtc 1.9.10 without xiaomi support. Reference xiaomi and homekit
modules as golden templates. Replace commit step with handoff to
add_generate_strix and add_probe_detector_strix.
2026-04-18 12:32:01 +00:00

23 KiB

name, description, disable-model-invocation, argument-hint
name description disable-model-invocation argument-hint
add_protocol_strix Add a new protocol support to Strix -- full flow from research to implementation. Covers stream handler registration, URL builder updates, database issues, and go2rtc integration. true
protocol-name

Add Protocol to Strix

You are adding support for a new protocol to Strix. Follow every step in order. Be thorough -- read all referenced files completely before writing any code.

The protocol name is provided as argument (e.g. /add_protocol_strix bubble). If no argument, use AskUserQuestion to ask which protocol to add.

Repositories

  • /add_generate_strix <proto> -- register a credentials extractor for the Frigate config generator. Run AFTER this skill if the protocol has tokens/passwords that must go into a separate YAML section of frigate-config.yaml.
  • /add_probe_detector_strix <proto> -- add a device-type detector to /api/probe so the frontend auto-routes a matching IP to your new protocol page.

STEP 0: Understand the existing implementations (REFERENCE)

Before doing anything, read these files completely to understand the patterns:

pkg/tester/source.go              -- handler registry + RTSP reference (Type A)
pkg/tester/worker.go              -- how handlers are called, screenshot logic
pkg/tester/session.go             -- session data structures
pkg/camdb/streams.go              -- URL builder, placeholder replacement
internal/test/test.go             -- API layer for tester
internal/search/search.go         -- search API (rarely needs changes)
internal/xiaomi/xiaomi.go         -- golden reference for Type B with cloud auth + token URLs
internal/homekit/homekit.go       -- reference for Type B with pairing-based custom source blocks

How RTSP works (the reference pattern)

Registration in pkg/tester/source.go:

var handlers = map[string]SourceHandler{}

func RegisterSource(scheme string, handler SourceHandler) {
    handlers[scheme] = handler
}

func init() {
    RegisterSource("rtsp", rtspHandler)
    RegisterSource("rtsps", rtspHandler)
    RegisterSource("rtspx", rtspHandler)
}

Handler -- receives a URL string, returns go2rtc core.Producer:

func rtspHandler(rawURL string) (core.Producer, error) {
    rawURL, _, _ = strings.Cut(rawURL, "#")

    conn := rtsp.NewClient(rawURL)
    conn.Backchannel = false

    if err := conn.Dial(); err != nil {
        return nil, fmt.Errorf("rtsp: dial: %w", err)
    }

    if err := conn.Describe(); err != nil {
        _ = conn.Stop()
        return nil, fmt.Errorf("rtsp: describe: %w", err)
    }

    return conn, nil
}

Data flow: URL -> GetHandler(url) -> handler(url) -> core.Producer -> GetMedias() -> codecs, latency -> getScreenshot() -> jpegSize() -> Result (with width, height)

Key: The handler ONLY needs to return a core.Producer. Everything else (codecs extraction, screenshot capture, session management) is handled automatically by worker.go.

How URLs are built in pkg/camdb/streams.go:

  1. Database has URL templates like /cam/realmonitor?channel=[CHANNEL]&subtype=0
  2. replacePlaceholders() substitutes [CHANNEL], [USERNAME], [PASSWORD], etc.
  3. buildURL() prepends protocol://user:pass@host:port to the path
  4. Credentials are URL-encoded with url.PathEscape / url.QueryEscape

Default ports are defined in defaultPorts map:

var defaultPorts = map[string]int{
    "rtsp": 554, "rtsps": 322, "http": 80, "https": 443,
    "rtmp": 1935, "mms": 554, "rtp": 5004,
}

STEP 1: Research the protocol in go2rtc

go2rtc already implements most camera protocols. Study the implementation:

Where to look in go2rtc

What Where
Protocol client logic ../go2rtc/pkg/{protocol}/
Module registration ../go2rtc/internal/{protocol}/
Core interfaces ../go2rtc/pkg/core/core.go
Stream handler registry ../go2rtc/internal/streams/handlers.go
Keyframe capture ../go2rtc/pkg/magic/keyframe.go

Version note: cloud-auth protocols like xiaomi require go2rtc >= 1.9.13. Frigate stable still ships with go2rtc 1.9.10; Frigate dev/0.18+ upgrades to 1.9.13+. A user on Frigate stable cannot stream a xiaomi camera even if Strix generates a perfect config -- the go2rtc binary inside Frigate will log unsupported scheme. Mention this in your handoff.

Protocol map in go2rtc

Protocol pkg/ (Dial function) internal/ (Init glue)
rtsp/rtsps pkg/rtsp/client.go internal/rtsp/rtsp.go
http/https pkg/magic/producer.go, pkg/tcp/request.go internal/http/http.go
rtmp pkg/rtmp/ internal/rtmp/rtmp.go
bubble pkg/bubble/ internal/bubble/bubble.go
dvrip pkg/dvrip/ internal/dvrip/dvrip.go
onvif pkg/onvif/ internal/onvif/onvif.go
homekit pkg/homekit/, pkg/hap/ internal/homekit/homekit.go
tapo pkg/tapo/ internal/tapo/tapo.go
kasa pkg/kasa/ internal/kasa/kasa.go
eseecloud pkg/eseecloud/ internal/eseecloud/eseecloud.go
nest pkg/nest/ internal/nest/init.go
ring pkg/ring/ internal/ring/ring.go
wyze pkg/wyze/ internal/wyze/wyze.go
xiaomi pkg/xiaomi/ internal/xiaomi/xiaomi.go
tuya pkg/tuya/ internal/tuya/tuya.go
doorbird pkg/doorbird/ internal/doorbird/doorbird.go
isapi pkg/isapi/ internal/isapi/init.go
flussonic pkg/flussonic/ internal/flussonic/flussonic.go
gopro pkg/gopro/ internal/gopro/gopro.go
roborock pkg/roborock/ internal/roborock/roborock.go

What to read

  1. Read /home/user/go2rtc/internal/{protocol}/{protocol}.go -- find streams.HandleFunc call, understand what function is called and how
  2. Read /home/user/go2rtc/pkg/{protocol}/ -- find the Dial() or NewClient() function, understand its signature and what it returns
  3. Understand: does it return core.Producer? Does it need special setup before Dial? Does it need credentials differently?

Typical go2rtc internal module (e.g. kasa -- simplest):

package kasa

import (
    "github.com/AlexxIT/go2rtc/internal/streams"
    "github.com/AlexxIT/go2rtc/pkg/core"
    "github.com/AlexxIT/go2rtc/pkg/kasa"
)

func Init() {
    streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
        return kasa.Dial(source)
    })
}

Most protocols follow this exact pattern: pkg/{protocol}.Dial(url) returns core.Producer.


STEP 2: Classify the protocol

Use AskUserQuestion to discuss with the user. Determine the protocol type:

Type A: Standard URL-based protocol (rtsp, rtmp, bubble, dvrip, http)

  • Has URL scheme (ex. bubble://host:port/path)
  • URLs stored in StrixCamDB database
  • Flow: user searches camera -> gets URL templates -> URLs built with credentials -> sent to tester
  • Needs: stream handler in tester + default port in URL builder + database issue
  • Hands off to: no other skill needed (credentials live in userinfo, captured by the URL itself)

Type B1: Custom pairing / discovery (homekit)

  • Does NOT use URL templates from database
  • mDNS discovery + multi-step pairing (PIN, PSK, etc.)
  • Custom frontend page, custom API endpoint, custom source block in /api/test
  • Data comes from /api/probe or direct user input
  • Needs: SourceBlockHandler registration, pairing endpoint, dedicated HTML page
  • Reference: internal/homekit/homekit.go, www/homekit.html
  • Hands off to: /add_probe_detector_strix (if detectable by IP)

Type B2: Cloud-auth with token URL (xiaomi, tapo, nest, ring, roborock, tuya)

  • Has URL scheme (xiaomi://userID:region@IP?did=X&model=Y&token=T)
  • Credentials come from a cloud API (Mi Cloud, Tapo Cloud, etc.) not from local discovery
  • Stateless design: token is extracted server-side, embedded in URL, then consumed by a generator extractor that moves it to a dedicated YAML section
  • Copy internal/<proto>/<proto>.go from go2rtc (adapt imports), add API endpoint for the cloud login flow, build a dedicated HTML page mirroring www/xiaomi.html
  • Reference: internal/xiaomi/xiaomi.go (copy template), www/xiaomi.html (UI template)
  • Hands off to: /add_generate_strix <proto> for the YAML credentials extractor, /add_probe_detector_strix <proto> if detectable by IP

Type C: HTTP sub-protocol (mjpeg, jpeg snapshot, hls)

  • Uses http:// or https:// URL scheme
  • Already has URLs in database (same as HTTP)
  • Needs special handling in tester based on Content-Type response
  • Needs: stream handler that detects content type and handles accordingly

STEP 3: For Type A -- Create StrixCamDB issue

ONLY for Type A protocols that have URL patterns stored in the database.

Create a GitHub issue using gh CLI for the new protocol:

cd /home/user/Strix
gh issue create --repo eduard256/StrixCamDB \
  --title "[New Protocol] {PROTOCOL_NAME}" \
  --label "new-protocol" \
  --body "$(cat <<'ISSUE_EOF'
```yaml
protocol: {PROTOCOL_NAME}
default_port: {PORT}
url_format: {EXAMPLE_URL_PATTERN}

Description

{DESCRIPTION -- what cameras use this, what firmware, how it works}

Known brands

  • {BRAND1}
  • {BRAND2}

URL patterns

  • {PATTERN1} -- main stream
  • {PATTERN2} -- sub stream

Where to research

Notes

{ANY_NOTES} ISSUE_EOF )"


If the protocol introduces new placeholders (e.g. `[STREAM]`), create a separate issue:

```bash
gh issue create --repo eduard256/StrixCamDB \
  --title "[New Placeholder] {PLACEHOLDER}" \
  --label "new-placeholder" \
  --body "$(cat <<'ISSUE_EOF'
placeholder: "{PLACEHOLDER}"
alternatives: ["{alt1}", "{alt2}"]
description: "{WHAT_IT_DOES}"
example_values: ["{VAL1}", "{VAL2}"]

## URL examples

- {URL_EXAMPLE_1}
- {URL_EXAMPLE_2}

## Known brands using this

- {BRAND1}
- {BRAND2}
ISSUE_EOF
)"

DO NOT wait for issue approval. Continue immediately to the next step.


STEP 4: Update URL builder (Type A only)

If the protocol needs a new default port, edit /home/user/Strix/pkg/camdb/streams.go:

Add the port to defaultPorts map:

var defaultPorts = map[string]int{
    "rtsp": 554, "rtsps": 322, "http": 80, "https": 443,
    "rtmp": 1935, "mms": 554, "rtp": 5004,
    // add new protocol here:
    "bubble": 80,
}

If the protocol needs new placeholders in replacePlaceholders(), add them to the pairs slice. Follow the existing pattern -- both [UPPER] and [lower] variants, plus {curly} variants.

Files to edit for URL builder:

  • /home/user/Strix/pkg/camdb/streams.go -- defaultPorts map and replacePlaceholders() function

STEP 5: Add stream handler to tester

Before writing code

  1. Read ALL existing handlers in /home/user/Strix/pkg/tester/source.go completely
  2. Read the go2rtc pkg/ implementation for this protocol (Step 1)
  3. Understand what the Dial() function needs and returns

For standard protocols (Type A, most Type C)

Most protocols follow the same pattern as RTSP. The handler:

  1. Takes a URL string
  2. Calls go2rtc's pkg/{protocol}.Dial(url) or equivalent
  3. Returns core.Producer

Add the handler to /home/user/Strix/pkg/tester/source.go.

Pattern for simple protocols (bubble, dvrip, rtmp, kasa, etc.):

import "github.com/AlexxIT/go2rtc/pkg/{protocol}"

// in init():
RegisterSource("{scheme}", {scheme}Handler)

// handler:
func {scheme}Handler(rawURL string) (core.Producer, error) {
    return {protocol}.Dial(rawURL)
}

If the protocol needs extra setup before Dial (like RTSP needs Backchannel = false), add it. Study the go2rtc internal module to see what setup is done.

Pattern for protocols that need connection setup (like RTSP):

func {scheme}Handler(rawURL string) (core.Producer, error) {
    rawURL, _, _ = strings.Cut(rawURL, "#")

    conn := {protocol}.NewClient(rawURL)
    // any setup specific to this protocol

    if err := conn.Dial(); err != nil {
        return nil, fmt.Errorf("{scheme}: dial: %w", err)
    }

    // protocol-specific validation (like RTSP Describe)

    return conn, nil
}

For Type B2 -- cloud-auth protocols (xiaomi, tapo, nest, ring, roborock, tuya)

Use xiaomi as the golden reference. These protocols fit the normal tester.RegisterSource contract -- their URL scheme IS routable, you just have to do cloud auth first and embed the resulting token in the URL.

1. Copy internal/<proto>/<proto>.go from go2rtc into Strix. Change imports:

// go2rtc:
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"

// strix:
"github.com/eduard256/strix/internal/api"
"github.com/eduard256/strix/internal/app"
"github.com/eduard256/strix/pkg/tester"

Replace streams.HandleFunc("<proto>", ...) with tester.RegisterSource("<proto>", ...). Drop app.LoadConfig/app.PatchConfig calls -- Strix is stateless, tokens live only in memory + URL (see xiaomi for the pattern).

2. Stream handler extracts token from URL query and seeds the in-memory cache:

tester.RegisterSource("<proto>", func(rawURL string) (core.Producer, error) {
    u, _ := url.Parse(rawURL)
    // seed in-memory tokens cache from the URL so cloud-auth'd functions work
    if token := u.Query().Get("token"); token != "" && u.User != nil {
        ...
    }
    if u.User != nil {
        rawURL, _ = getCameraURL(u) // cloud call for p2p keys
    }
    return <proto>.Dial(rawURL)
})

3. Cloud auth API endpoint -- 4-step flow (username/password -> captcha -> 2FA -> success):

api.HandleFunc("api/<proto>", apiHandler)

See internal/xiaomi/xiaomi.go for the exact switch on GET/POST and the 401+JSON-with-captcha/verify_phone response shape. The frontend mirrors this across several state transitions.

4. Register with main.go:

modules := []module{
    ...
    {"<proto>", <proto>.Init},
}

5. Build a frontend page www/<proto>.html mirroring www/xiaomi.html. It has 6 states: loading, login, captcha, verify, region picker, not found. Also update www/index.html's navigateXiaomi-style router to handle this protocol's probe type.

6. Register credentials extractor with the config generator. Do this in THE SAME Init() by calling /add_generate_strix <proto> (or hand off to that skill). The extractor strips ?token=... from the URL and moves it into a top-level section under go2rtc: in the generated Frigate config.

For Type B1 -- pairing-based protocols (homekit)

These don't fit the URL scheme contract -- data comes from a mDNS discovery plus a user-entered PIN. They use SourceBlockHandler instead of SourceHandler:

1. Define block handler in pkg/tester/source.go:

type SourceBlockHandler func(data json.RawMessage, s *Session)

var sourceHandlers = map[string]SourceBlockHandler{}

func RegisterSourceBlock(name string, handler SourceBlockHandler) {
    sourceHandlers[name] = handler
}

2. Update internal/test/test.go:apiTestCreate() to parse and dispatch custom source blocks alongside sources.streams.

3. Extended request format:

{
  "sources": {
    "streams": ["rtsp://...", "http://..."],
    "homekit": {"device_id": "AA:BB:CC", "pin": "123-45-678"}
  }
}

4. Write the block handler -- parses its params, runs pairing, calls s.AddResult(...) and s.AddTested(...) directly.

Reference: internal/homekit/homekit.go, www/homekit.html.

IMPORTANT: Before starting Type B1, discuss the approach with the user -- pairing flows are rare and each one is custom.


STEP 6: Test the implementation

Build and verify

cd /home/user/Strix
go build ./...

If it compiles, test with the running container:

# rebuild image
docker build -t strix:test .

# restart container
docker rm -f strix
docker run -d --name strix --network host --restart unless-stopped strix:test

# check logs
docker logs strix

# test the new protocol (example for bubble)
curl -s -X POST http://localhost:4567/api/test \
  -H 'Content-Type: application/json' \
  -d '{"sources":{"streams":["bubble://admin:password@192.168.1.100:80/"]}}'

What to verify

  1. Handler is registered -- check logs for no errors at startup
  2. URLs with the new scheme are dispatched to the correct handler
  3. If Type A: verify /api/streams returns URLs with correct scheme and port
  4. Test with a real device if available

Once the tester handler works and the test returns a screenshot, the protocol is NOT fully wired yet. Check what else is needed:

  • Does the URL carry credentials (tokens, passwords)? Run /add_generate_strix <proto> to register the extractor that moves them into a top-level section of the generated Frigate config. Without this, frigate-config.yaml embeds the full URL with the token, and a user pasting the config into Frigate directly will leak the secret (plus go2rtc:xiaomi section won't populate).
  • Is the device detectable by IP probe? Run /add_probe_detector_strix <proto> so that /api/probe?ip=X returns type: "<proto>" and the frontend auto-routes to the protocol page.

Do NOT commit -- leave changes staged for the user to review.


CODE STYLE RULES

All code MUST follow AlexxIT go2rtc style:

File organization

  • One handler per protocol is fine in source.go if it's a one-liner (return pkg.Dial(url))
  • If handler needs >10 lines of custom logic, create source_{protocol}.go
  • Keep source.go as the registry + simple handlers
  • Complex protocols get their own file

Naming

  • Handler: {scheme}Handler (e.g. bubbleHandler, rtmpHandler)
  • Error prefix: "{scheme}: dial: ..." or "{scheme}: ..."
  • Short var names: conn for connection, prod for producer

Error handling

  • Wrap errors with protocol prefix: fmt.Errorf("bubble: dial: %w", err)
  • Close/stop connections on error: _ = conn.Stop()
  • Return nil Producer on error, never a half-initialized one

Comments

  • Comment ONLY if the "why" is not obvious
  • No docstrings on every function
  • Inline examples: // ex. "bubble://admin:pass@192.168.1.100:80/"

Imports

  • go2rtc packages: "github.com/AlexxIT/go2rtc/pkg/{protocol}"
  • Always import "github.com/AlexxIT/go2rtc/pkg/core" for Producer interface
  • Group: stdlib, then go2rtc, then project packages

go2rtc INTERNALS REFERENCE

core.Producer interface (pkg/core/core.go)

Every protocol handler must return something that implements core.Producer:

type Producer interface {
    GetMedias() []*Media    // what tracks are available (video/audio codecs)
    GetTrack(media *Media, codec *Codec) (*Receiver, error)  // get specific track
    Start() error           // start receiving packets (blocking)
    Stop() error            // close connection
}

The tester uses:

  1. GetMedias() -- to list codecs (H264, AAC, etc.)
  2. GetTrack() + Start() -- to capture screenshot (keyframe)
  3. Stop() -- to clean up

How screenshot and resolution work (pkg/tester/worker.go)

  1. getScreenshot(prod) is called after successful Dial
  2. Creates magic.NewKeyframe() consumer
  3. Matches video media between producer and consumer
  4. Gets track via prod.GetTrack()
  5. Starts prod.Start() in goroutine (blocking -- reads packets)
  6. Waits for first keyframe via cons.WriteTo() with 10s timeout
  7. If H264/H265 -- converts to JPEG via ffmpeg
  8. If already JPEG -- uses as-is
  9. jpegSize(jpeg) extracts width and height from JPEG SOF0/SOF2 marker
  10. Resolution stored in Result.Width and Result.Height

This works automatically for ANY protocol that returns a valid core.Producer. You do NOT need to implement screenshot or resolution logic per protocol.

Result struct (pkg/tester/session.go)

type Result struct {
    Source     string   `json:"source"`
    Screenshot string   `json:"screenshot,omitempty"`
    Codecs     []string `json:"codecs,omitempty"`
    Width      int      `json:"width,omitempty"`      // from JPEG screenshot
    Height     int      `json:"height,omitempty"`      // from JPEG screenshot
    LatencyMs  int64    `json:"latency_ms,omitempty"`
    Skipped    bool     `json:"skipped,omitempty"`
}

Resolution is extracted from the JPEG screenshot, not from SDP or protocol-specific data. This means width/height are only available when a screenshot was successfully captured. The frontend uses these values to classify streams as Main (HD) or Sub (SD).

magic.NewKeyframe() (pkg/magic/keyframe.go)

Captures first video keyframe from any Producer. Supports H264, H265, JPEG, MJPEG. The tester uses this -- you never call it directly from a protocol handler.

Connection patterns in go2rtc

Simple Dial (most protocols):

// pkg/bubble/client.go
func Dial(rawURL string) (core.Producer, error) {
    // parse URL, connect, return producer
}

Client with setup (rtsp):

// pkg/rtsp/client.go
conn := rtsp.NewClient(rawURL)
conn.Backchannel = false  // optional setup
conn.Dial()               // TCP connect
conn.Describe()           // RTSP DESCRIBE (gets SDP)
// conn is now a Producer

HTTP-based (complex -- content type detection):

// pkg/magic/producer.go
// Opens HTTP connection, detects Content-Type:
// - multipart/x-mixed-replace -> MJPEG
// - image/jpeg -> single JPEG frame
// - application/vnd.apple.mpegurl -> HLS
// - video/mp2t -> MPEG-TS
// - etc.

TCP/TLS connection (pkg/tcp/)

Many protocols use pkg/tcp for low-level connection:

  • tcp.Dial(rawURL) -- TCP connect with timeout
  • tcp.Client -- HTTP client with digest/basic auth
  • Used by RTSP, HTTP, and others internally

CHECKLIST BEFORE FINISHING

  • Read all existing protocol handlers in pkg/tester/source.go
  • Read xiaomi (internal/xiaomi/xiaomi.go) AND homekit (internal/homekit/homekit.go) as references
  • Read go2rtc pkg/ and internal/ for this protocol
  • Determined protocol type (A / B1 / B2 / C)
  • For Type A: created StrixCamDB issue (protocol + placeholders if needed)
  • For Type A: added default port to defaultPorts in streams.go (if not already there)
  • Added handler registration (or full internal/<proto>/ module for B2)
  • Handler follows established pattern (RTSP for A, xiaomi for B2, homekit for B1)
  • Error messages prefixed with protocol name
  • Connections closed on error
  • go build ./... compiles
  • For B2: frontend page created (www/<proto>.html) and www/index.html router updated
  • For B2: /add_generate_strix <proto> run to register the credentials extractor
  • For detectable protocols: /add_probe_detector_strix <proto> run
  • Changes LEFT STAGED (not committed -- user will review)