Skip to content

Latest commit

 

History

History
1429 lines (1173 loc) · 52.2 KB

File metadata and controls

1429 lines (1173 loc) · 52.2 KB

HTTP API Contract

Purpose

  • This document is the source of truth for the runtime HTTP API.
  • Any added, changed, or removed HTTP endpoint behavior must be reflected here.

Design Principles

  • Keep endpoints lean and focused on direct operational value.
  • Accepted /api/v1/set and /api/v1/defaults changes are persisted to the registered config path before the response returns. Manual /api/v1/restart still reloads exactly what is already on disk.
  • Keep JSON payloads simple and descriptive.
  • Keep mutability semantics explicit:
    • live — applied immediately without pipeline restart.
    • restart_required — triggers automatic pipeline reinit (teardown + rebuild).
    • read_only — cannot be changed via API.

Contract Version

  • contract_version: 0.10.1
  • status: active

Governance Rules

  • Non-breaking changes: add optional fields, add new endpoints, extend enum values.
  • Breaking changes: remove endpoints, rename fields, change required field semantics.
  • For every breaking change: increment contract major version, add migration note, update HISTORY.md.
  • For every non-breaking change: increment contract minor/patch version, update this file.

Transport And Format

  • HTTP/1.0, all methods use GET (compatible with BusyBox wget)
  • Default port: 80 (configurable via system.web_port in config)
  • Response content type: application/json; charset=UTF-8
  • Query parameters: field name is the key, value (if any) follows =

Standard Response Envelope

Success

{
  "ok": true,
  "data": {}
}

Error

{
  "ok": false,
  "error": {
    "code": "string_code",
    "message": "human readable message"
  }
}

Error Codes

Code HTTP Status Meaning
invalid_request 400 Missing or malformed parameters
validation_failed 400/409 Value rejected by field or config validation
not_found 404 Unknown field or route
record_active 409 Action blocked while recording is in progress
not_implemented 501 Apply callback not available for this field
internal_error 500 Server-side failure

Endpoints

GET /api/v1/version

Return app, backend, schema, and contract version information.

curl http://<device-ip>/api/v1/version

Response 200:

{
  "ok": true,
  "data": {
    "app_version": "0.1.7",
    "contract_version": "0.10.1",
    "config_schema_version": "1.0.0",
    "backend": "star6e"
  }
}

GET /api/v1/config

Return the full active runtime config.

curl http://<device-ip>/api/v1/config

Response 200:

{
  "ok": true,
  "data": {
    "config": {
      "system": { "webPort": 80, "overclockLevel": 2, "verbose": false },
      "sensor": { "index": -1, "mode": -1, "unlockEnabled": true, "..." : "..." },
      "isp": { "sensorBin": "/etc/sensors/imx415_greg_fpvXVIII-gpt200.bin", "legacyAe": false, "aeFps": 15, "gainMax": 0, "awbMode": "auto", "awbCt": 5500, "keepAspect": true },
      "image": { "mirror": false, "flip": false, "rotate": 0 },
      "video0": { "codec": "h265", "rcMode": "cbr", "fps": 90, "size": "auto", "bitrate": 8192, "gopSize": 1.0, "qpDelta": 0, "frameLost": true, "sceneThreshold": 0, "sceneHoldoff": 2, "intraRefreshMode": "off", "intraRefreshLines": 0, "intraRefreshQp": 0, "zoomPct": 0.0, "zoomX": 0.5, "zoomY": 0.5 },
      "outgoing": { "enabled": true, "server": "udp://192.168.2.20:5600", "streamMode": "rtp", "maxPayloadSize": 1400, "connectedUdp": false },
      "fpv": { "roiEnabled": true, "roiQp": 0, "roiSteps": 2, "roiCenter": 0.25, "noiseLevel": 0 },
      "record": { "enabled": false, "mode": "off", "dir": "/tmp/sdcard", "format": "ts", "maxSeconds": 300, "maxMB": 500 },
      "debug": { "showOsd": false }
    },
    "runtime": {
      "active_precrop": { "x": 0, "y": 240, "w": 2560, "h": 1440 }
    }
  }
}

The runtime block is read-only and reports pipeline state that is not part of the editable config:

  • active_precrop — VIF crop rectangle currently programmed (includes any sensor overscan offsets or SCL crop origin). Present whenever a Star6E or Maruko pipeline has been started; absent before pipeline start or after pipeline stop.

GET /api/v1/capabilities

Return per-field mutability and backend support.

curl http://<device-ip>/api/v1/capabilities

Response 200:

{
  "ok": true,
  "data": {
    "fields": {
      "video0.bitrate": { "mutability": "live", "supported": true },
      "video0.fps": { "mutability": "live", "supported": true },
      "video0.gop_size": { "mutability": "live", "supported": true },
      "video0.qp_delta": { "mutability": "live", "supported": true },
      "video0.codec": { "mutability": "restart_required", "supported": true },
      "video0.size": { "mutability": "restart_required", "supported": true },
      "video0.scene_threshold": { "mutability": "restart_required", "supported": true },
      "video0.scene_holdoff": { "mutability": "restart_required", "supported": true },
      "video0.intra_refresh_mode": { "mutability": "restart_required", "supported": true },
      "video0.intra_refresh_lines": { "mutability": "restart_required", "supported": true },
      "video0.intra_refresh_qp": { "mutability": "restart_required", "supported": true },
      "video0.zoom_pct": { "mutability": "restart_required", "supported": true },
      "video0.zoom_x": { "mutability": "live", "supported": true },
      "video0.zoom_y": { "mutability": "live", "supported": true },
      "system.verbose": { "mutability": "live", "supported": true },
      "outgoing.enabled": { "mutability": "live", "supported": true },
      "outgoing.server": { "mutability": "live", "supported": true },
      "outgoing.stream_mode": { "mutability": "restart_required", "supported": true },
      "outgoing.connected_udp": { "mutability": "restart_required", "supported": true },
      "fpv.roi_qp": { "mutability": "live", "supported": true }
    }
  }
}

(truncated — all fields listed in actual response)

supported is backend-specific. Current Star6E and Maruko builds both expose scene detection, intra refresh, and digital zoom fields.

GET /api/v1/config.json

Majestic-compatible alias of /api/v1/config.

curl http://<device-ip>/api/v1/config.json

GET /api/v1/get?<field_name>

Read a single config field. The field name is the query parameter key (no value needed).

# Read current bitrate
curl "http://<device-ip>/api/v1/get?video0.bitrate"

# Read current codec
curl "http://<device-ip>/api/v1/get?video0.codec"

