Documentation
¶
Overview ¶
Package camfarm serves synthetic RTSP video streams and answers the control-plane requests that camera-discovery code uses to find and drive a fleet, so that video-analytics pipelines can be tested without physical hardware.
Its distinguishing property is deterministic fault injection: a fault fires because a seed said it would, at a reproducible point in the stream, and the same seed and fleet spec reproduce it exactly.
This version serves RTSP media for a fleet of cameras and carries the seed and fault-injection seams throughout, with every catalogued fault deliberately inert: a fault named in a spec is refused rather than silently ignored. The control plane and the fault effects follow. The architecture is recorded in docs/superpowers/specs/2026-09-05-camfarm-architecture-design.md.
Index ¶
- Constants
- Variables
- type Addrs
- type AuthSpec
- type Camera
- func (c *Camera) Auth() (user, pass string, ok bool)
- func (c *Camera) Clear() error
- func (c *Camera) Faults() []FaultSpec
- func (c *Camera) ID() string
- func (c *Camera) Inject(f FaultSpec) error
- func (c *Camera) LocalRTSPURL() string
- func (c *Camera) ONVIFEndpoint() string
- func (c *Camera) ONVIFUUID() string
- func (c *Camera) RTSPURL() string
- func (c *Camera) Screenshot(ctx context.Context) (image.Image, error)
- func (c *Camera) Spec() CameraSpec
- func (c *Camera) Stats() Stats
- type CameraSpec
- type DiscoverySpec
- type Event
- type EventType
- type FaultCount
- type FaultSpec
- type Fleet
- func (f *Fleet) AddCamera(spec CameraSpec) (*Camera, error)
- func (f *Fleet) Addr() Addrs
- func (f *Fleet) Camera(id string) (*Camera, error)
- func (f *Fleet) Close() error
- func (f *Fleet) DiscoveryAddr() string
- func (f *Fleet) Journal() []JournalEntry
- func (f *Fleet) List() []Status
- func (f *Fleet) RemoveCamera(id string) error
- func (f *Fleet) ReplayLine() string
- func (f *Fleet) Seed() uint64
- func (f *Fleet) SpecHash() string
- func (f *Fleet) Subscribe(ctx context.Context) <-chan Event
- type JournalEntry
- type ListenSpec
- type SourceKind
- type SourceSpec
- type Spec
- type Stats
- type Status
- type TB
- type VideoSpec
Constants ¶
const ( // DiscoveryMulticast joins the WS-Discovery multicast group. DiscoveryMulticast = "multicast" // DiscoveryUnicast binds a unicast UDP socket, for tests and CI where // multicast is unavailable. DiscoveryUnicast = "unicast" )
Discovery modes.
Variables ¶
var ( // ErrUnknownCamera means no camera with the given ID exists in the fleet. ErrUnknownCamera = errors.New("camfarm: unknown camera") // ErrUnsupported means this build cannot do what was asked. It is not a // malfunction and not a transient condition. ErrUnsupported = errors.New("camfarm: unsupported") // ErrNotReady means the fleet or camera has not finished initialising. ErrNotReady = errors.New("camfarm: not ready") // ErrFaultActive means the request was refused *because* a fault is // deliberately in effect. // // This must never read as a malfunction. "This camera is lying on purpose" // and "this camera is broken" being indistinguishable would make the whole // tool useless. ErrFaultActive = errors.New("camfarm: refused by an active fault") // ErrDuplicateCamera means a camera with that ID already exists. ErrDuplicateCamera = errors.New("camfarm: duplicate camera") // ErrInvalidSpec means a spec was malformed or out of range, as opposed to // merely unsupported by this build. ErrInvalidSpec = errors.New("camfarm: invalid spec") )
The error taxonomy. These are kept distinct because conflating them is a known pain for consumers of virtual cameras: a caller that cannot tell "this build cannot do that" from "this is not ready yet" has to guess.
Functions ¶
This section is empty.
Types ¶
type AuthSpec ¶
type AuthSpec struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
AuthSpec configures a camera's ONVIF HTTP Digest challenge.
The zero value means open — consistent with the pattern VideoSpec already establishes ("zero fields mean whatever"), and consistent with RTSP itself having no auth today. A camera only challenges when both fields are non-empty.
type Camera ¶
type Camera struct {
// contains filtered or unexported fields
}
Camera is one camera in a running fleet.
func (*Camera) Clear ¶
Clear removes every fault from a camera. With no faults active this succeeds and does nothing: asking for the state that already holds is not an error.
func (*Camera) Inject ¶
Inject adds a fault to a running camera, replacing any active fault of the same kind. A per-frame kind takes effect at the camera's next frame boundary, a control-plane kind (rtsp_error, sdp_lie, teardown) on the next request. Only implemented kinds are accepted; the rest return ErrUnsupported.
Faults come in two classes, per-frame (the pump) and per-request (the control engine). An inject or clear rebuilds the engine of every class whose fault list changed, from the camera seed, so that class's random streams restart at their first draw and keyframe_starvation's keyframe counter restarts too (the next keyframe counts as the first and is delivered). A class whose list did not change is left alone, and injecting a fault identical to the active one is a no-op that journals nothing. A replay must restart a class's streams at each journaled inject or clear of that class (see JournalEntry.Clock).
func (*Camera) LocalRTSPURL ¶
LocalRTSPURL returns a URL for dialing the camera from this process: the bound address rather than the advertised host, which may not resolve from inside a container.
func (*Camera) ONVIFEndpoint ¶
ONVIFEndpoint returns the camera's ONVIF base URL, e.g. "http://127.0.0.1:PORT/onvif/front-door". Append "/device" or "/media" for the two services this version serves.
func (*Camera) ONVIFUUID ¶
ONVIFUUID is the camera's ONVIF endpoint UUID. It is deterministic from the camera seed. The camera's ONVIF Device service returns it from GetEndpointReference with a "urn:uuid:" prefix; this accessor, the API and the dashboard show it bare.
func (*Camera) Screenshot ¶
Screenshot returns one decoded frame from this camera's live RTSP stream.
It dials the camera's own RTSP server on its bound address (not the advertised host, which may not resolve here), so it exercises the real wire path, and blocks until the stream's next random access point (bounded by one loop of the source). It returns ErrNotReady if the fleet's RTSP server is not serving, and ctx's own error if ctx ends first.
func (*Camera) Spec ¶
func (c *Camera) Spec() CameraSpec
Spec returns the camera's spec as it was added, with its currently active faults.
type CameraSpec ¶
type CameraSpec struct {
// ID is operator-chosen and appears in the camera's RTSP path.
ID string `json:"id"`
Source SourceSpec `json:"source,omitempty"`
Video VideoSpec `json:"video,omitempty"`
Faults []FaultSpec `json:"faults,omitempty"`
Auth AuthSpec `json:"auth,omitempty"`
}
CameraSpec describes one camera.
type DiscoverySpec ¶
type DiscoverySpec struct {
// Mode is "multicast" or "unicast".
Mode string `json:"mode"`
// Interface selects the interface (name or IP) that joins the multicast
// group. Multicast mode only.
Interface string `json:"interface,omitempty"`
// Addr is the UDP address to bind in unicast mode, e.g. 127.0.0.1:0.
// Required for unicast; forbidden for multicast.
Addr string `json:"addr,omitempty"`
// AnnounceAddr is where Hello and Bye go. Unicast mode only; empty
// disables announcements.
AnnounceAddr string `json:"announceAddr,omitempty"`
}
DiscoverySpec configures the WS-Discovery responder.
type Event ¶
type Event struct {
Seq uint64 `json:"seq"`
Type EventType `json:"type"`
Camera string `json:"camera"`
Fault *FaultSpec `json:"fault,omitempty"`
// FrameIndex is the frame a fault change took effect on; -1 for add/remove.
FrameIndex int `json:"frameIndex"`
// Clock names what FrameIndex counts: "frame" or "request". Empty for
// add/remove.
Clock string `json:"clock,omitempty"`
}
Event is one change to a running fleet.
type FaultCount ¶
FaultCount is one fault kind's tally.
type FaultSpec ¶
type FaultSpec struct {
// Kind names a catalogued fault.
Kind string `json:"kind"`
// Rate is the probability for rate-based faults, in [0,1].
Rate float64 `json:"rate,omitempty"`
// ProtectKeyframes never applies frame_drop or packet_loss to a
// random-access frame.
ProtectKeyframes bool `json:"protectKeyframes,omitempty"`
// MeanBurst, for packet_loss, selects burst loss when greater than 1.
MeanBurst float64 `json:"meanBurst,omitempty"`
// KeepEvery, for keyframe_starvation, delivers every Nth keyframe.
KeepEvery int `json:"keepEvery,omitempty"`
// On, for rtsp_error, names the failing method: describe, setup, play or all.
On string `json:"on,omitempty"`
// Status, for rtsp_error, is the RTSP status code of the reply.
Status int `json:"status,omitempty"`
// OnlyRequest, for rtsp_error and sdp_lie, fires on the Nth matching request only.
OnlyRequest int `json:"onlyRequest,omitempty"`
// AfterFrames, for teardown, is the frames delivered before teardown.
AfterFrames int `json:"afterFrames,omitempty"`
// Mode selects a variant of teardown (close, reset) or sdp_lie (codec,
// sprop, resolution, contentBase).
Mode string `json:"mode,omitempty"`
// MaxMs, for jitter, bounds each frame's random delay in milliseconds.
MaxMs int `json:"maxMs,omitempty"`
// HoldFrames, for burst_delivery, is the frames held before a burst.
HoldFrames int `json:"holdFrames,omitempty"`
// ReorderDepth, for pts_dts_mismatch, is how far timestamps run out of order.
ReorderDepth int `json:"reorderDepth,omitempty"`
// DelayMs, for pts_delayed, is a constant timestamp lag in milliseconds.
DelayMs int `json:"delayMs,omitempty"`
// DriftPpm, for pts_delayed, is a timestamp drift in parts per million.
DriftPpm int `json:"driftPpm,omitempty"`
}
FaultSpec configures one fault.
type Fleet ¶
type Fleet struct {
// contains filtered or unexported fields
}
Fleet is a running set of synthetic cameras.
func StartT ¶
StartT brings up a fleet for a test: ephemeral ports, silent by default, and closed automatically when the test finishes.
func (*Fleet) AddCamera ¶
func (f *Fleet) AddCamera(spec CameraSpec) (*Camera, error)
AddCamera starts a new camera on a running fleet. It applies the same validation Start does, so a spec that would be refused at Start is refused here too.
Its seed is derived from a monotonically increasing index that never resets, even across RemoveCamera calls, so a dynamically added camera's seed is a pure function of call order -- reproducible if that order is logged, per CLAUDE.md's "Determinism is the product."
func (*Fleet) DiscoveryAddr ¶
DiscoveryAddr returns the bound WS-Discovery UDP address, or "" when discovery is disabled.
func (*Fleet) Journal ¶
func (f *Fleet) Journal() []JournalEntry
Journal returns the runtime changes made so far, oldest first.
func (*Fleet) RemoveCamera ¶
RemoveCamera stops and removes a camera from a running fleet.
It is best effort once id is known to exist: there is nothing a caller could sensibly do to retry a partial removal, so both the RTSP and ONVIF sides are always attempted regardless of whether the other failed.
func (*Fleet) ReplayLine ¶
ReplayLine returns the one line a failing test should print to make its failure reproducible.
type JournalEntry ¶
type JournalEntry struct {
Seq uint64 `json:"seq"`
Action string `json:"action"` // add, remove, inject, clear
Camera string `json:"camera"`
Fault FaultSpec `json:"fault"`
// FrameIndex is when a fault change landed. It depends on when the change
// was requested, which is the limit of replaying an interactive run.
FrameIndex int `json:"frameIndex"`
// Clock says whether FrameIndex counts frames (per-frame faults) or RTSP
// requests (control-plane faults). Part of the replay hash.
Clock string `json:"clock,omitempty"`
}
JournalEntry records one runtime change. Start-time cameras and faults are not journaled: the spec hash already identifies them.
type ListenSpec ¶
type ListenSpec struct {
// Host defaults to 127.0.0.1, so a test fixture is not exposed to the
// network by accident.
Host string `json:"host,omitempty"`
// AdvertiseHost, when set, is the host written into every URL a client is
// told to dial (RTSP URLs, ONVIF endpoints and XAddrs, stream URIs,
// WS-Discovery XAddrs, the dashboard's links), instead of Host. Host
// stays the bind address. It is required when Host is a wildcard
// (0.0.0.0 or ::), as in a container. It must be a bare host or IP. The
// ports in those URLs are the real bound ports, so a published container
// port must equal the container port, or the ports must be fixed.
AdvertiseHost string `json:"advertiseHost,omitempty"`
// RTSPPort of zero requests an ephemeral port, readable back via
// Fleet.Addr.
RTSPPort int `json:"rtspPort,omitempty"`
// ONVIFPort of zero requests an ephemeral port for the ONVIF HTTP
// listener, readable back via Camera.ONVIFEndpoint.
ONVIFPort int `json:"onvifPort,omitempty"`
// Discovery, when set, makes the fleet answer WS-Discovery probes. Nil
// (the default) leaves discovery off and the spec's hash unchanged.
// Enabling it changes Spec.Hash() and therefore the replay line, so a run
// with discovery on does not share a replay line with the same fleet
// without it.
Discovery *DiscoverySpec `json:"discovery,omitempty"`
}
ListenSpec describes what the fleet binds.
type SourceKind ¶
type SourceKind string
SourceKind selects where a camera's video comes from.
const ( // SourceFixture uses the media bundled with camfarm. It is the default. SourceFixture SourceKind = "fixture" // SourceFile reads an MPEG-TS file from disk. SourceFile SourceKind = "file" )
Source kinds.
type SourceSpec ¶
type SourceSpec struct {
// Kind defaults to SourceFixture.
Kind SourceKind `json:"kind,omitempty"`
// Path is required when Kind is SourceFile.
Path string `json:"path,omitempty"`
}
SourceSpec describes a camera's media source.
type Spec ¶
type Spec struct {
// Seed roots every random decision the fleet makes.
Seed uint64 `json:"seed"`
Cameras []CameraSpec `json:"cameras"`
Listen ListenSpec `json:"listen,omitempty"`
// Log receives fleet events. Nil means silent, which is the default: a
// consumer may run camfarm inside a process whose stdout carries a protocol
// stream, so nothing is ever written there.
Log *slog.Logger `json:"-"`
}
Spec describes a fleet.
type Stats ¶
type Stats struct {
// Seed is this camera's derived seed, not the fleet root.
Seed uint64
// FramesServed counts access units served. It is monotonic across loops of
// the source, so it doubles as the camera's position in the run.
FramesServed uint64
// LoopsCompleted counts completed passes over the source media.
LoopsCompleted uint64
// FaultsFired counts fault decisions that have fired for this camera. For
// packet_loss it counts frames that had loss, not packets; use FaultCounts
// for packet counts.
FaultsFired uint64
// FaultCounts tallies each fault kind's effects: frames dropped, packets
// or keyframes withheld (Applied) and decisions spared by
// ProtectKeyframes (Suppressed). Never nil.
FaultCounts map[string]FaultCount
// OutboundRTPPackets counts RTP packets sent on this camera's stream,
// read from the RTSP stream's own counters.
OutboundRTPPackets uint64
// OutboundBytes counts bytes sent on this camera's stream, read from the
// RTSP stream's own counters.
OutboundBytes uint64
// Readers is the number of sessions currently set up against this
// camera. Unlike the other fields, it is a live count, not a running
// total.
Readers int
}
Stats are a camera's counters, correlated with the seed that produced them.
type Status ¶
type Status struct {
ID string
RTSPURL string
Source string
Codec string
Width int
Height int
FPS float64
Readers int
}
Status is a camera's current state.
type TB ¶
TB is the part of *testing.T that StartT needs.
An interface rather than *testing.T so that this library does not import testing, which would register test flags in every consumer's binary. *testing.T and *testing.B both satisfy it.
type VideoSpec ¶
type VideoSpec struct {
Codec string `json:"codec,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
FPS float64 `json:"fps,omitempty"`
}
VideoSpec describes the video a camera should produce.
Zero fields mean "whatever the source provides". A non-zero field is a request for that output value, which this version can satisfy only when it already matches the source, because it can pass a source through but not transform it. Any other value is refused with ErrUnsupported.
Note for callers pinning this API: these fields name the *desired output*, so a spec that is refused today will be satisfied by rescaling or transcoding in a later version rather than continuing to be refused. That is a deliberate trade, taken for a smaller API surface than a separate output block would give; the refusal messages name the behaviour that will eventually replace them so the change is legible before it happens.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
camfarm
command
Command camfarm (CamFarm) runs a simulated camera fleet with a web dashboard.
|
Command camfarm (CamFarm) runs a simulated camera fleet with a web dashboard. |
|
internal
|
|
|
buildinfo
Package buildinfo reports which build of camfarm is running.
|
Package buildinfo reports which build of camfarm is running. |
|
clock
Package clock supplies the time source the media pump is paced by.
|
Package clock supplies the time source the media pump is paced by. |
|
dashboard
Package dashboard serves an HTTP API and an embedded web page that drive a camfarm fleet.
|
Package dashboard serves an HTTP API and an embedded web page that drive a camfarm fleet. |
|
fault
Package fault decides, deterministically, when a camera should misbehave.
|
Package fault decides, deterministically, when a camera should misbehave. |
|
hostmetrics
Package hostmetrics samples host CPU, memory and GPU load plus camfarm's own process usage.
|
Package hostmetrics samples host CPU, memory and GPU load plus camfarm's own process usage. |
|
library
Package library is the set of video files the farm can serve: directories ("roots") that are scanned, an optional upload target, and a validity report per file.
|
Package library is the set of video files the farm can serve: directories ("roots") that are scanned, an optional upload target, and a validity report per file. |
|
media
Package media turns a source of encoded video into the immutable slice of access units the RTSP pumps serve.
|
Package media turns a source of encoded video into the immutable slice of access units the RTSP pumps serve. |
|
media/fixture
Package fixture holds the bundled default media.
|
Package fixture holds the bundled default media. |
|
obs
Package obs records what a fleet did, so a test can assert it and a failing test can report enough to reproduce it.
|
Package obs records what a fleet did, so a test can assert it and a failing test can report enough to reproduce it. |
|
onvif
internal/onvif/auth.go
|
internal/onvif/auth.go |
|
quality
Package quality runs rtspeek stream analyses against a fleet's own cameras and keeps the latest result per camera in memory.
|
Package quality runs rtspeek stream analyses against a fleet's own cameras and keeps the latest result per camera in memory. |
|
rtsp
Package rtsp serves the fleet's synthetic streams.
|
Package rtsp serves the fleet's synthetic streams. |
|
screenshot
Package screenshot grabs one decoded frame from a live RTSP stream.
|
Package screenshot grabs one decoded frame from a live RTSP stream. |
|
seed
Package seed derives the deterministic random streams camfarm runs on.
|
Package seed derives the deterministic random streams camfarm runs on. |


