Control4 Subsystem Reference

# Camera Extras and Dynamic URLs

Two capabilities a camera driver can opt into from its manifest: building its own control panel at runtime, and handing over stream and snapshot URLs on demand instead of declaring them once. Both are driven entirely with proxy notifications. Extras are not specific to cameras and are worth knowing wherever a driver needs controls its proxy does not define.

- **Proxy**: camera

- **Binding**: 5001

- **Opt in via**: <capabilities>

- **Payloads**: XML strings

## The model

>  A camera driver has always been able to declare fixed URLs and a fixed feature set in its manifest. These capabilities invert that. The driver publishes its controls and its URLs at runtime, as XML, over the proxy binding it already owns.

Two independent mechanisms share one opt in style, and a driver may adopt either without the other.

**Extras** are driver defined controls rendered in the camera interface. The driver sends a document describing sections and objects. Each object names the command the proxy will send back when a viewer manipulates it, so the driver defines both the control and its callback in one place. Nothing about an extra is declared in the manifest, which means the control set can differ per device, per model, or per firmware revision.

**Dynamic URLs** replace static stream and snapshot addresses. Rather than resolving a URL once at setup, the driver answers with a current list whenever one is needed, and can invalidate the previous answer at any time. This suits devices whose addresses depend on how they are attached, on a credential that rotates, or on a recorder that fronts them.

Both mechanisms are pure notification traffic. There are no new bindings and no new proxies. Everything travels over the existing camera proxy binding, which is conventionally `5001`.

**Extras are not a camera concept.** The same notifications and the same document format are used by drivers across many proxy types, including thermostats, lights, fireplaces and keypads. Only the dynamic URL half is specific to cameras. Read the extras sections below as general proxy technique that happens to be documented here against a camera.

## Opting in

Three flags inside the manifest's `<capabilities>` block. They gate the behavior: without them, the notifications below are ignored.

**Capability flags**

| Flag | Effect |
| --- | --- |
| has_extras | The driver supplies its own control panel. Enables the extras notifications. |
| requires_dynamic_stream_urls | Stream addresses come from the driver at runtime rather than from static configuration. |
| requires_dynamic_snapshot_urls | The same for still images. |

```lua
<capabilities>
  <modes>H265,H264,SNAPSHOT</modes>

  <has_extras>true</has_extras>
  <requires_dynamic_stream_urls>true</requires_dynamic_stream_urls>
  <requires_dynamic_snapshot_urls>true</requires_dynamic_snapshot_urls>
</capabilities>
```

The stream and snapshot flags are independent. A device that serves stills from a stable path but streams from a rotating one sets only the stream flag.

## Call flow

_Diagram: Sequence diagram: at startup the driver sends EXTRAS_SETUP_CHANGED and DYNAMIC_CAPABILITIES_CHANGED to the camera proxy. When a viewer changes a control the proxy sends back the command named in that control's definition, and the driver answers with EXTRAS_STATE_CHANGED. Separately the driver publishes STREAM_URLS_READY carrying a key, and later sends DYNAMIC_URLS_CHANGED to invalidate it._

_ Almost all traffic runs driver to proxy. The single inbound arrow is **the command the driver itself named** when it defined the control, which is what makes the extras surface self describing. _

## Conventions

### Everything is a notification

Each message below is an ordinary proxy notification on the camera binding. There is no response and no return value. The driver announces state, and the interface catches up.

```lua
C4:SendToProxy(CAMERA_BINDING, "EXTRAS_SETUP_CHANGED", { XML = document }, "NOTIFY")
```

### Payloads are XML strings, not tables

The documents travel as a single string parameter, conventionally `XML`, built by the driver. This is unlike most proxy traffic, where parameters are a flat table of scalars. Escape any value that could contain a quote or an angle bracket, since device supplied names frequently do.

### Identifiers are yours, and they are load bearing

An object's `id` is chosen by the driver. It is the handle used to update that control's value later, so it must stay stable for as long as the control exists. The `command` attribute is likewise arbitrary and is simply the command name the proxy will send back. Deriving one from the other keeps the two ends aligned.

### Library conventions are not the contract

The examples here dispatch inbound commands through a table looked up by name, which is a common driver idiom rather than a requirement. What is fixed is the notification names, the parameter names (`XML`, `KEY`, `URLS`), the element and attribute names in the documents, and the fact that the inbound command is whatever the driver put in the `command` attribute. A driver may receive that command in a plain `ReceivedFromProxy` with a chain of comparisons and behave identically.

## Driver notifications

>  Sent by the driver on the camera proxy binding. All are fire and forget.