# Read current qpDelta
curl "http://<device-ip>/api/v1/get?video0.qp_delta"

# Read a string field
curl "http://<device-ip>/api/v1/get?isp.sensor_bin"

Response 200:

{"ok":true,"data":{"field":"video0.bitrate","value":8192}}
{"ok":true,"data":{"field":"video0.codec","value":"h265"}}
{"ok":true,"data":{"field":"video0.qp_delta","value":0}}
{"ok":true,"data":{"field":"isp.sensor_bin","value":"/etc/sensors/imx415_greg_fpvXVIII-gpt200.bin"}}

Error 400 — missing field name:

{"ok":false,"error":{"code":"invalid_request","message":"missing query parameter (field name)"}}

Error 404 — unknown field:

{"ok":false,"error":{"code":"not_found","message":"unknown config field"}}

Majestic-style camelCase aliases are also accepted for selected fields, including fpv.roiQp, fpv.roiEnabled, fpv.roiSteps, fpv.roiCenter, fpv.noiseLevel, isp.sensorBin, isp.awbMode, isp.awbCt, isp.keepAspect, video0.rcMode, video0.gopSize, video0.qpDelta, video0.sceneThreshold, video0.sceneHoldoff, video0.intraRefreshMode, video0.intraRefreshLines, video0.intraRefreshQp, video0.zoomPct, video0.zoomX, video0.zoomY, outgoing.maxPayloadSize, outgoing.audioPort, system.webPort, and system.overclockLevel.

GET /api/v1/set?<field_name>=<value>

Write a config field. The field name is the query key, the new value follows =.

Live fields (mutability: "live") are applied immediately without pipeline restart:

# Change bitrate to 4096 kbps
curl "http://<device-ip>/api/v1/set?video0.bitrate=4096"

# Change FPS
curl "http://<device-ip>/api/v1/set?video0.fps=60"

# Swap ISP tuning bin (empty = auto-detect /etc/sensors/<sensor>.bin)
curl "http://<device-ip>/api/v1/set?isp.sensorBin=/etc/sensors/imx415_fpv.bin"

# Change GOP interval (seconds between keyframes; 0 = all-intra)
curl "http://<device-ip>/api/v1/set?video0.gop_size=0.5"

# Bias relative I-frame QP (Majestic-compatible range: -12..12)
curl "http://<device-ip>/api/v1/set?video0.qp_delta=-4"

# Pan within the active digital zoom crop
curl "http://<device-ip>/api/v1/set?video0.zoomX=0.25&video0.zoomY=0.75"

# Apply multiple live fields atomically in one request
curl "http://<device-ip>/api/v1/set?video0.bitrate=4096&system.verbose=true"

# Coupled live timing changes can be sent together
curl "http://<device-ip>/api/v1/set?video0.fps=30&video0.gopSize=1.0"

When video0.scene_threshold is non-zero, the inline scene detector tracks frame size EMA and requests IDR after scene change spikes settle.

If a GET /api/v1/set request contains multiple key=value pairs joined by &, every field must be live. Mixed live + restart requests are rejected. Duplicate fields are also rejected after alias canonicalization, so video0.qp_delta and video0.qpDelta cannot appear in the same batch.

Response 200:

{"ok":true,"data":{"field":"video0.bitrate","value":4096}}

Response 200 for multi-set:

{"ok":true,"data":{"applied":[{"field":"video0.bitrate","value":4096},{"field":"system.verbose","value":true}]}}

Restart-required fields (mutability: "restart_required") trigger an automatic pipeline reinit (sensor→VIF→VPE→VENC teardown and rebuild):

# Change resolution (single call, triggers one pipeline reinit)
curl "http://<device-ip>/api/v1/set?video0.size=1280x720"

# Use sensor native resolution (default — no downscaling)
curl "http://<device-ip>/api/v1/set?video0.size=auto"

# Preset shortcuts also work
curl "http://<device-ip>/api/v1/set?video0.size=720p"
curl "http://<device-ip>/api/v1/set?video0.size=1080p"

# Enable scene-change IDR control
curl "http://<device-ip>/api/v1/set?video0.scene_threshold=150"

# Enable 2x digital zoom (encoded resolution becomes half width/height)
curl "http://<device-ip>/api/v1/set?video0.zoomPct=0.5"

Response 200 (includes "reinit_pending": true):

{"ok":true,"data":{"field":"video0.size","value":"1280x720","reinit_pending":true}}

Restart/reinit writes stay single-field by design. Even though the main loop debounces reinit requests, clients should send restart-required changes one at a time and let each accepted write schedule the pipeline rebuild.

Adaptive control usage notes:

  • Keep video0.scene_threshold=0 for fixed-GOP workflows and drive keyframe interval through video0.gop_size.
  • On the current Star6E IMX335 bench, a practical starting point is: video0.sceneThreshold=150, video0.sceneHoldoff=2.
  • Tune threshold first, holdoff second. In practice, threshold changes are a safer first response than raising holdoff.

Example Star6E tuning sequence:

curl "http://<device-ip>/api/v1/set?video0.sceneThreshold=150"
curl "http://<device-ip>/api/v1/set?video0.sceneHoldoff=2"

Validation errors — some values are rejected before being applied:

# Attempt to switch Star6E RTP mode to h264
curl "http://<device-ip>/api/v1/set?video0.codec=h264"

Error 409:

{"ok":false,"error":{"code":"validation_failed","message":"star6e RTP mode currently supports h265 only"}}

Error 501 — apply callback not available:

{"ok":false,"error":{"code":"not_implemented","message":"apply callback not available"}}

Error 400 — multi-set included a restart-required field:

{"ok":false,"error":{"code":"invalid_request","message":"multi-set only supports live fields; restart-required fields must be set one at a time"}}

The same camelCase aliases listed above are accepted here for Majestic-oriented clients.

video0.qp_delta

  • Type: signed integer
  • Range: -12..12
  • Mutability: live
  • Alias: video0.qpDelta
  • Semantics: adjusts I-frame QP relative to P-frame; negative values lower I-frame QP (higher quality keyframes), positive values raise it.

video0.zoom_pct, video0.zoom_x, video0.zoom_y

  • Types: double
  • Ranges:
    • video0.zoom_pct: 0.0 to disable zoom, or 0.25..1.0 crop fraction
    • video0.zoom_x: 0.0..1.0
    • video0.zoom_y: 0.0..1.0
  • Mutability:
    • video0.zoom_pct: restart_required because it changes encoded resolution
    • video0.zoom_x, video0.zoom_y: live
  • Aliases: video0.zoomPct, video0.zoomX, video0.zoomY
  • Semantics: digital zoom uses a 1:1 crop. The crop window and encoded output resolution shrink together; there is no SCL upscale and no additional output bandwidth pressure. zoom_x and zoom_y move the crop center inside the active aspect-ratio-corrected source surface.

