OneIP and V2IP

Streaming endpoints, stream sources and statistics.

Pulse-Eight OneIP Devices

Pulse-Eight OneIP HDMI-over-IP devices expose additional streaming properties:

# stream source addresses
if device.is_v2ip and device.v2ip_sources:
    for source in device.v2ip_sources:
        print(f"Video: {source.video.ip}:{source.video.port}")
        print(f"Audio: {source.audio.ip}:{source.audio.port}")

# stream details (encoder/decoder config)
if device.v2ip_details:
    details = device.v2ip_details
    print(f"Video: {details.video}")
    print(f"TX rate: {details.tx_rate}")   # None when the sender offered no rate

    # per-stream DSCP marking (None from peers that predate it)
    if details.dscp and details.dscp.complete:
        print(f"DSCP: {details.dscp}")     # video / audio / anc, 0..63

# streaming statistics
# The subscription lapses 60s after the request and there is no free-running
# mode, so call this again inside the minute to keep the 1Hz reports coming.
await device.read_stats(enable=True)   # start collecting
# ... later ...
stats = device.v2ip_stats

# what the sink's decoder recovered from the codestream it is being given.
# Three answers, and they mean different things:
if stats.decoder is None:
    pass                                  # sender predates MatrixOS 10.12.46
elif stats.decoder.reading is None:
    pass                                  # decoder has never answered
else:
    reading = stats.decoder.reading
    # Geometry is what says a picture was recovered. `format` never does: with
    # no stream it reads RGB, which is indistinguishable from a real RGB source.
    if reading.recovered:
        print(f"{reading.width}x{reading.height} {reading.format}")
    # `reason` for display, `causes` for logic. Every true cause sets its bit
    # in flags; which one keeps `reason` is a fixed priority in the video
    # processor, not the numbering. TX_BRIDGE_UNLOCKED ranks below every
    # input-side cause, so a repeating pipeline restart shows as bit 9 in
    # `causes` while `reason` names the input cause instead.
    print(f"{reading.reason}")            # None for a cause this build cannot name
    print(f"{reading.causes}")            # raw reason values, lowest first

Trusting a decoder reading after a switch

The values are read off the video processor every 2s and reported every 1s, latched between reads, so roughly every other report repeats a reading already seen — a frame arriving says nothing about freshness. reading.updates counts readings actually stored, so it stays still while a processor is stalled rather than implying a refresh. It is monotonic, never reset, and wraps at 65535 (~36h).

After changing what a sink is pointed at, wait for updates to advance by two before trusting the geometry. It ticks when the reply lands, not when the query is sent, so a single tick can carry an answer read fractionally before the switch.

before = device.v2ip_stats.decoder.reading.updates
await sink.switch_source(...)
# ... wait until reading.updates - before >= 2 (mod 65536) ...

Colour depth is deliberately absent: the video processor answers depth from a driver constant rather than from the codestream, so it was withheld rather than shipped as a constant that looks like a measurement. Assert depth at the encoder’s input bay instead.

What this block cannot tell you

Whether a sink is off on purpose. A sink someone deliberately disabled reports NO_PACKETS, indefinitely — the same cause as a sink that should be receiving and is not. There is no enablement field here, so read that from MXR_OP_V2IP_DEVICE_CFG or the device’s HTTP status. IDLE sounds like the answer and is not: it is effectively unreachable in shipping firmware.

Whether a single sample is representative. reason during a teardown is a sequence, not a state — a measured one passed through SWITCH_PENDING before settling on NO_PACKETS six seconds later. Use updates to tell a fresh reading from a repeat, and note it cannot see a TX_BRIDGE_UNLOCKED flag carried forward across a format change: a value held over is a stored reading like any other.

Mesh and firmware

# mesh operations
await device.mesh_promote()   # promote to mesh master
await device.mesh_remove()    # remove from mesh

# firmware versions
if device.v2ip_firmware_versions:
    for fw_type, fw in device.v2ip_firmware_versions.items():
        print(f"{fw_type}: {fw.version}")

Documentation index | Project README