EXTRAS_SETUP_CHANGED Extras

Publishes the entire control panel. Send it once the driver knows what the device supports, and again whenever the shape of the panel changes, such as when a capability probe finishes or a viewer changes how the device is attached.

**Parameters**

| Name | Type | Notes |
| --- | --- | --- |
| XML | string req | An `extras_setup` document. Replaces the previous panel entirely. |

This is a replacement, not a merge. Any control absent from the new document is gone.

EXTRAS_STATE_CHANGED Extras

Updates the value of controls already published, without resending the panel. Use it when the device reports a change that did not originate in the interface, and to confirm a change that did.

**Parameters**

| Name | Type | Notes |
| --- | --- | --- |
| XML | string req | An `extras_state` document carrying one or more id and value pairs. |

An id that does not match a published object has no effect. There is no error.

DYNAMIC_CAPABILITIES_CHANGED Capabilities

Announces the movement related capability set, which is carried as an ordinary parameter table rather than a document. Send it alongside the extras panel whenever the driver has finished working out what the device can physically do.

STREAM_URLS_READY URLs

Delivers the current stream list. Required when the driver declares `requires_dynamic_stream_urls`.

**Parameters**

| Name | Type | Notes |
| --- | --- | --- |
| KEY | string | Correlates this answer with the request that prompted it. Echoed into the document's own `key` attribute. |
| URLS | string req | A `streams` document. |

The key appears in two places, as a parameter and as an attribute on the document root. Set both to the same value.

SNAPSHOT_URLS_READY URLs

The still image counterpart, carrying a `snapshots` document. Same parameters and same key handling.

DYNAMIC_URLS_CHANGED URLs

Invalidates whatever was published previously. Takes no parameters. Send it when an address, port, credential, or attachment method changes, so that anything holding an old URL discards it.

This is the mechanism that makes the whole dynamic URL arrangement worthwhile. A driver that never invalidates has gained nothing over static configuration.

## Proxy commands

>  Inbound commands arrive on the camera binding and are handled like any other proxy traffic.

the command you named Extras

When a viewer operates a control, the proxy sends the command string from that object's `command` attribute. The driver chose the name, so there is no fixed vocabulary here, only the promise that what the driver declared is what it will receive.

The new value arrives in the command parameters. Apply it to the device, then confirm with `EXTRAS_STATE_CHANGED` rather than assuming the interface and the device now agree. A control whose write fails should be pushed back to its old value the same way.

Momentary controls carry no meaningful value. Treat the arrival of the command as the whole event.

Connection and addressing Standard

The usual camera proxy commands still arrive and still matter here, because each one can invalidate a previously published URL. A driver using dynamic URLs should treat every one of these as a trigger to send `DYNAMIC_URLS_CHANGED`.

**Commands that can invalidate a URL**

| Command | Carries |
| --- | --- |
| SET_ADDRESS | ADDRESS |
| SET_HTTP_PORT | PORT |
| SET_HTTPS_PORT | PORT |
| SET_RTSP_PORT | PORT |
| SET_USERNAME | credential |
| SET_PASSWORD | credential |
| SET_USE_HTTPS | scheme selection |
| SET_AUTHENTICATION_REQUIRED | auth policy |
| SET_AUTHENTICATION_TYPE | auth policy |
| SET_PUBLICLY_ACCESSIBLE | reachability |

On an address change the driver also emits `ADDRESS_CHANGED` back to the proxy carrying the new `ADDRESS`.

## Payload shapes

### extras_setup

Sections group controls under a heading. Objects are the controls. Four object types exist: `list`, `slider`, `switch`, and `button`.

```lua
<extras_setup>
  <extra>
    <section label="Illumination">

      <object type="list" id="beam_mode" label="Beam mode"
              command="SET_BEAM_MODE" value="auto">
        <list maxselections="1" minselections="1">
          <item text="Automatic" value="auto"/>
          <item text="Manual"    value="manual"/>
        </list>
      </object>

      <object type="slider" id="beam_level" label="Brightness"
              command="SET_BEAM_LEVEL" value="50" min="1" max="100"/>

      <object type="switch" id="beam_enabled" label="Enabled"
              command="SET_BEAM_ENABLED" value="true"/>

      <object type="button" id="beam_test" label="Test"
              command="SET_BEAM_TEST"/>

    </section>
  </extra>
</extras_setup>
```

**Object attributes**

| Attribute | Applies to | Meaning |
| --- | --- | --- |
| type | all | list, slider, switch, or button. |
| id | all | Driver chosen handle. Must stay stable while the control exists. |
| label | all | Shown to the viewer. |
| command | all | The command the proxy sends back on interaction. |
| value | list, slider, switch | Current value. Omit for momentary controls. |
| min, max | slider | Range bounds. |
| maxselections, minselections | list | On the inner `list` element. Both are 1 for a single choice. |

