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.
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 |
|
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
- Strix: current working directory (
/home/user/Strix) - go2rtc:
/home/user/go2rtc(reference implementation, read-only) - StrixCamDB: issues at https://github.com/eduard256/StrixCamDB/issues (for database updates)
Related skills (know when to hand off)
/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 offrigate-config.yaml./add_probe_detector_strix <proto>-- add a device-type detector to/api/probeso 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:
- Database has URL templates like
/cam/realmonitor?channel=[CHANNEL]&subtype=0 replacePlaceholders()substitutes[CHANNEL],[USERNAME],[PASSWORD], etc.buildURL()prependsprotocol://user:pass@host:portto the path- 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
- Read
/home/user/go2rtc/internal/{protocol}/{protocol}.go-- findstreams.HandleFunccall, understand what function is called and how - Read
/home/user/go2rtc/pkg/{protocol}/-- find theDial()orNewClient()function, understand its signature and what it returns - 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/probeor direct user input - Needs:
SourceBlockHandlerregistration, 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>.gofrom go2rtc (adapt imports), add API endpoint for the cloud login flow, build a dedicated HTML page mirroringwww/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://orhttps://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
- go2rtc source: https://github.com/AlexxIT/go2rtc/tree/master/pkg/{PROTOCOL_NAME}
- ispyconnect: search for "{PROTOCOL_NAME}" cameras
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--defaultPortsmap andreplacePlaceholders()function
STEP 5: Add stream handler to tester
Before writing code
- Read ALL existing handlers in
/home/user/Strix/pkg/tester/source.gocompletely - Read the go2rtc pkg/ implementation for this protocol (Step 1)
- 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:
- Takes a URL string
- Calls go2rtc's
pkg/{protocol}.Dial(url)or equivalent - 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
- Handler is registered -- check logs for no errors at startup
- URLs with the new scheme are dispatched to the correct handler
- If Type A: verify
/api/streamsreturns URLs with correct scheme and port - Test with a real device if available
STEP 7: Hand off to related skills
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.yamlembeds the full URL with the token, and a user pasting the config into Frigate directly will leak the secret (plusgo2rtc:xiaomisection won't populate). - Is the device detectable by IP probe? Run
/add_probe_detector_strix <proto>so that/api/probe?ip=Xreturnstype: "<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.goif it's a one-liner (return pkg.Dial(url)) - If handler needs >10 lines of custom logic, create
source_{protocol}.go - Keep
source.goas 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:
connfor connection,prodfor 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:
GetMedias()-- to list codecs (H264, AAC, etc.)GetTrack()+Start()-- to capture screenshot (keyframe)Stop()-- to clean up
How screenshot and resolution work (pkg/tester/worker.go)
getScreenshot(prod)is called after successful Dial- Creates
magic.NewKeyframe()consumer - Matches video media between producer and consumer
- Gets track via
prod.GetTrack() - Starts
prod.Start()in goroutine (blocking -- reads packets) - Waits for first keyframe via
cons.WriteTo()with 10s timeout - If H264/H265 -- converts to JPEG via ffmpeg
- If already JPEG -- uses as-is
jpegSize(jpeg)extracts width and height from JPEG SOF0/SOF2 marker- Resolution stored in
Result.WidthandResult.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 timeouttcp.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/andinternal/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
defaultPortsinstreams.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) andwww/index.htmlrouter 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)