690 lines
40 KiB
Markdown
690 lines
40 KiB
Markdown
# WAI Extension: Energy-Aware Content Steering
|
||
|
||
> Status: Draft. Registers the record `wai.live.steering`: a steering server's choice, for
|
||
> one client, of the pathway that delivers a room's stream for the fewest joules within the
|
||
> operator's quality and latency limits, decided from the per-hop session records
|
||
> ([live-metering](live-metering.md) §3, `WMH1`) that each pathway's hops signed, and signed
|
||
> so that anyone holding those records can recompute it. The operator signs a **steering
|
||
> policy**, `WSP1`: the pathways, their hops, their limits and the window the evidence is
|
||
> read over. For each request the steering server derives each pathway's **evidence**, its
|
||
> joules per delivered byte (or per delivered second at the floor tier) with the
|
||
> acquisition class of [energy-measurement](energy-measurement.md) §2, applies one integer
|
||
> **decision rule**, and signs the **steering decision**, `WSD1`, which names the evidence
|
||
> records by digest, the client's report, the server's own inputs and the output. A figure
|
||
> is compared only with figures of its class; a pathway whose energy is not known, because a
|
||
> hop is unmetered, its classes differ or its evidence is stale, is never ranked above one
|
||
> whose energy is, so that not measuring is never rewarded. The output is the steering
|
||
> manifest that HLS and DASH players already read, written byte for byte, carrying the
|
||
> decision's URI in one key that players ignore. No new transport. The reference
|
||
> implementation is the `energy_steering` module of `wai-rs` (feature `energy_steering`);
|
||
> the corpus is [`live-conformance/steering/`](../live-conformance/steering/), and its
|
||
> second implementation is `live-conformance/energy_steering_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: `draft-pantos-hls-rfc8216bis-22` (§4.4.6.6, the
|
||
> `EXT-X-CONTENT-STEERING` tag, and §7, content steering in HLS) and
|
||
> `draft-pantos-content-steering-04` (§2 to §7: pathway IDs, the steering manifest, pathway
|
||
> cloning, query parameters and client behaviour), Internet-Drafts not past Working Group
|
||
> Last Call; ETSI TS 103 998 V1.1.1 (2024-01), content steering for DASH (clauses 5 to 8);
|
||
> CTA-5004-B (the CMCD keys `bl` and `mtp`, and the query and header modes); RFC 9651 §4.2;
|
||
> RFC 3986 §2; RFC 8259; RFC 8032; FIPS 180-4. A section number of one of those drafts in
|
||
> this text is that revision's.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the steering policy, its pathways and limits, and who signs it (§3); what a
|
||
steering server reads from a client's steering request (§4); a pathway's evidence and its
|
||
figure (§5); the decision rule (§6); the steering decision record and who signs it (§7);
|
||
the steering manifest, the HLS tag and the DASH element, written exactly, and how the
|
||
record rides beside them (§8); verification (§9).
|
||
|
||
Out of scope: a transport, and a steering server's network service, which this extension
|
||
describes only through its inputs and its output; how a hop measures its energy, which
|
||
energy-measurement governs; how an operator chooses its limits; and how a player applies a
|
||
steering manifest, which the three texts above fix and this extension does not change.
|
||
|
||
Relationship to the core spec: `wai.live.steering` is a **record** ([SPEC](../SPEC.md) §5):
|
||
a signed record a WAI tool emits and verifies. It names no payload format and is not
|
||
dispatchable.
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **room clock**, **room key**, **hop**, **per-hop session record**, **unmetered
|
||
marker**: as in live-rooms §2 and live-metering §2. A record's **digest** is live-metering
|
||
§3.5's.
|
||
- **Pathway**, **pathway ID**, **pathway clone**, **base pathway**, **steering manifest**,
|
||
**steering server**: as in `draft-pantos-content-steering-04` §2. A pathway is an HLS
|
||
pathway (rfc8216bis-22 §7.1) and a DASH service location (ETSI TS 103 998 clause 5.1).
|
||
- **Content description**: the HLS multivariant playlist or the DASH MPD.
|
||
- **Edge hop** of a pathway: a hop whose sessions are clients of the stream; **interior
|
||
hop**: a hop of the pathway that feeds other hops.
|
||
- **Tier**: a quality level, numbered from 0, the lowest. Tier *i* is the room's layer
|
||
class *i* (live-metering §3.3), and the policy states its bitrate.
|
||
- **Class**: an acquisition class of energy-measurement §2, by its tag (§2.7): 2
|
||
`ModelBased`, 3 `Estimator`, 4 `OnChipCounter`, 5 `CalibratedInstrument`.
|
||
- *u128*, *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers of that many bits. A
|
||
digest is SHA-256 (FIPS 180-4), 32 bytes. **ppm**: parts per million.
|
||
- Every quantity in this text is an integer and every operation is exact: a product is
|
||
formed in full, never rounded or saturated, and a quotient is named with its rounding.
|
||
|
||
## 3. The steering policy
|
||
|
||
### 3.1 Layout
|
||
|
||
The operator signs one policy for a room's steering, `WSP1`:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WSP1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id whose per-hop records are the evidence |
|
||
| `signer` | 32 | the key that signs the policy (§3.5) |
|
||
| `period_us` | 8 | u64: *P*, the period on which the pathways' hops cut their records (§5.1) |
|
||
| `window_us` | 8 | u64: *W*, the evidence window, a whole number of periods |
|
||
| `lag_us` | 8 | u64: *L*, how long after a period ends its records are first required |
|
||
| `basis` | 1 | 1: joules per delivered byte; 2: joules per delivered second at or above the floor tier (§5.3) |
|
||
| `floor` | 1 | *F*: the floor tier |
|
||
| `latency_ceiling_ms` | 4 | u32: the latency ceiling |
|
||
| `min_buffer_ms` | 4 | u32: below this buffer a client is not moved (§6.4) |
|
||
| `hysteresis_ppm` | 4 | u32: *h*, the margin by which a pathway must beat a client's current one (§6.4) |
|
||
| `ttl_s` | 4 | u32: the steering manifest's TTL |
|
||
| `min_ttl_s` | 4 | u32: the TTL when the decision holds a client or finds no pathway within the limits |
|
||
| `flags` | 1 | bit 0: DASH `queryBeforeStart`; every other bit 0 |
|
||
| `initial` | 1 | the index of the initial pathway of the content description |
|
||
| `tiers` | 1 + 8 × *n* | a u8 count *n*, then each tier's bitrate in bits per second (u64), tier 0 first |
|
||
| `server_uri` | 2 + … | a u16 length, then the steering server's URI, which the content description names |
|
||
| `reload_uri` | 2 + … | a u16 length, then the URI the manifest names for the next request (§8.1) |
|
||
| `decision_uri` | 2 + … | a u16 length, then the prefix of the URI at which a decision record is served (§8.4) |
|
||
| `pathways` | 1 + … | a u8 count, then each pathway entry (§3.2), in the policy's order |
|
||
| `sig` | 64 | §3.4 |
|
||
|
||
### 3.2 Pathway entries
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `id` | 1 + … | a u8 length, then the pathway ID |
|
||
| `base` | 1 | 255: a pathway of the content description; otherwise the index of the pathway it clones |
|
||
| `host` | 1 + … | a u8 length, then the clone's `HOST` (§8.1); length 0 for none |
|
||
| `params` | 1 + … | a u8 count, then each of the clone's `PARAMS`: a u8 length and the name, a u8 length and the value |
|
||
| `top_tier` | 1 | the highest tier the pathway carries |
|
||
| `latency_ms` | 4 | u32: the latency the operator states for the pathway |
|
||
| `capacity` | 4 | u32: the most sessions the server steers to it first at once; 2^32 − 1 for no limit |
|
||
| `min_share_ppm` | 4 | u32: the share of sessions it is placed first for whatever its evidence (§6.3) |
|
||
| `hops` | 1 + 33 × *k* | a u8 count *k*, then each hop: its key (32) and its place (u8: 1 edge, 2 interior) |
|
||
|
||
The pathways that are not clones are the content description's: each is the `PATHWAY-ID` of
|
||
its HLS variant streams (rfc8216bis-22 §4.4.6.2) and the `@serviceLocation` of its DASH
|
||
`BaseURL` elements (ETSI TS 103 998 clause 5.2). A clone is a pathway of its own, with its
|
||
own hops: the same content reached through another host.
|
||
|
||
### 3.3 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given, in the order of §3.1, as live-events §3.3
|
||
reads a record, with `WSP1` for `WEV1` (`truncated`, `malformed`, `unknown_version`). It
|
||
refuses as `clone_unknown` a policy with a clone whose `base` is not the index of a pathway
|
||
before it: a clone of a pathway the manifest would not yet define, which
|
||
`draft-pantos-content-steering-04` §5 has a client ignore. It refuses as `policy_invalid` a
|
||
policy that breaks any of these:
|
||
|
||
- `period_us` is above 0, and `window_us` is a multiple of it above 0;
|
||
- `basis` is 1 or 2; 1 ≤ `min_ttl_s` ≤ `ttl_s`; `hysteresis_ppm` ≤ 1 000 000; `flags` is 0
|
||
or 1;
|
||
- 1 ≤ *n* ≤ 8; the bitrates are above 0 and strictly ascending; `floor` < *n*;
|
||
- each URI is 1 to 2048 bytes of RFC 3986's characters (unreserved, reserved and `%`, never
|
||
`#`), and each `%` begins a percent-encoding of two hexadecimal digits;
|
||
- 1 to 32 pathways; each `id` is 1 to 64 bytes of `A`–`Z`, `a`–`z`, `0`–`9`, `.`, `-` and
|
||
`_` (content-steering-04 §2), and no two are equal;
|
||
- `initial` is the index of a pathway that is not a clone;
|
||
- a pathway that is not a clone has no `host` and no `params`; a clone's `host` is at most
|
||
255 bytes of `A`–`Z`, `a`–`z`, `0`–`9`, `-` and `.`; its `params` have names of 1 to 64
|
||
bytes and values of 0 to 255 bytes, each of RFC 3986's unreserved characters and
|
||
percent-encodings, and names in strictly ascending bytewise order (content-steering-04
|
||
§5 appends them in UTF-8 order, which is bytewise for these characters);
|
||
- each pathway's `top_tier` < *n*; each has at least one hop and at least one edge hop; each
|
||
place is 1 or 2; no hop key appears twice in the policy, in one pathway or in two;
|
||
- the sum of the `min_share_ppm` is at most 1 000 000.
|
||
|
||
**A hop belongs to one pathway.** A hop that carries two pathways' traffic signs one figure
|
||
for both, and no record states how it divides; a share of it would be an allocation, not a
|
||
reading. A hop common to every pathway, such as the origin that feeds them all, changes no
|
||
comparison and is listed in none.
|
||
|
||
### 3.4 Digest and signature
|
||
|
||
The **digest** of a policy is SHA-256 over its bytes before `sig`. Its **signature** is
|
||
Ed25519 (RFC 8032) by `signer` over the 20 bytes `wai:steering-policy` (ASCII) and 0x01,
|
||
followed by the bytes the digest covers, verified strictly as [SPEC](../SPEC.md) §7
|
||
*Signatures* requires: a key that fails step 1 or 2 is `bad_key`, a signature that fails a
|
||
later step `bad_signature`.
|
||
|
||
### 3.5 Who signs: the steering authority
|
||
|
||
A policy and a decision are signed by the room authority, or by a **steering key**: a key
|
||
the room's origin's key log lists, not revoked, with the authority `live/steer` in its
|
||
`may_sign` (jwp-receipts §7.5). A decision restates figures with their classes, so its
|
||
signer's entry, the room authority's included, **MUST** also hold `energy/` followed by the
|
||
label of each class among its figures (§7.1), as jwp-receipts §7.5 requires of every signer
|
||
that restates a figure with its class. A room that names no origin has no key log, and only
|
||
its authority signs. A signer that is neither is `signer_not_authorised`, and a steering key
|
||
whose entry is `revoked` is `signer_revoked` (the codes of live-metering §4.3).
|
||
|
||
Steering is the operator's act, as metering is, and its key lives in the origin's key log
|
||
for the reason live-metering §4.3 gives: a decision moves every client of the room, and a
|
||
grant's powers are live-rooms' closed set.
|
||
|
||
## 4. The client's report
|
||
|
||
A steering request is an HTTP GET of the steering server's URI, or of the last manifest's
|
||
`RELOAD-URI`. The server reads three things from it, each **absent** when the request does
|
||
not carry it in the form below; a value that does not read is absent, never zero:
|
||
|
||
- the client's **current pathway**, *c*: the index of a pathway of the policy;
|
||
- its **throughput**, *T*: bits per second;
|
||
- its **buffer**, *B*: milliseconds.
|
||
|
||
### 4.1 The query
|
||
|
||
The query is the request target's bytes after its first `?` and before any `#`. It is cut
|
||
at each `&` into parts; a part is cut at its first `=` into a name and a value (a part with
|
||
no `=` has an empty value), and both are percent-decoded (RFC 3986 §2.1; `+` is not a
|
||
space). A part with a `%` not followed by two hexadecimal digits is skipped. Of the parts
|
||
with a name the server reads, the first is read and later ones are ignored. A value is
|
||
**unquoted** by removing one `"` at each end when it both begins and ends with one.
|
||
|
||
An **integer** here is 1 to 20 ASCII digits whose value is at most 2^64 − 1.
|
||
|
||
### 4.2 HLS
|
||
|
||
rfc8216bis-22 §7.4 has the client add `_HLS_pathway="<CURRENT-PATHWAY-ID>"` and
|
||
`_HLS_throughput=<THROUGHPUT>`, the throughput an integer number of bits per second:
|
||
|
||
- *c*: `_HLS_pathway`, unquoted, when it is the `id` of a pathway of the policy;
|
||
- *T*: `_HLS_throughput`, when it is an integer.
|
||
|
||
### 4.3 DASH
|
||
|
||
ETSI TS 103 998 clause 7 step 7 has the client add `_DASH_pathway`, the service locations it
|
||
used since its last request, comma-separated within one pair of double quotes, and
|
||
`_DASH_throughput`, a comma-separated list of integer bits per second, the *n*-th for the
|
||
*n*-th service location:
|
||
|
||
- *c*: of `_DASH_pathway`, unquoted and cut at each `,`, the first item that is the `id` of a
|
||
pathway of the policy, at position *j* of the list (from 0);
|
||
- *T*: of `_DASH_throughput`, cut at each `,`, the item at position *j*, or at 0 when no
|
||
pathway is current, when it is an integer.
|
||
|
||
The clause's own example writes `_DASH_pathway` without quotes, so both are read.
|
||
|
||
### 4.4 CMCD
|
||
|
||
A client that reports CMCD (CTA-5004-B) sends its keys as the query parameter `CMCD`, or in
|
||
the header shards; CTA-5004-B assigns `bl` (buffer length, milliseconds) and `mtp`
|
||
(measured throughput, kilobits per second) to the shard `CMCD-Request`. The server reads
|
||
the `CMCD` parameter's value or, when the query has no such part, the `CMCD-Request`
|
||
header's value, as an RFC 9651 Dictionary (§4.2.2 of that RFC, the whole grammar, Dates and
|
||
Display Strings included, and a Byte Sequence whose content is not base64 of RFC 4648 §4, its
|
||
padding supplied where it is missing, refused); a value that does not parse gives neither
|
||
key. A key read is:
|
||
|
||
- an Integer of 0 or above; or a non-empty Inner List of such Integers, each with any
|
||
parameters (CTA-5004-B carries one per object type, its token a parameter): the **least**
|
||
of them, the object type whose buffer runs out first, or the slowest measurement;
|
||
- anything else, absent.
|
||
|
||
Then *B* is `bl`, and when §4.2 or §4.3 gives no *T*, *T* is `mtp` × 1000.
|
||
|
||
## 5. A pathway's evidence
|
||
|
||
### 5.1 The window
|
||
|
||
A pathway's hops cut their per-hop records (live-metering §3.8) on the policy's period: each
|
||
record's span runs from one multiple of *P* on the room clock to a later one. At a decision's
|
||
instant *t* on the room clock the **window** is [*a*, *b*), with
|
||
|
||
b = ⌊(t − L) / P⌋ × P (0 when t < L)
|
||
a = b − W (0 when b < W)
|
||
|
||
so that the window ends on the last period that has been over for at least *L*: *L* gives a
|
||
hop's record of a period time to reach the steering server before the server needs it. A
|
||
record is **in the window** when *a* ≤ its `start` and its `end` ≤ *b*.
|
||
|
||
### 5.2 Coverage and staleness
|
||
|
||
A decision names, for each pathway, the records of its hops it read, grouped by hop in the
|
||
pathway's hop order and each hop's in ascending order of `start` (§7.1). A hop is
|
||
**covered** when the records named for it **tile** the window: there is at least one, the
|
||
first starts at *a*, each starts where the one before it ends, and the last ends at *b*. The
|
||
window is then empty for no hop: when *a* = *b* no hop is covered.
|
||
|
||
A pathway's evidence is **stale** when one of its hops is not covered: a period the hop has
|
||
not signed, or whose record has not reached the server within *L*; a gap; an overlap; a
|
||
record that does not reach to *b*. Its energy over the window is then not known, and §6
|
||
ranks it so. Because the window is tiled exactly, every hop of every pathway is read over
|
||
the same span, so the figures of two pathways describe the same stretch of room clock; and a
|
||
server that leaves out a record makes a pathway stale. It cannot make one look cheaper.
|
||
|
||
### 5.3 The figure
|
||
|
||
Over the records named for a pathway, its **status** is the first of these that holds:
|
||
|
||
1. **stale** (3): a hop is not covered (§5.2);
|
||
2. **unmetered** (1): a record's figure is the unmetered marker;
|
||
3. **mixed** (2): the records' classes are not all one;
|
||
4. **empty** (4): *D* = 0;
|
||
5. **figure** (0): otherwise.
|
||
|
||
Its **joules** *J* is the sum of the records' `joules`, in microjoules, and its **delivery**
|
||
*D* is, over the records of its edge hops:
|
||
|
||
- with `basis` 1, the sum of the `bytes` of every track entry of kind 1 (a media track) and
|
||
direction 2 (out): the media bytes the pathway delivered;
|
||
- with `basis` 2, the sum of `subscriber_us`[*c*] for the layer classes *c* from *F* to 7:
|
||
the microseconds of delivery at the floor tier or above.
|
||
|
||
A pathway of status figure has the **figure** *J* / *D*, in the records' common class; a
|
||
figure is never written as a quotient, and two figures are compared by their products (§6.2).
|
||
On `basis` 2 the pathway's every joule is charged to its delivery at the floor tier or above,
|
||
so the figure is never less than the joules per such second that any division of the hops'
|
||
joules among tiers would give.
|
||
|
||
### 5.4 What is not known is not zero
|
||
|
||
A pathway of any status other than figure has **no figure**, and its *J* and *D* are stated
|
||
as 0 (§7.2). In particular:
|
||
|
||
- **An unmetered hop is not a hop of zero joules.** A sum over a pathway's records that
|
||
leaves out an unmetered record would state less energy than the pathway spent, and the
|
||
less a pathway measured the cheaper it would look. A decision that gives a pathway with an
|
||
unmetered record a figure is refused `unmetered_as_zero`.
|
||
- **Figures of two classes are not summed.** A `ModelBased` figure added to an
|
||
`OnChipCounter` figure is neither, and labelled as either it would present a model as a
|
||
reading or a reading as a model (live-metering §4.5). A decision that gives a pathway of
|
||
mixed classes a figure is refused `classes_summed`.
|
||
- **Stale evidence is not current evidence**, and a pathway whose hops delivered nothing
|
||
(empty) has no rate to state.
|
||
|
||
## 6. The decision rule
|
||
|
||
The rule is a function of the policy; the evidence (§5); the client's report (§4); the
|
||
client's **session**, 16 bytes the server minted at the client's first request and carries
|
||
in the manifest's `RELOAD-URI` (§8.1); and each pathway's **load**, the number of sessions
|
||
the server is steering to it first, its own count. Its output is the order of the pathways,
|
||
a TTL and the reason the first was chosen.
|
||
|
||
### 6.1 Limits
|
||
|
||
A pathway *p* is **within the limits** for the request when all three hold:
|
||
|
||
- **latency**: its `latency_ms` ≤ `latency_ceiling_ms`;
|
||
- **quality**: its `top_tier` ≥ *F*: it carries the floor tier;
|
||
- **capacity**: its `capacity` is 2^32 − 1; or its load < `capacity`; or it is *c* and its
|
||
load ≤ `capacity`, since *c*'s load already counts the client.
|
||
|
||
### 6.2 The order
|
||
|
||
A pathway's **rank** is set by its evidence: 0 for a figure of class 5
|
||
(`CalibratedInstrument`), 1 of class 4 (`OnChipCounter`), 2 of class 2 (`ModelBased`), 3 of
|
||
class 3 (`Estimator`), and 4 for no figure. Pathway *p* comes before *q* when, of these, the
|
||
first that tells them apart says so:
|
||
|
||
1. *p*'s rank is below *q*'s;
|
||
2. both have a figure, of one class, and *J*<sub>*p*</sub> × *D*<sub>*q*</sub> <
|
||
*J*<sub>*q*</sub> × *D*<sub>*p*</sub>: *p*'s figure is the lower;
|
||
3. *p*'s `id` is below *q*'s, bytewise.
|
||
|
||
Every two pathways differ in `id`, so the order is total. Figures are compared only within a
|
||
class (step 2). Across classes the order is by what backs the figure, the order
|
||
energy-measurement §2 gives its classes, and never by the figures: no figure of one class
|
||
can be compared with one of another, so the rule cannot tell which of two such pathways
|
||
spends less, and it prefers the evidence it can rely on. A pathway with no figure comes after
|
||
every pathway with one (step 1), and among pathways with no figure only the `id` orders them
|
||
(step 3): being unknown cannot raise a pathway above any pathway whose energy is known, and
|
||
nothing about an unknown pathway's energy orders it.
|
||
|
||
### 6.3 The draw and the minimum shares
|
||
|
||
The client's **draw** is
|
||
|
||
d = u32(SHA-256("wai:steering-draw" ‖ 0x01 ‖ policy digest ‖ session)[0..4]) mod 1 000 000
|
||
|
||
the first four bytes read as a u32. It is fixed for the session, so it moves no client from
|
||
one decision to the next. The policy's pathways, in its order, divide [0, 1 000 000) from 0
|
||
into **bands**: pathway *i*'s band is [*S*<sub>*i*</sub>, *S*<sub>*i*</sub> +
|
||
`min_share_ppm`<sub>*i*</sub>), with *S*<sub>*i*</sub> the sum of the shares before it. A
|
||
session whose draw falls in a pathway's band is placed on that pathway first while it is
|
||
within the limits (§6.4 step 4), whatever its evidence.
|
||
|
||
The minimum shares are how evidence stays fresh: a pathway no client is steered to delivers
|
||
nothing, and its records soon state no delivery, or none at all. A pathway's share of
|
||
sessions from its band is its `min_share_ppm`, the same whether its energy is known or not,
|
||
so a pathway gains no session by being unknown.
|
||
|
||
### 6.4 The first pathway
|
||
|
||
The client's **sustainable tier** is, when *T* is known, the highest tier whose bitrate is
|
||
at most *T*, or none when tier 0's is above it. The client is **at its floor** when *T* is
|
||
known and its sustainable tier is none or at most *F*.
|
||
|
||
Let *w* be the first pathway of the order (§6.2) that is within the limits. *w* **beats** a
|
||
pathway *x* of its rank when both have a figure and
|
||
|
||
J_w × D_x × (1 000 000 + h) < J_x × D_w × 1 000 000
|
||
|
||
that is, *w*'s figure is below *x*'s by more than the margin *h*. The first pathway, and the
|
||
**reason** it is first, is given by the first of these steps that applies:
|
||
|
||
1. **buffer** (1): *c* is known and within the limits, *B* is known, and *B* <
|
||
`min_buffer_ms`: *c*. A switch costs a fresh start on the new pathway, which a short
|
||
buffer may not cover.
|
||
2. **floor** (2): *c* is known and within the limits, and the client is at its floor: *c*.
|
||
Its throughput was measured on *c*, and a client with no margin above the floor is not
|
||
moved to a pathway on which it was never measured.
|
||
3. **none** (6): no pathway is within the limits: *c* when it is known, otherwise the first
|
||
pathway of the order. The client is not moved for a limit no pathway meets
|
||
(content-steering-04 §7 step 5 has it stay).
|
||
4. **share** (3): the draw falls in the band of a pathway *e* within the limits: *e*.
|
||
5. **hold** (4): *c* is known and within the limits, *c* is not *w*, *c* has *w*'s rank, and
|
||
either that rank is 4 or *w* does not beat *c*: *c*. This is the **hysteresis**: a client
|
||
moves only for a figure lower than its own by more than the margin, so that two figures
|
||
within the margin of each other do not swap its pathway from one decision to the next.
|
||
Between two pathways with no figure nothing says that either is cheaper, and the client
|
||
stays.
|
||
6. **rank** (5): *w*.
|
||
|
||
Steps 1, 2 and 4 hold a client or place it whatever its pathway's evidence, and so do not
|
||
prefer a pathway for being unknown; step 5 holds a client only against a pathway of its own
|
||
rank, so a client on a pathway with no figure moves as soon as a pathway with a figure is
|
||
within the limits.
|
||
|
||
### 6.5 The priority and the TTL
|
||
|
||
The **priority** is the first pathway, then the pathways within the limits in the order
|
||
(§6.2), then the others in the order, each once. Every pathway is in it, so a client that
|
||
penalises its first pathway (content-steering-04 §7 step 3) falls back in the same order.
|
||
|
||
The **TTL** is `min_ttl_s` when the reason is buffer, floor or none, so that a client held
|
||
or left without a pathway within the limits asks again soon; and `ttl_s` otherwise.
|
||
|
||
## 7. The steering decision
|
||
|
||
### 7.1 Layout
|
||
|
||
For each request the steering server signs a decision, `WSD1`:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WSD1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `signer` | 32 | the key that signs the decision (§3.5) |
|
||
| `policy` | 32 | the policy's digest |
|
||
| `t` | 8 | u64: the decision's instant on the room clock, in microseconds |
|
||
| `session` | 16 | the client's session (§6) |
|
||
| `draw` | 4 | u32: *d* (§6.3) |
|
||
| `protocol` | 1 | 1 HLS (§4.2), 2 DASH (§4.3): which parameters the report was read from |
|
||
| `report` | 18 | `present` (u8: bit 0 *c*, bit 1 *T*, bit 2 *B*), `pathway` (u8: *c*), `throughput` (u64: *T*), `buffer` (u64: *B*); an absent value is 0 |
|
||
| `pathways` | 1 + … | a u8 count, the policy's, then for each pathway in the policy's order: `load` (u32); `limits` (u8: bit 0 latency, bit 1 quality, bit 2 capacity, each set when it holds, §6.1); `status` (u8, §5.3); `class` (u8: the figure's tag, 0 for no figure); `joules` (u128: *J*); `delivery` (u128: *D*); `evidence`, a u8 count and the digest of each record named (§5.2) |
|
||
| `ttl` | 4 | u32: the TTL (§6.5) |
|
||
| `reason` | 1 | the first pathway's reason (§6.4): 1 buffer, 2 floor, 3 share, 4 hold, 5 rank, 6 none |
|
||
| `priority` | 1 + *m* | a u8 count *m*, then the index of each pathway, the first first (§6.5) |
|
||
| `sig` | 64 | §7.3 |
|
||
|
||
### 7.2 Reading and rules
|
||
|
||
A reader reads the bytes as §3.3 reads a policy, with `WSD1` for `WSP1`, and refuses as
|
||
`decision_invalid` a decision that breaks any of these:
|
||
|
||
- `protocol` is 1 or 2; `draw` < 1 000 000; `ttl` ≥ 1; `reason` is 1 to 6;
|
||
- `present` has no bit above 2; an absent value is 0; a present `pathway` is below the
|
||
count of `pathways`;
|
||
- each pathway's `limits` has no bit above 2 and its `status` is 0 to 4; with status 0 its
|
||
`class` is 2 to 5 and its `joules` and `delivery` are above 0; with any other status its
|
||
`class`, `joules` and `delivery` are 0;
|
||
- no digest is named twice in the decision;
|
||
- `priority` names each pathway once: *m* is the count of `pathways` and its indices are a
|
||
permutation of 0 to *m* − 1.
|
||
|
||
### 7.3 Digest and signature
|
||
|
||
The **digest** of a decision is SHA-256 over its bytes before `sig`; its **signature** is
|
||
Ed25519 by `signer` over the 22 bytes `wai:steering-decision` (ASCII) and 0x01, followed by
|
||
the bytes the digest covers, verified strictly (SPEC §7 *Signatures*). Who may sign it is
|
||
§3.5's rule.
|
||
|
||
## 8. The mappings
|
||
|
||
### 8.1 The steering manifest
|
||
|
||
The output of a decision is one steering manifest (`draft-pantos-content-steering-04` §4,
|
||
the DASH Content Steering Manifest of ETSI TS 103 998 clause 6), served with the media type
|
||
§11.1 of that draft registers. It is the JSON text (RFC 8259) below, UTF-8, with no white
|
||
space outside its strings, its members in this order:
|
||
|
||
{"VERSION":1,"TTL":<ttl>,"RELOAD-URI":"<reload>","PATHWAY-PRIORITY":[<ids>],
|
||
"PATHWAY-CLONES":[<clones>],"science.transaction.wai-decision":"<decision>"}
|
||
|
||
(one line, cut here for legibility), where:
|
||
|
||
- `<ttl>` is the decision's `ttl` in decimal digits, with no leading zero;
|
||
- `<reload>` is the policy's `reload_uri`, then `&` when it holds a `?` and `?` otherwise,
|
||
then `wai_session=` and the session as 32 lowercase hexadecimal digits;
|
||
- `<ids>` is the `id` of each pathway of the `priority`, in its order, each in double
|
||
quotes, separated by `,`;
|
||
- `"PATHWAY-CLONES":[<clones>],` is written only when the policy has a clone. `<clones>` is,
|
||
for each clone in the policy's order, separated by `,`:
|
||
`{"BASE-ID":"<base id>","ID":"<id>","URI-REPLACEMENT":{<replacement>}}`, where
|
||
`<replacement>` is `"HOST":"<host>"` when the clone has a host, then (after a `,` when a
|
||
host was written) `"PARAMS":{<params>}` when it has params, `<params>` being
|
||
`"<name>":"<value>"` for each, in the policy's order, separated by `,`; and empty when it
|
||
has neither;
|
||
- `<decision>` is the policy's `decision_uri`, then the decision's digest as 64 lowercase
|
||
hexadecimal digits (§8.4).
|
||
|
||
§3.3's character rules leave nothing in these strings that JSON escapes, so the text is
|
||
fixed byte for byte. `PATHWAY-CLONES` is written for every clone, whether or not it is
|
||
first, so that a client can fall back to any pathway of the priority.
|
||
|
||
**HLS and DASH are the same bytes.** ETSI TS 103 998 clause 6.2 defines the same keys as
|
||
content-steering-04 §4 and §5, and a DASH client ignores the `URI-REPLACEMENT` keys HLS
|
||
adds (`PER-VARIANT-URIS`, `PER-RENDITION-URIS`, rfc8216bis-22 §7.3; clause 7 step 13),
|
||
which a policy of this revision does not state. A DASH `PATHWAY-PRIORITY` names service
|
||
locations, which are the same IDs (§3.2). The two protocols differ in the request (§4) and
|
||
in the content description (§8.2, §8.3).
|
||
|
||
### 8.2 HLS
|
||
|
||
The multivariant playlist carries the policy as one tag (rfc8216bis-22 §4.4.6.6):
|
||
|
||
#EXT-X-CONTENT-STEERING:SERVER-URI="<server_uri>",PATHWAY-ID="<initial id>"
|
||
|
||
with the policy's `server_uri` and the `id` of its `initial` pathway. The tag's
|
||
`PATHWAY-ID` is the pathway a client applies until its first steering manifest, and
|
||
content-steering-04 §4 asks that the first manifest's first pathway agree with it: a client
|
||
whose first request carries no report and whose draw is in no band gets the first pathway of
|
||
the order, which an operator **SHOULD** make the initial one when it writes the policy.
|
||
|
||
### 8.3 DASH
|
||
|
||
The MPD carries the policy as one element (ETSI TS 103 998 clause 5.2):
|
||
|
||
<ContentSteering defaultServiceLocation="<initial id>"
|
||
queryBeforeStart="<true|false>"><server_uri></ContentSteering>
|
||
|
||
(one line), `queryBeforeStart` `true` when the policy's flag bit 0 is set and `false`
|
||
otherwise, and the URI with each `&` written `&`, the only character of §3.3's URIs that
|
||
XML text escapes. With `queryBeforeStart` `true` a client asks before it starts (clause 7
|
||
step 5), and its first request carries no `_DASH_` parameters, so its first decision has no
|
||
report.
|
||
|
||
### 8.4 Carrying the decision
|
||
|
||
The decision record is served at the URI the manifest's `science.transaction.wai-decision`
|
||
member names: the policy's `decision_uri` followed by the decision's digest in hexadecimal.
|
||
A player ignores the member: content-steering-04 §4 lets a manifest hold other keys and has
|
||
a client ignore any key it does not recognise, and ETSI TS 103 998 clause 6.1 does the same.
|
||
The member is the decision's digest, not its signature over the manifest: the manifest is a
|
||
function of the decision and the policy (§8.1), and its member names the decision, so there
|
||
is no loop between them. A party that holds the manifest fetches the record, verifies it
|
||
(§9), and requires that the manifest be the bytes §8.1 writes from it. A steering server
|
||
**SHOULD** keep each decision available for as long as the records it names are.
|
||
|
||
## 9. Verification
|
||
|
||
### 9.1 Inputs and steps
|
||
|
||
A verifier is given the room's descriptor and the head of its authority's key log, or none;
|
||
the policy; the decision; the per-hop records; and, optionally, the manifest that was served
|
||
with the decision. It runs these steps and stops at the first refusal, which it reports with
|
||
the reason and where:
|
||
|
||
1. **The room**, at `room`: the descriptor reads and verifies, and the head is held for its
|
||
origin and lists its authority (live-rooms §9.1 steps 1 and 2, with that section's
|
||
reasons).
|
||
2. **The policy** (§3), at `policy`: `truncated`, `malformed`, `unknown_version`,
|
||
`clone_unknown`, `policy_invalid`; `wrong_room` when its `room` is not the room id;
|
||
`bad_key`, `bad_signature`; its signer (§3.5), `signer_not_authorised`,
|
||
`signer_revoked`.
|
||
3. **The decision** (§7), at `decision`: `truncated`, `malformed`, `unknown_version`,
|
||
`decision_invalid`; `wrong_room`; `record_missing` when its `policy` is not the given
|
||
policy's digest; `decision_invalid` when its count of `pathways` is not the policy's, or a
|
||
present `pathway` is not a pathway of it; `bad_key`, `bad_signature`; its signer,
|
||
`signer_not_authorised`, `signer_revoked`.
|
||
4. **Each record it names**, in order, at `evidence` with the pathway's index and the
|
||
record's place in its list:
|
||
1. a record whose digest is the one named is given: otherwise `record_missing`;
|
||
2. it reads, its room is the room id and its signature verifies (live-metering §3.6:
|
||
`truncated`, `malformed`, `unknown_version`, `energy_invalid`, `record_invalid`,
|
||
`wrong_room`, `bad_key`, `bad_signature`);
|
||
3. its hop may sign it (live-metering §5.2: `hop_not_authorised`, `hop_revoked`): a key
|
||
the room's key log does not list for its role and class does not sign evidence;
|
||
4. its `hop` is a hop of the pathway it is named under: otherwise
|
||
`evidence_not_on_pathway`;
|
||
5. it is in the window of the decision's `t` (§5.1): otherwise `evidence_outside_window`;
|
||
6. it follows the record named before it under the pathway in the order of §5.2, a later
|
||
hop of the pathway or the same hop and a later `start`: otherwise `decision_invalid`.
|
||
5. **The evidence** (§5), at `pathway` and its index: each pathway's status, class, *J* and
|
||
*D* recompute from its records. A pathway stated with a figure whose records hold the
|
||
unmetered marker is `unmetered_as_zero`; one stated with a figure whose records are of
|
||
more than one class is `classes_summed`; any other difference is `evidence_mismatch`.
|
||
6. **The output** (§6), at `output`: the draw, each pathway's `limits`, the priority, the
|
||
TTL and the reason recompute from the policy, the evidence, the report, the session and
|
||
the loads. When they do not, the decision is `unknown_preferred` if its first pathway has
|
||
no figure while the first of the recomputed priority has one, and `decision_mismatch`
|
||
otherwise.
|
||
7. **The manifest**, at `manifest`, when one is given: it is the bytes §8.1 writes from the
|
||
decision and the policy, otherwise `manifest_mismatch`.
|
||
|
||
A decision that passes is **accepted**: the priority, the TTL and the reason are what the
|
||
rule gives for the evidence the records state, the report and the loads it names.
|
||
|
||
### 9.2 What is recomputed and what is stated
|
||
|
||
A decision has inputs of three kinds, and a verifier treats them differently:
|
||
|
||
- **The evidence** is signed by the hops and verified: its figures cannot be other than the
|
||
records give.
|
||
- **The report** is the client's, as the server read it (§4). The client does not sign it,
|
||
and the decision states it rather than the request, so that a request's other parameters
|
||
(tokens, the session) are not published with it.
|
||
- **The session and the loads** are the server's own. The decision states them; a verifier
|
||
recomputes the rule from them and the draw from the session, and an auditor that holds a
|
||
server's decisions checks over them that the loads and the bands' shares are what the
|
||
server claims.
|
||
|
||
### 9.3 Outcome
|
||
|
||
- **refuse**: the reason and where (§9.1).
|
||
- **accept**: the decision's digest; the priority by pathway ID; the TTL and the reason;
|
||
each pathway's status, class, *J*, *D* and limits; the steering manifest; and the record's
|
||
URI.
|
||
|
||
## 10. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `policy_invalid` | §3.3, §9.1 step 2 |
|
||
| `clone_unknown` | §3.3, §9.1 step 2 |
|
||
| `decision_invalid` | §7.2, §9.1 steps 3 and 4 |
|
||
| `evidence_not_on_pathway` | §9.1 step 4 |
|
||
| `evidence_outside_window` | §5.1, §9.1 step 4 |
|
||
| `evidence_mismatch` | §5.3, §9.1 step 5 |
|
||
| `unmetered_as_zero` | §5.4, §9.1 step 5 |
|
||
| `unknown_preferred` | §6.2, §9.1 step 6 |
|
||
| `decision_mismatch` | §6, §9.1 step 6 |
|
||
| `manifest_mismatch` | §8.1, §9.1 step 7 |
|
||
|
||
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
|
||
policy, a decision or a record that does not read; `bad_key` and `bad_signature` for their
|
||
signatures; `wrong_room` for one of another room; live-metering's `classes_summed`,
|
||
`signer_not_authorised`, `signer_revoked`, `record_missing`, `energy_invalid`,
|
||
`record_invalid`, `hop_not_authorised` and `hop_revoked`; and live-rooms §9.1's codes for
|
||
the room, at `room`.
|
||
|
||
## 11. Conformance
|
||
|
||
[`live-conformance/steering/`](../live-conformance/steering/) holds the corpus, in one file,
|
||
`vectors.json`, written from fixed seeds by `wai_energy_steering_vectors gen`. It sits beside
|
||
the metering corpus because its evidence is per-hop records of live-metering, read by that
|
||
corpus's second implementation. It holds a room with its key log, policies, the per-hop
|
||
records of the pathways' hops, and:
|
||
|
||
- **requests**: steering requests over HLS and DASH, with and without CMCD in the query and
|
||
in the header, with the report §4 reads from each;
|
||
- **vectors**, each a policy, a decision, the records it names and the manifest served with
|
||
it, with the outcome §9.3 gives them. **Accepted**: three pathways with figures of one
|
||
class; mixed classes, ordered by class; a pathway with an unmetered hop, ranked after those
|
||
with figures though its metered hops alone are the cheapest; stale evidence; a client at
|
||
its floor, and one with a short buffer, held; hysteresis holding a client and, a period
|
||
later, moving it; pathway clones; a session placed by its band; a pathway at its capacity
|
||
and one past the latency ceiling; the per-second basis; DASH; and no pathway within the
|
||
limits; each with its steering manifest, HLS tag and DASH element byte for byte. Two
|
||
more show what has no figure: hops of two classes in one pathway, and, beside the session
|
||
its band places, one outside the band, whose unmetered pathway comes last.
|
||
**Refused**: each reason of §10, and the codes it reuses from the extensions it reads, at
|
||
least once, each vector a control of one defect.
|
||
|
||
A conforming verifier gives every request its report and every vector the outcome, the
|
||
reason, the place and the facts the corpus states. `wai_energy_steering_vectors verify` and
|
||
the test `tests/energy_steering.rs` check that the file regenerates byte for byte, that
|
||
`wai-rs` gives every request and vector its outcome, that every reason of §10 refuses a
|
||
vector, and that §10 lists exactly the reference's codes.
|
||
`live-conformance/energy_steering_verify.py` is the second implementation, written from
|
||
this text: it shares no code with `wai-rs`, verifies signatures with WAI's strict rule written
|
||
from RFC 8032 (`claim-conformance/_ed25519.py`), reads the room with `live_rooms_verify.py`
|
||
and the per-hop records with `live_metering_verify.py`, parses RFC 9651 itself, recomputes
|
||
every report, figure, decision and manifest with Python's integers, gives every request and
|
||
vector the same outcome, and requires every mutated copy of the corpus to fail.
|
||
|
||
The corpus's energy figures are synthetic, chosen to exercise the rule. It computes which
|
||
pathway the rule places first, and claims nothing about the energy any deployment spends or
|
||
saves.
|
||
|
||
## 12. Security and privacy considerations
|
||
|
||
- **The evidence is signed; the choice is checkable.** A steering server cannot state a
|
||
figure the records do not give, or a priority the rule does not, without its decision
|
||
being refused. What it can do is leave records out, which makes a pathway stale (§5.2)
|
||
and so ranks it lower, never higher; and state its own loads and sessions (§9.2), which
|
||
only an audit over its decisions checks.
|
||
- **Not measuring is never rewarded.** No status other than figure gives a pathway a
|
||
figure (§5.4), no pathway with no figure ranks above one with a figure (§6.2), and the
|
||
rules that place a client whatever its pathway's evidence (§6.4 steps 1, 2 and 4) give a
|
||
pathway with no figure no more than they give one with a figure. An operator that wants a
|
||
pathway preferred meters it.
|
||
- **Measuring better is rewarded, measuring worse is not penalised below unknown.** Across
|
||
classes the order is by what backs the figure; a pathway of the weakest class still ranks
|
||
above every pathway with no figure.
|
||
- **A hop's figure is its own statement** (energy-measurement §5): the decision makes the
|
||
choice recomputable from what the hops signed, not the figures true. A hop that understates
|
||
its energy wins traffic, and the remedies are those of live-metering §11: its class says
|
||
how the figure was obtained, the key log confines which classes a key may state, and an
|
||
upstream hop's record contradicts a downstream one's.
|
||
- **Flapping.** The draw is fixed for a session, the window moves one period at a time, and
|
||
the margin *h* keeps a client on its pathway against a figure within it (§6.4 step 5).
|
||
- **Privacy.** A decision holds no identifier of the client beyond its session, which the
|
||
server minted and which names no person; the report is the throughput and buffer the
|
||
client sends to every steering server; and the decision does not carry the request, so a
|
||
token in its query is not published with it (§9.2). A CMCD report is read for `bl` and
|
||
`mtp` alone, and the decision keeps nothing else of it.
|