# Schema 3.2.0

ECN and RFC 8888 counters, PSNR, structured quality-limitation durations

Version coverage: /docs/reference/versions/


Entirely additive on the record level: no field was removed or renamed. Several counters were
widened from `int` to `long`.

## Added — ECN and RFC 8888 congestion feedback

Explicit Congestion Notification lets the network mark packets as "congestion experienced" instead
of dropping them, and RFC 8888 defines the feedback report that carries per-packet arrival
information back to the sender. Together they are the foundation of L4S-style congestion control.

### InboundRtpStats

| Field | Type | Description |
|---|---|---|
| `packetsReceivedWithEct1` | `int` *optional* | Total RTP packets received for this SSRC marked with the ECT(1) marking |
| `packetsReceivedWithCe` | `int` *optional* | Total RTP packets received for this SSRC marked with the CE marking |
| `packetsReportedAsLost` | `int` *optional* | Total RTP packets for which an RFC 8888 report has been sent with a zero R bit |
| `packetsReportedAsLostButRecovered` | `int` *optional* | Total RTP packets reported as lost but later recovered in a subsequent RFC 8888 report |

### RemoteInboundRtpStats

The same four fields, plus:

| Field | Type | Description |
|---|---|---|
| `packetsWithBleachedEct1Marking` | `long` *optional* | Packets with ECT(1) marking that were bleached by a middlebox |

### OutboundRtpStats

| Field | Type | Description |
|---|---|---|
| `packetsSentWithEct1` | `long` *optional* | Packets sent with ECT(1) congestion marking |

### IceTransportStats

| Field | Type | Description |
|---|---|---|
| `ccfbMessagesSent` | `long` *optional* | Congestion Control Feedback messages sent on this transport |
| `ccfbMessagesReceived` | `long` *optional* | Congestion Control Feedback messages received on this transport |

{{< callout context="note" title="Sparse by nature" icon="info-circle" >}}
Only browsers implementing ECN and RFC 8888 populate these. Treat every one of them as optional in
your processing — most samples in the field will not carry them for some time.

`packetsWithBleachedEct1Marking` is the interesting one operationally: it says a middlebox on the
path stripped the ECN marking, which silently disables the congestion signalling you thought you
had.
{{< /callout >}}

## Added — video quality measurement

### PsnrSum (new record)

Peak signal-to-noise ratio per colour plane, nested in `OutboundRtpStats.psnrSum`.

| Field | Type | Description |
|---|---|---|
| `y` | `double` | PSNR value for the Y (luminance) component |
| `u` | `double` | PSNR value for the U (chrominance) component |
| `v` | `double` | PSNR value for the V (chrominance) component |

### OutboundRtpStats

| Field | Type | Description |
|---|---|---|
| `psnrSum` | `PsnrSum` *optional* | Cumulative PSNR measurements for Y, U, V components |
| `psnrMeasurements` | `long` *optional* | Total number of PSNR measurements collected |
| `encodingIndex` | `int` *optional* | Index of the encoding in the encodings array |

`encodingIndex` is small but useful for simulcast: it identifies which configured encoding an
outbound RTP stream corresponds to, without inferring it from `rid` or resolution.

Divide `psnrSum` by `psnrMeasurements` for the average PSNR per plane over the session.

## Changed — qualityLimitationDurations

Restructured from a loose map into a named record:

```typescript
type QualityLimitationDurations = {
    none: number;       // seconds with no quality limitation
    cpu: number;        // seconds limited by CPU
    bandwidth: number;  // seconds limited by bandwidth
    other: number;      // seconds limited for other reasons
};
```

{{< callout context="tip" title="Why this matters for analysis" icon="rocket" >}}
`qualityLimitationReason` tells you what is limiting the encoder *right now* — a single sample of a
state that flickers. `qualityLimitationDurations` is cumulative, so differencing it across two
samples gives you *how many seconds of this interval were CPU-limited versus bandwidth-limited*.
That is the number that actually answers "was this call CPU-bound or network-bound?".

`CallHealthAggregator` in [`observer-js`](/docs/observer-js/entities/)
surfaces exactly this as `health.qualityLimitation`.
{{< /callout >}}

## Changed — counter widening

Byte and packet counters that could realistically exceed 32 bits on long, high-bitrate sessions
were widened from `int` to `long` across the inbound, remote-inbound, outbound and transport
records.

## Changed — infrastructure

- `frozen` added as a state on `IceCandidatePairStats`.
- Node 22 across the CI workflows.
- Protobuf handling reworked, dependencies updated, `CONTRIBUTING.md` added.

## Compatibility

Additive on the record level, but remember that **protobuf field numbers derive from sorted field
order** — new fields inserted before existing ones in the sort renumber what follows. Keep encoder
and decoder on matching versions.

[← Back to version history](/docs/schema/versions/) · [3.1.0 →](../v3-1-0/)