### extras_state

Value updates for controls already published.

```lua
<extras_state>
  <extra>
    <object id="beam_level" value="72"/>
  </extra>
</extras_state>
```

### streams and snapshots

Both wrap a flat list. Order is meaningful: the first entry is treated as the primary, higher quality source, and later entries as progressively lighter alternates.

```lua
<streams key="REQUEST_KEY">
  <stream url="rtsp://host:554/main" codec="h265" resolution="2560x1440"/>
  <stream url="rtsp://host:554/sub"  codec="h264" resolution="640x480"/>
</streams>
```

The `key` attribute is omitted when the publication was not prompted by a request. Snapshot documents follow the same pattern with `snapshots` and `snapshot` element names.

## Behavior notes

#### The panel is replaced, never merged

Every setup document supersedes the last. Rebuild the whole thing from current state rather than trying to send a difference.

#### State updates do not resize the panel

A value update can change what a control reads, not what controls exist, and not the items inside a list. Changing the options themselves means sending a fresh setup document.

#### Nothing is acknowledged

These are notifications, so there is no delivery confirmation and no error path. A malformed document fails silently, which makes an empty or stale panel the normal symptom of a construction bug.

#### Confirm writes rather than assuming them

The interface shows the value the driver last published. If a write to the device fails and the driver stays quiet, the control keeps displaying a setting that was never applied.

#### Capability flags gate everything

Without the manifest flags the notifications are ignored. A panel that never appears is worth checking against the manifest before the code.

#### Invalidate on every addressing change

Address, ports, credentials, scheme, and reachability all feed URL construction. Any of them changing without an invalidation leaves consumers holding an address that no longer resolves.

#### The two halves have very different adoption

Extras are widely used across many proxy types and can be treated as well exercised. The dynamic URL flags are set by far fewer drivers, so that half is thinner and its edge cases are more likely to be untested.

## Integration recipe

The minimum for a driver that wants both halves. Helper names here are placeholders for whatever the driver already has.

```lua
-- 1. publish the panel once capabilities are known
local function PublishExtras()
  local doc = {}

  doc[#doc + 1] = '<extras_setup><extra>'
  doc[#doc + 1] = '<section label="Illumination">'
  doc[#doc + 1] = string.format(
    '<object type="slider" id="%s" label="%s" command="%s"'
      .. ' value="%d" min="1" max="100"/>',
    "beam_level", "Brightness", "SET_BEAM_LEVEL", DeviceState.level)
  doc[#doc + 1] = '</section></extra></extras_setup>'

  C4:SendToProxy(CAMERA_BINDING, "EXTRAS_SETUP_CHANGED", {
    XML = table.concat(doc),
  }, "NOTIFY")
end

-- 2. handle the command the control declared
local function OnBeamLevel(tParams)
  local level = tonumber(tParams.LEVEL or tParams.VALUE)

  if not WriteLevelToDevice(level) then
    level = DeviceState.level  -- push the old value back
  else
    DeviceState.level = level
  end

  C4:SendToProxy(CAMERA_BINDING, "EXTRAS_STATE_CHANGED", {
    XML = string.format(
      '<extras_state><extra><object id="%s" value="%s"/></extra></extras_state>',
      "beam_level", tostring(level)),
  }, "NOTIFY")
end

-- 3. publish stream URLs
local function PublishStreams(key)
  local doc = {}

  if key then
    doc[#doc + 1] = string.format('<streams key="%s">', key)
  else
    doc[#doc + 1] = '<streams>'
  end

  for _, s in ipairs(BuildStreamList()) do
    doc[#doc + 1] = string.format(
      '<stream url="%s" codec="%s" resolution="%s"/>', s.url, s.codec, s.resolution)
  end

  doc[#doc + 1] = '</streams>'

  C4:SendToProxy(CAMERA_BINDING, "STREAM_URLS_READY", {
    KEY  = key,
    URLS = table.concat(doc),
  }, "NOTIFY")
end

-- 4. invalidate whenever addressing changes
local function OnAddressingChanged()
  C4:SendToProxy(CAMERA_BINDING, "DYNAMIC_URLS_CHANGED", {}, "NOTIFY")
end
```

Escaping is omitted above for brevity. Device supplied labels and stream names routinely contain characters that will break a document built by concatenation, so run every interpolated value through an XML escape before it reaches the string.

Camera extras and dynamic URLs, subsystem reference Camera proxy