GET /api/v1/fps/config

Return the configured target FPS from the active runtime config.

curl http://<device-ip>/api/v1/fps/config

Response 200:

{"ok":true,"data":{"fps":60}}

GET /api/v1/fps/live

Return the live/applied FPS reported by the active backend. If a backend does not expose a distinct live value, this falls back to the configured FPS.

curl http://<device-ip>/api/v1/fps/live

Response 200:

{"ok":true,"data":{"fps":60}}

Output Enable/Disable

The outgoing.enabled field controls whether encoded frames are sent over UDP.

# Enable output (starts sending, restores FPS, issues IDR)
curl "http://<device-ip>/api/v1/set?outgoing.enabled=true"

# Disable output (stops sending, reduces FPS to 5fps idle)
curl "http://<device-ip>/api/v1/set?outgoing.enabled=false"

Behavior when disabled:

  • FPS is reduced to 5fps (idle rate) to minimize sensor/ISP power draw.
  • Encoder keeps running at the reduced rate; frames are encoded and discarded.
  • The previous FPS is stored and restored when output is re-enabled.
  • An IDR keyframe is issued on re-enable for immediate stream sync.

Default: false — output must be explicitly enabled. Configure outgoing.server before enabling.

Live Destination Redirect

The outgoing.server field can be changed at runtime to redirect the stream.

# Redirect stream to a different GCS
curl "http://<device-ip>/api/v1/set?outgoing.server=udp://<receiver-ip>:5600"
  • Accepted URI schemes:
    • udp://HOST:PORT — standard UDP datagram output
    • unix://NAME — Linux abstract Unix datagram socket @NAME
    • shm://NAME — shared-memory RTP ring buffer
  • No pipeline restart required.
  • An IDR keyframe is issued after the change for stream continuity.
  • If connectedUdp is enabled, the UDP socket is re-connected to the new destination.
  • Live redirects support udp:// and unix://. Live switch to shm:// is not supported.
  • connectedUdp applies only to udp://.
  • shm:// remains RTP-only. It cannot share audio; use a nonzero audioPort for separate UDP audio.
  • On Star6E, audioPort=0 piggybacks on the active video destination for both udp:// and unix://.
  • On Star6E, a nonzero audioPort keeps audio on a dedicated UDP port. With unix:// or shm:// video output, that dedicated audio port is sent to 127.0.0.1:<audioPort>.

Stream Mode and Send Feedback

# Live: change max payload size on the fly (576..4000)
curl "http://<device-ip>/api/v1/set?outgoing.maxPayloadSize=4000"

