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)