# Restart-only
curl "http://<device-ip>/api/v1/set?outgoing.stream_mode=compact"
curl "http://<device-ip>/api/v1/set?outgoing.connected_udp=true"
  • outgoing.stream_mode: "rtp" (default) or "compact". Determines packetization format.
  • outgoing.max_payload_size: Maximum RTP/compact packet payload in bytes. Default 1400. MUT_LIVE — applies on the next encoded frame; in-flight packetization for the current frame keeps the old size. Range [576, 4000]. Values above ~1472 require end-to-end MTU support (e.g. Realtek's 3993-byte jumbo-frame links); on a standard 1500-MTU path the kernel will IP-fragment, defeating the point. Composes with other live fields in a single multi-set request — for example ?video0.bitrate=8000&outgoing.maxPayloadSize=4000 applies both atomically. Live updates are accepted across all transports (udp://, unix://, shm://): the SHM ring slot is sized at startup to fit the validated ceiling so any value in range applies live without restart, just like UDP/Unix.
  • outgoing.connected_udp: When true, calls connect() on the UDP socket so the kernel returns ICMP port-unreachable errors via sendmsg(). Useful for detecting that a receiver is down. Default false (fire-and-forget).

Live FPS Control — Behavior Details

Setting video0.fps via the API applies hardware-level frame decimation within the active sensor mode. The sensor continues running at its native maxFps; the MI_SYS bind layer between VPE and VENC drops frames to match the requested rate.

# On a 90fps sensor mode: set output to 30fps (sensor stays at 90, VENC receives 30)
curl "http://<device-ip>/api/v1/set?video0.fps=30"

# Set output to 60fps
curl "http://<device-ip>/api/v1/set?video0.fps=60"

# Restore full sensor rate
curl "http://<device-ip>/api/v1/set?video0.fps=90"

Clamping: If the requested FPS exceeds the current sensor mode's maxFps, the value is silently clamped to the mode maximum. For example, requesting 120fps on a 90fps mode sets the output to 90fps. To access a higher sensor mode, edit /etc/venc.json and restart the process.

What happens under the hood:

  1. VPE→VENC bind is torn down and re-established with src_fps:dst_fps ratio
  2. VENC rate control fpsNum is updated for correct bitrate allocation
  3. No pipeline restart — latency is sub-second

Mode switching limitation: Changing sensor modes (e.g. 90fps→120fps) requires a full process restart. The SigmaStar kernel driver does not reliably reinitialize the MIPI PHY when switching modes in-process. Use /api/v1/restart (reloads /etc/venc.json) or restart the venc process to change sensor modes.

GET /api/v1/restart

Reload /etc/venc.json from disk and rebuild the pipeline. Equivalent to sending SIGHUP. This endpoint does NOT write the in-memory config back to disk, so a manual file swap (editor, scp, json_cli) followed by /api/v1/restart reloads exactly what was placed on disk.

In v0.7.8 persistence moved into the /api/v1/set layer — every set (LIVE or RESTART) now saves to disk before returning, so the WebUI "Save & Restart" flow (applyChanges → /api/v1/restart) ends with the on-disk copy already matching memory before the reload runs.

curl http://<device-ip>/api/v1/restart

Response 200:

{"ok":true,"data":{"reinit":true}}

GET /api/v1/defaults

Overwrite the in-memory config with compiled-in defaults, persist to /etc/venc.json, then trigger a full pipeline reinit. Drives the "Restore Defaults" button in the WebUI.

Added in v0.7.8. The saved field in the response reflects whether persistence actually succeeded — false means the runtime is at defaults but the on-disk copy is stale (e.g. disk full, readonly FS, permission error); check the venc log for the [venc_config] ERROR: line.

curl http://<device-ip>/api/v1/defaults

Response 200:

{"ok":true,"data":{"defaults":true,"reinit":true,"saved":true}}

GET /api/v1/ae

Return live AE diagnostics from the active backend.

curl http://<device-ip>/api/v1/ae

Response 200:

{
  "ok": true,
  "data": {
    "sensor_plane": { "ret": 0, "pad": 0, "shutter_us": 1112, "sensor_gain_x1024": 10240, "comp_gain_x1024": 1024 },
    "exposure_limit": { "ret": 0, "min_shutter_us": 150, "max_shutter_us": 10000, "min_sensor_gain": 1024, "max_sensor_gain": 30000, "min_isp_gain": 1024, "max_isp_gain": 1024 },
    "exposure_info": { "ret": 0, "stable": true, "reach_boundary": false, "long_us": 9999, "long_sensor_gain_x1024": 1673, "long_isp_gain_x1024": 1024, "luma_y": 236, "avg_y": 247 },
    "state": { "ret": 0, "raw": 0, "name": "normal" },
    "expo_mode": { "ret": 0, "raw": 0, "name": "auto" },
    "metrics": { "exposure_us": 9999, "sensor_gain_x1024": 1673, "isp_gain_x1024": 1024, "fps": 90 },
    "runtime": { "sensor_fps": 90, "active_precrop": { "x": 0, "y": 240, "w": 2560, "h": 1440 } }
  }
}

runtime.active_precrop is included on both backends whenever the pipeline has been started; it is omitted before the first start and after a stop.

Error 501:

{"ok":false,"error":{"code":"not_implemented","message":"AE query not available"}}

GET /api/v1/awb

Return live AWB diagnostics from the active backend.

curl http://<device-ip>/api/v1/awb

Error 501:

{"ok":false,"error":{"code":"not_implemented","message":"AWB query not available"}}

GET /api/v1/iq

Query all ISP IQ parameter values. Always available on Star6E backend.

curl http://<device-ip>/api/v1/iq

Response 200:

{
  "ok": true,
  "data": {
    "lightness": {"ret": 0, "enabled": true, "op_type": "auto", "value": 50},
    "contrast": {"ret": 0, "enabled": true, "op_type": "manual", "value": 70},
    "color_to_gray": {"ret": 0, "value": false},
    "demosaic": {"ret": 0, "enabled": true, "value": 45}
  }
}

Each parameter reports:

  • ret: MI_ISP return code (0 = success)
  • enabled: bEnable flag
  • op_type: "auto" or "manual" (omitted for bool-only and manual-only params)
  • value: current primary value (backward-compat scalar)
  • fields: (multi-field params only) object with all named sub-fields and arrays
  • available: false if the dlsym symbol was not found

Multi-field example (colortrans):

"colortrans": {
  "ret": 0, "enabled": true, "value": 200,
  "fields": {
    "y_ofst": 200, "u_ofst": 0, "v_ofst": 0,
    "matrix": [23, 45, 9, 1005, 987, 56, 56, 977, 1015]
  }
}

Error 501 if backend doesn't support IQ (Maruko):

{"ok":false,"error":{"code":"not_implemented","message":"IQ query not available"}}

GET /api/v1/iq/set?<param>=<value>

Set a single IQ parameter. The parameter is switched to manual mode (for auto/manual params) and the value is written to the primary manual field.

Supports dot-notation for multi-field params, comma-separated arrays, and enable/disable toggling via the .enabled virtual field:

# Simple scalar
curl "http://<device-ip>/api/v1/iq/set?contrast=70"

# Dot-notation for sub-field
curl "http://<device-ip>/api/v1/iq/set?colortrans.y_ofst=200"

# Array value (comma-separated)
curl "http://<device-ip>/api/v1/iq/set?colortrans.matrix=23,45,9,1005,987,56,56,977,1015"

# Enable/disable toggle (non-bool params only)
curl "http://<device-ip>/api/v1/iq/set?colortrans.enabled=0"
curl "http://<device-ip>/api/v1/iq/set?crosstalk.enabled=1"

# Bool toggle
curl "http://<device-ip>/api/v1/iq/set?color_to_gray=1"

Response 200:

{"ok":true,"data":{"param":"colortrans.y_ofst","value":200}}
{"ok":true,"data":{"param":"colortrans.matrix","value":[23,45,9,1005,987,56,56,977,1015]}}

POST /api/v1/iq/import

Import IQ parameters from a JSON body (output of GET /api/v1/iq). Partial imports are supported — only parameters present in the JSON are applied. The enabled field is respected during import — parameters with "enabled":false will be disabled on the ISP.

# Full import from exported file
curl -X POST -H "Content-Type: application/json" \
  -d @my_tuning.json http://<device-ip>/api/v1/iq/import

# Partial import — only specific params
echo '{"lightness":{"value":75},"demosaic":{"fields":{"dir_thrd":30}}}' | \
  curl -X POST -H "Content-Type: application/json" -d @- http://<device-ip>/api/v1/iq/import

Response 200:

{"ok":true,"data":{"imported":true}}

GET / (Web Dashboard)

Serves a self-contained HTML dashboard (gzip-compressed, ~14KB). The dashboard provides Settings, API Reference, and Image Quality tabs. All modern browsers decompress the gzip response automatically.

Available parameters (62 total, Star6E):

Parameter Type Range Description
lightness u32 0-100 Lightness level
contrast u32 0-100 Contrast level
brightness u32 0-100 Brightness level
saturation u8 0-127 Color saturation (32=1X)
sharpness u8 0-255 Overshoot gain
hsv u8 0-64 Hue LUT first entry
nr3d u8 0-255 3D NR motion threshold
nr3d_ex u32 0-1 3D NR extended AR enable
nr_despike u8 0-15 De-spike blend ratio
nr_luma u8 0-255 Luma NR strength
nr_luma_adv u32 0-1 Advanced luma NR debug enable
nr_chroma u8 0-127 Chroma NR match ratio
nr_chroma_adv u8 0-255 Advanced chroma NR strength
false_color u8 0-255 False color frequency threshold
crosstalk u8 0-31 Cross-talk correction strength
demosaic u8 0-63 Demosaic direction threshold
obc u16 0-255 Optical black correction R value
dynamic_dp u8 0-1 Hot pixel detection enable
dp_cluster u32 0-1 Cluster dead pixel edge mode
r2y u16 0-1023 R2Y matrix first coefficient
colortrans u16 0-2047 Color transform Y offset
rgb_matrix u16 0-8191 CCM first coefficient
wdr u8 0-4 WDR box number
wdr_curve_adv u16 0-16384 WDR curve slope
pfc u8 0-255 Phase focus correction strength
pfc_ex u32 0-1 Extended PFC debug enable
hdr u8 0-1 HDR NR enable
hdr_ex u16 0-65535 HDR sensor exposure ratio
shp_ex u32 0-1 Extended sharpness debug enable
rgbir u8 0-7 RGBIR position type
iq_mode u32 0-1 IQ mode (0=day, 1=night)
lsc u16 0-65535 Lens shading center X
lsc_ctrl u8 0-255 LSC R ratio by CCT
alsc u8 0-255 Adaptive LSC grid X
alsc_ctrl u8 0-255 ALSC R ratio by CCT
obc_p1 u16 0-255 OBC phase 1 R value
stitch_lpf u16 0-256 Stitch LPF first coefficient
rgb_gamma bool 0/1 RGB gamma enable
yuv_gamma bool 0/1 YUV gamma enable
wdr_curve_full bool 0/1 WDR full curve enable
dummy bool 0/1 Dummy tuning enable
dummy_ex bool 0/1 Extended dummy enable
defog bool 0/1 Defogging enable
color_to_gray bool 0/1 Grayscale mode
nr3d_p1 bool 0/1 3D NR phase 1 enable
fpn bool 0/1 Fixed pattern noise enable

Hardware test results (SSC30KQ, imx335):

  • 45/46 symbols resolved (stitch_lpf not present)
  • 40/45 params roundtrip correctly (set → query reads same value)
  • 3 offset mismatches: nr_despike, pfc, hdr (set succeeds but readback differs — struct padding)
  • 2 ISP-rejected: nr3d_p1, fpn (set succeeds but ISP ignores on this sensor)

GET /api/v1/audio/status

Return live observability for the audio capture/encode pipeline. Useful for diagnosing silent audio failures (missing libmi_ai.so on Maruko, missing libopus.so, capture thread not running, codec mismatch).

curl http://<device-ip>/api/v1/audio/status

Response 200 (Star6E with audio enabled):

{
  "ok": true,
  "data": {
    "enabled": true,
    "backend": "star6e",
    "lib_loaded": true,
    "device_enabled": true,
    "channel_enabled": true,
    "running": true,
    "codec": "opus",
    "sample_rate": 48000,
    "channels": 1,
    "opus_loaded": true
  }
}

Response 200 (Maruko with audio enabled):

{
  "ok": true,
  "data": {
    "enabled": true,
    "backend": "maruko",
    "lib_loaded": true,
    "device_opened": true,
    "group_enabled": true,
    "running": true,
    "codec": "opus",
    "sample_rate": 48000,
    "channels": 1,
    "opus_loaded": true
  }
}

Response 200 when audio.enabled=false:

{"ok":true,"data":{"enabled":false,"backend":"maruko"}}

Field reference:

Field Meaning
enabled audio.enabled=true and *_audio_init reached the run state
lib_loaded The MI audio shared library (libmi_audio.so Star6E / libmi_ai.so Maruko) was found and dlopened
device_enabled / device_opened The capture device handle is open
channel_enabled / group_enabled The capture channel / group is enabled
running Capture and encode threads are alive
codec "g711a", "g711u", "opus", "pcm", or "unknown"
sample_rate Configured audio sample rate (Hz)
channels 1 (mono) or 2 (stereo)
opus_loaded When codec="opus", the Opus encoder was successfully initialized. false here while codec="opus" means audio falls back to raw PCM with a startup warning.

Error 501 — backend has no audio observability hook (query_audio_status not registered):

{"ok":false,"error":{"code":"not_implemented","message":"audio status not available on this backend"}}

GET /metrics/isp

Return a compact Prometheus-style ISP metrics snapshot.

curl http://<device-ip>/metrics/isp

Response 200:

# HELP isp_again Analog Gain
# TYPE isp_again gauge
isp_again 1673
# HELP isp_dgain Digital Gain
# TYPE isp_dgain gauge
isp_dgain 1024
# HELP isp_fps Sensor fps
# TYPE isp_fps gauge
isp_fps 90

GET /api/v1/record/start

Start SD card recording. Optional ?dir=/path query parameter overrides the default recording directory (from config record.dir, default /media).

# Start recording with default dir
wget -q -O- "http://<device-ip>/api/v1/record/start"

# Start with custom directory
wget -q -O- "http://<device-ip>/api/v1/record/start?dir=/media/clips"

Response 200:

{"ok":true,"data":{"action":"start","dir":"/media"}}

Recording format is determined by record.format config: "ts" (default, MPEG-TS with audio) or "hevc" (raw HEVC NAL stream). File rotation is controlled by record.maxSeconds and record.maxMB config fields.

Backend gating: only the Star6E backend currently runs the runtime poll that honors HTTP-driven start/stop. On Maruko, recording is config-driven only (set record.enabled=true + record.mode="mirror"|"dual" in /etc/venc.json) and /api/v1/record/start returns:

{"ok":false,"error":{"code":"not_implemented","message":"HTTP record control not available on this backend"}}

with HTTP 501. This avoids the prior behaviour where the request returned {"ok":true} but no recording started. Tracked as Phase 6.5 in MARUKO_PARITY_PLAN.md.

GET /api/v1/record/stop

Stop SD card recording.

wget -q -O- "http://<device-ip>/api/v1/record/stop"

Response 200:

{"ok":true,"data":{"action":"stop"}}

Same backend gating applies — Maruko returns 501 not_implemented.

GET /api/v1/record/status

Query recording status.

wget -q -O- "http://<device-ip>/api/v1/record/status"

Response 200:

{
  "ok": true,
  "data": {
    "active": true,
    "format": "ts",
    "path": "/media/rec_01h23m45s_abcd.ts",
    "frames": 1500,
    "bytes": 12345678,
    "segments": 1,
    "stop_reason": "none"
  }
}

stop_reason values: "none" (currently recording), "manual", "disk_full", "write_error".

GET /api/v1/recordings

List .ts and .hevc files in the configured record.dir along with disk-usage totals.

wget -q -O- "http://<device-ip>/api/v1/recordings"

Response 200:

{
  "ok": true,
  "data": {
    "dir": "/mnt/mmcblk0p1",
    "free_bytes": 1234567890,
    "total_bytes": 15000000000,
    "files": [
      { "name": "rec_01h00m00s_abcd.ts", "size": 12345678, "mtime": 1713600000 }
    ],
    "truncated": false
  }
}

Error 503 not_available — directory not mounted. Error 500 internal_error — cannot read directory or out of memory.

The listing is capped at 512 entries; truncated is true when the cap was hit and older recordings were not included. free_bytes / total_bytes are -1 when statvfs is unavailable.

GET /api/v1/recordings/download?file=<name>

Stream a single recording as an attachment download. file must be a plain name from the listing — leading ., path separators and control bytes are rejected.

wget "http://<device-ip>/api/v1/recordings/download?file=rec_01h00m00s_abcd.ts"

Content-Type is video/mp2t for .ts files and application/octet-stream for .hevc.

Error 400 invalid_request — missing or unsafe file parameter. Error 404 not_found — file does not exist in record.dir.

GET /api/v1/recordings/delete?file=<name>

Remove a recording from record.dir.

wget -q -O- "http://<device-ip>/api/v1/recordings/delete?file=rec_01h00m00s_abcd.ts"

Response 200:

{"ok":true}

Error 400 invalid_request — missing or unsafe file parameter. Error 404 not_found — file already gone. Error 409 record_active — file is currently being written; stop recording first. Error 500 delete_failed — filesystem error.

GET /request/idr

Request an IDR (keyframe) from the encoder.

curl http://<device-ip>/request/idr

Response 200:

{"ok":true,"data":{"idr":true}}

If outgoing.sidecar_port is enabled at the same time, Star6E also appends the scene-detector telemetry trailer to sidecar FRAME packets. That is the intended external interface for per-frame size/type/complexity observations.

GET /api/v1/dual/status

Query the secondary VENC channel status. Always returns 200; the active field tells you whether dual or dual-stream mode is currently running.

wget -q -O- "http://<device-ip>/api/v1/dual/status"

Response 200 — dual VENC active:

{"ok":true,"data":{"active":true,"channel":1,"bitrate":20000,"fps":120,"gop":240}}

Response 200 — dual VENC not active (off, mirror, or any non-dual mode):

{"ok":true,"data":{"active":false}}

GET /api/v1/dual/set?<param>=<value>

Live-change secondary VENC channel parameters. Supported parameters:

Parameter Type Description
bitrate uint Bitrate in kbps (applied immediately via MI_VENC, IDR issued)
gop double GOP interval in seconds (converted to frames using ch1 fps)
# Change ch1 bitrate to 10 Mbps
wget -q -O- "http://<device-ip>/api/v1/dual/set?bitrate=10000"

# Change ch1 GOP to 1 second (120 frames at 120fps)
wget -q -O- "http://<device-ip>/api/v1/dual/set?gop=1.0"

Response 200:

{"ok":true,"data":{"field":"bitrate","value":10000}}
{"ok":true,"data":{"field":"gop","value":1.00,"frames":120}}

Error 400 — missing or invalid parameter:

{"ok":false,"error":{"code":"missing_param","message":"Usage: /api/v1/dual/set?bitrate=N or ?gop=N"}}

Error 404 — dual VENC not active.

Error 501 — backend does not support live dual/set (Maruko). The Star6E binding owns the low-level MI_VENC_*ChnAttr write path; a Maruko port has not landed yet.

GET /api/v1/dual/idr

Request an IDR keyframe on the secondary VENC channel.

wget -q -O- "http://<device-ip>/api/v1/dual/idr"

Response 200:

{"ok":true,"data":{"idr":true}}

Error 404 — dual VENC not active.

GET /api/v1/idr/stats

Return per-channel IDR-rate-limit counters. The encoder enforces a minimum spacing between honored IDRs to keep bitrate predictable when many sources (scene detector, HTTP /request/idr and /api/v1/dual/idr, recorder segment rotation) ask for keyframes simultaneously. This endpoint reports how many requests were honored vs. coalesced (dropped) per channel.

curl http://<device-ip>/api/v1/idr/stats

Response 200:

{
  "ok": true,
  "data": {
    "min_spacing_us": 250000,
    "channels": [
      {"idx": 0, "honored": 47, "dropped": 3},
      {"idx": 1, "honored": 12, "dropped": 0}
    ]
  }
}

min_spacing_us is the compile-time minimum spacing in microseconds. Channels with both counters at zero are omitted. Available on both backends; always returns a valid response (even when no IDR has been requested yet — channels is then an empty array).

GET /api/v1/transport/status

Return live observability for the active video transport (UDP / Unix / SHM). Used by the WebUI status bar and by external link controllers that need to detect output backpressure.

curl http://<device-ip>/api/v1/transport/status

Response 200 (SHM ring transport, common for outgoing.server=shm://...):

{
  "ok": true,
  "data": {
    "active": true,
    "transport": "shm",
    "fillPct": 12,
    "inPressure": false,
    "transportDrops": 0,
    "pressureDrops": 0,
    "packetsSent": 184523,
    "oversizeDrops": 0,
    "slotCount": 1024,
    "usedSlots": 122
  }
}

Response 200 (UDP/Unix kernel-buffer fill_pct):

{
  "ok": true,
  "data": {
    "active": true,
    "transport": "udp",
    "fillPct": 4,
    "inPressure": false,
    "pressureDrops": 0
  }
}

Response 200 (output disabled or no socket open):

{"ok":true,"data":{"active":false,"transport":"none"}}

Field reference:

Field Meaning
transport "shm", "udp", "unix", or "none"
fillPct Current fill ratio 0..100. For SHM, ring fill; for UDP/Unix, kernel send-buffer fill
inPressure True when fillPct >= 70 (high-water threshold)
transportDrops (SHM only) Lifetime ring-full drops
pressureDrops Frames dropped by the in-process backpressure path while a sidecar probe was subscribed
packetsSent (SHM only) Lifetime writes accepted by the ring
oversizeDrops (SHM only) Frames rejected for exceeding slot capacity
slotCount / usedSlots (SHM only) Ring sizing; usedSlots is a snapshot

Error 501 — backend has no transport observability hook.

GET /api/v1/modes

Return the table of sensor pads and resolution modes the underlying SDK reports for the currently-loaded sensor driver. Used to populate the WebUI sensor-mode dropdown and to validate sensor.mode writes.

curl http://<device-ip>/api/v1/modes

Response 200:

{
  "ok": true,
  "data": {
    "selected_pad": 0,
    "selected_mode": 1,
    "pads": [
      {
        "pad": 0,
        "modes": [
          {"index": 0, "width": 1920, "height": 1080, "min_fps": 1, "max_fps": 60,  "desc": "1080p60",  "selected": false},
          {"index": 1, "width": 1920, "height": 1080, "min_fps": 1, "max_fps": 90,  "desc": "1080p90",  "selected": true},
          {"index": 2, "width": 1472, "height": 816,  "min_fps": 1, "max_fps": 120, "desc": "1472x816@120", "selected": false}
        ]
      }
    ]
  }
}

selected_pad / selected_mode reflect the currently-active pipeline selection. The full pads[].modes[] list always shows every mode the driver enumerates so callers can show an "available modes" UI.

Error 500 modes_failedMI_SNR_QueryResCount failed (e.g. sensor driver not loaded yet during a brief startup window).

SIGHUP Pipeline Reinit

In addition to the /api/v1/restart endpoint, the pipeline can be reinited by sending SIGHUP to the venc process:

# From the device shell
killall -HUP venc

# Remotely via SSH
ssh root@<device-ip> "killall -HUP venc"

Behavior:

  • Tears down the full pipeline (VENC→VPE→VIF→sensor, unbinds, closes socket)
  • Reloads /etc/venc.json from disk
  • Rebuilds the pipeline with the new config
  • The HTTP server survives reinit cycles (no port re-bind)
  • Stress-tested: 10+ consecutive SIGHUPs without failure

Important Safety Notes

  1. Accepted config writes are persistent. /api/v1/set persists accepted live and restart-required field changes to the registered config path before returning. /api/v1/defaults persists compiled defaults. /api/v1/restart reloads the on-disk config and does not synthesize new changes by itself.

  2. Codec restriction is backend-specific. Star6E RTP mode requires video0.codec=h265, so attempting to set video0.codec=h264 there returns a 409 error. Maruko accepts both h264 and h265.

  3. BusyBox compatibility. All endpoints use GET method so they work with BusyBox wget (which only supports GET):

    # On-device with BusyBox wget
    wget -q -O- "http://127.0.0.1/api/v1/get?video0.fps"
    wget -q -O- "http://127.0.0.1/api/v1/set?video0.bitrate=4096"

Backend Compatibility Notes

  • Star6E is the reference behavior for API-touching features.
  • Maruko may return not_implemented for specific apply paths until parity work is complete.
  • GET endpoints must remain consistent across backends.

Backend Support Matrix

Endpoints that behave the same on both backends are omitted. Only feature divergence is listed. As of contract_version: 0.10.1:

Feature / Endpoint Star6E Maruko Notes
/api/v1/record/{start,stop} yes 501 Maruko has no runtime poll loop yet (Phase 6.5). Config-driven recording (record.enabled=true + record.mode="mirror"|"dual") works.
/api/v1/record/status live counters live counters Both backends register a status callback against the live Star6eTsRecorderState; Maruko reflects daemon-config-driven recording (mirror/dual).
/api/v1/recordings* yes yes File listing/download/delete works against record.dir regardless of which backend wrote the file.
/api/v1/audio/status yes yes Both backends register query_audio_status.
/api/v1/dual/status, /dual/idr yes yes /dual/status always 200 (active:false when off, active:true,channel,bitrate,fps,gop when on). /dual/idr returns 200 when active, 404 when not. Maruko HTTP registration landed in 0.10.4 — earlier Maruko builds returned 404 from these even when record.mode=dual was running.
/api/v1/dual/set yes 501 Star6E-only: the underlying MI_VENC_*ChnAttr write path binds to i6_venc_chn, but Maruko's venc library expects i6c_venc_chn (different layout). Maruko returns 501 until the call path is ported.
/api/v1/iq and /api/v1/iq/set full (≈45 params) full (parity in maruko_iq.c) Both backends use the same IQ table schema.
/api/v1/awb live live Both backends register query_awb_info.
/api/v1/ae live + runtime.active_precrop live + runtime.active_precrop Both backends now include runtime.active_precrop in the AE response (Maruko parity landed in 0.8.4).
/api/v1/transport/status yes yes SHM-ring fields are shown when outgoing.server=shm://; otherwise the UDP/Unix subset.
/api/v1/idr/stats yes yes Identical schema; values reflect each backend's IDR rate-limit.
video0.codec=h264 rejected with 409 accepted Star6E RTP mode is HEVC-only on this build.
video0.scene_threshold / scene_holdoff yes yes Restart-required fields; both backends run the shared scene detector.
video0.zoom_pct / zoom_x / zoom_y yes yes zoom_pct requires reinit; zoom_x/y are live pan controls.
isp.aeMode ("native" / "throttle") accepted but no-op applied Maruko-only opt-in; switching modes mid-run requires a process restart. Default "native" on both backends.

Change Log (Contract)

  • 0.10.1:
    • GET /api/v1/dual/status always returns 200 now. When dual VENC is not active the body is {"ok":true,"data":{"active":false}} instead of the previous 404 + not_active error envelope. /dual/set and /dual/idr keep the 404 + not_active semantics — those are write endpoints that need a live ch1 to operate on.
    • Maruko: /api/v1/dual/{status,idr} now actually reflect the live dual VENC state. Before this version Maruko started chn 1 when record.mode = "dual" or "dual-stream" but never registered the handle with the HTTP API, so all three endpoints returned 404 even when dual was running. Star6E behaviour unchanged.
    • /api/v1/dual/set returns 501 on Maruko (was: silent 404). Star6E behaviour unchanged. See "Backend Support Matrix".
  • 0.10.0:
    • Added digital zoom fields: video0.zoom_pct (zoomPct alias, restart-required) plus live pan fields video0.zoom_x / video0.zoom_y (zoomX / zoomY aliases).
    • Added validation for zoom API writes: zoom_pct must be 0.0 or [0.25, 1.0]; zoom_x/y must be finite values in [0.0, 1.0].
    • Updated WebUI-facing field metadata examples for intra refresh and zoom.
    • Corrected the persistence note: accepted /api/v1/set writes have been persisted since v0.7.8.
  • 0.8.4:
    • GET /api/v1/record/status now reflects daemon-config-driven recording on Maruko (mirror/dual): previously the response was zero-fill (active:false, all counters 0) even when a TS file was being written. The Maruko runtime now registers a status callback against the same Star6eTsRecorderState the recorder uses. No schema change.
    • GET /api/v1/ae on Maruko now includes runtime.active_precrop, matching Star6E. The precrop was already being reported via /api/v1/config; only the AE response was missing it.
    • Internal (no API surface change): the /api/v1/record/start|stop 501 gate now keys off an explicit venc_api_set_record_http_control_supported(true) opt-in instead of the status-callback presence. This decoupling is what allowed Maruko to add status visibility without accidentally re-enabling the HTTP-driven control endpoints (which it still doesn't consume).
  • 0.8.3:
    • Added GET /api/v1/audio/status — live observability for the audio capture/encode pipeline (lib loaded, capture running, codec, rate, channels, Opus encoder available). Available on both backends; returns 501 when the backend has no audio observability hook.
    • GET /api/v1/record/start and GET /api/v1/record/stop now return 501 not_implemented on backends without a runtime record poll (currently only Maruko). Previously the requests appeared to succeed with {"ok":true} but did nothing. Star6E behaviour is unchanged.
    • Documented three pre-existing routes that had landed in code without contract entries: GET /api/v1/modes (sensor pad/mode introspection), GET /api/v1/transport/status (output transport observability), and GET /api/v1/idr/stats (per-channel IDR rate-limit counters). No behavioural change.
    • Added a Backend Support Matrix table covering Star6E vs Maruko divergence post-Phase-5 (audio), Phase-6 (recording), Phase-7 (dual VENC), and Phase-9 (isp.aeMode).
    • In-binary /api/v1/version now reports contract_version=0.8.3 (previously the constant was stuck at 0.3.0 while the doc moved forward to 0.8.2).
  • 0.8.2:
    • outgoing.max_payload_size is now MUT_LIVE (was MUT_RESTART) and can be batched with other live fields in a single /api/v1/set call, e.g. ?video0.bitrate=8000&outgoing.maxPayloadSize=4000.
    • Validation range tightened to [576, 4000] (boot will refuse a config outside that range).
    • SHM ring slot is sized at startup to fit the validated ceiling (4000 + 12 RTP header = 4012 bytes per slot, 8-byte aligned), so shm:// accepts the full live range with no restart-to-grow caveat, matching udp:// and unix:// behavior. Costs ~1.3 MiB extra SHM per ring.
  • 0.6.3:
    • Added GET /api/v1/recordings — list files with size/mtime plus free_bytes / total_bytes for the configured record.dir.
    • Added GET /api/v1/recordings/download?file=<name> — stream a recording as an attachment download.
    • Added GET /api/v1/recordings/delete?file=<name> — delete a file; refuses the currently-active recording with 409 record_active.
    • New error code record_active (409) for actions blocked while recording.
    • Browser UI for the above endpoints lives in the Recordings tab on the dashboard at /; there is no separate HTML route.
  • 0.6.2:
    • Added isp.keepAspect (boolean, default true) to config schema. When false, VIF captures the full sensor area and VPE scales without aspect-ratio cropping (image is stretched if sensor and encode AR differ). MUT_RESTART — applied on SIGHUP / Save & Restart. Star6E only; Maruko reads but ignores the field until SCL crop port lands as a follow-up.
    • Added isp.keepAspect camelCase alias (isp.keep_aspect).
    • GET /api/v1/config response gains a runtime block with active_precrop ({x,y,w,h}) — the VIF crop currently programmed (includes any sensor overscan offsets). Omitted when the pipeline has not started or after stop. Available on both backends.
    • GET /api/v1/ae Star6E response includes runtime.active_precrop with the same rectangle.
  • 0.5.0:
    • Added GET /api/v1/iq — query all ISP IQ parameter values (46 params).
    • Added GET /api/v1/iq/set?param=value — set individual IQ parameters live.
    • Always enabled on Star6E (no config toggle needed — zero runtime overhead).
    • Params cover image quality, noise reduction, corrections, dynamic range, lens calibration, LUT enables, and ISP mode controls.
    • Star6E: 45/46 symbols resolved, Maruko returns 501.
  • 0.4.0:
    • Added GET /api/v1/dual/status — query secondary VENC channel state.
    • Added GET /api/v1/dual/set?bitrate=N — live ch1 bitrate change.
    • Added GET /api/v1/dual/set?gop=N — live ch1 GOP change (in seconds).
    • Added GET /api/v1/dual/idr — request IDR on secondary channel.
    • All dual endpoints return 404 when dual VENC is not active.
    • Config record section expanded: mode ("off"/"mirror"/"dual"/"dual-stream"), bitrate, fps, gopSize for ch1 config, server for dual-stream.
  • 0.3.0:
    • Added GET /api/v1/record/start — start SD card recording (optional ?dir=).
    • Added GET /api/v1/record/stop — stop SD card recording.
    • Added GET /api/v1/record/status — query recording status (active, format, bytes, segments, stop_reason).
    • Config record section expanded: format ("hevc" or "ts"), maxSeconds, maxMB.
    • MPEG-TS muxer: HEVC video + PCM audio in power-loss safe container.
    • File rotation at IDR boundaries by time (default 300s) or size (default 500MB).
    • RTP streaming and recording operate concurrently.
  • 0.2.3:
    • Added GET /api/v1/ae for live AE diagnostics.
    • Added GET /api/v1/awb for live AWB diagnostics.
    • Added GET /metrics/isp for compact ISP metrics export.
    • Added Majestic-compatible GET /api/v1/config.json alias.
    • Added GET /api/v1/fps/config for configured FPS queries.
    • Added GET /api/v1/fps/live for live/applied FPS queries.
    • Added support for selected Majestic-style camelCase field aliases on GET /api/v1/get and GET /api/v1/set.
  • 0.2.1:
    • outgoing.max_payload_size now applies to RTP mode (was only used by compact mode). Default 850. Set to 0 to disable adaptive sizing.
  • 0.2.0:
    • Added outgoing.enabled (MUT_LIVE): enable/disable UDP output with FPS idle.
    • Added outgoing.server changed from MUT_RESTART to MUT_LIVE: live destination redirect.
    • Added outgoing.streamMode (MUT_RESTART): explicit stream mode selection.
    • Added outgoing.connectedUdp (MUT_RESTART): connected UDP error reporting.
    • IDR keyframe issued on output enable, destination change, and bitrate change.
    • Server URIs now accept udp://, unix://, and shm://.
  • 0.1.3:
    • Documented live FPS control behavior (hardware bind decimation, clamping, mode switching limitation).
    • video0.fps set via API now uses MI_SYS_BindChnPort2 rebind instead of /proc write.
    • Removed isp.exposure config field, capability, and Prometheus metric. Auto-cap to frame period (1/fps) is now the only exposure mode.
    • Changed video0.size default from "1920x1080" to "auto" (use sensor native resolution). Added "auto" preset to size parser.
    • Removed "4MP" size preset (sensor-specific, not a standard resolution).
  • 0.1.2:
    • Updated to reflect actual implemented API (was draft, now active).
    • All endpoints use GET method (BusyBox wget compatibility).
    • Documented query parameter format: ?field_name for get, ?field_name=value for set.
    • Added /api/v1/restart endpoint (replaces planned POST /api/v1/actions/restart).
    • Added /request/idr endpoint.
    • Removed unimplemented PUT /api/v1/config and PATCH /api/v1/config (future work).
    • Added curl examples for all endpoints.
    • Added SIGHUP reinit documentation.
    • Added safety notes (in-memory only, codec restriction).
  • 0.1.1:
    • Updated examples to use video.capture_resolution restart semantics.
  • 0.1.0:
    • Initial draft contract and endpoint definitions.