All checks were successful
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 4s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m45s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m23s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m10s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 17s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
WAI conformance / does this change touch it (pull_request) Successful in 11s
WAI energy receipts / does this change touch it (pull_request) Successful in 16s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 13s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 16s
WAI neural sink gates / does this change touch it (pull_request) Successful in 12s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 11s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m55s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 10s
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Successful in 10s
wai-web wasm32 / does this change touch it (pull_request) Successful in 10s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 15m35s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m10s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 32s
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 21m37s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 3m3s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m54s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m35s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 51s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 12m25s
wai-pack / wai-pack · linux (pull_request) Successful in 1m21s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m18s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 3m21s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 20s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 39s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m57s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m25s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 6m3s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 4m35s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 14m26s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 31s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m2s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 17m25s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 2m19s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 20m35s
wai-pack / wai-pack · macos (pull_request) Successful in 1m23s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 5m31s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 2m35s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 54m46s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 52m38s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 6m32s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 52m15s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 5s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 3m22s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 7m47s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m26s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m35s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 23s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m28s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 7m32s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 5m11s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
592 lines
34 KiB
Markdown
592 lines
34 KiB
Markdown
# WAI Extension: Live Composition
|
||
|
||
> Status: Draft. Registers the record `wai.live.compose`: the deterministic composition
|
||
> of a live room's participants. A signed **composition descriptor** fixes a canvas, a
|
||
> background, a layout (a grid, with a featured tile, or a grid under a
|
||
> picture-in-picture window), the grant attributes it reads and its limits. The
|
||
> **participant set** at a room-clock instant is derived from the room's lifecycle
|
||
> replay ([live-rooms](live-rooms.md) §9): who publishes, each one's video and audio
|
||
> source and its dimensions, the attributes of its grant in effect, and its join order.
|
||
> The layout is then a pure integer function of the descriptor and the set, and the mix
|
||
> a pure integer function of decoded PCM, so every sink composes the same rectangles
|
||
> locally, with no server encode, and a server compositor that publishes a composed track
|
||
> signs a **composition receipt** that anyone can check against the same function. No new
|
||
> transport: a composed track is a track of the room, and the receipt names the objects it
|
||
> was made from. The reference implementation is the `live_compose` module of `wai-rs`
|
||
> (feature `live_compose`); the corpus is
|
||
> [`live-conformance/compose/`](../live-conformance/compose/), and its second
|
||
> implementation is `live-conformance/live_compose_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: `draft-ietf-moq-transport-22` (MOQT), an Internet-Draft
|
||
> not past Working Group Last Call, for the Full Track Name (§2.4.1) and the Location
|
||
> (§8.2); RFC 6716 §6; RFC 8032; FIPS 180-4; and IEC 61966-2-1 (sRGB). A section number of
|
||
> that draft in this text is that revision's.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the composition descriptor, its digest and signature, its limits and who may
|
||
sign it (§3); the participant set at an instant, the attributes the descriptor reads, the
|
||
order, and the set's digest (§4); the layout function (§5); the audio mix (§6); where
|
||
composition runs (§7); the composition receipt and its verification (§8, §9).
|
||
|
||
Out of scope: a transport; how a renderer scales and filters a source into its rectangle,
|
||
and how it converts the background's colour into its output's (the layout fixes
|
||
rectangles, not pixels); decoding, resampling and encoding; and the browser compositor
|
||
that renders a layout in a page, which a later slice builds from this function.
|
||
|
||
Relationship to the core spec: `wai.live.compose` 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**, **principal**, **event**, **chain**, **grant**, **grant in
|
||
effect**, **room key**, **merged order**, **slot**, **publish profile**: as in
|
||
live-events §2 and live-rooms §2, §8, §9.2.
|
||
- **Room authority**: the key that signs the room's descriptor (live-rooms §2).
|
||
- **Compose power**: bit 2 of a capability set's `powers` (live-rooms §3.3).
|
||
- **Canvas**: the picture a composition makes, `width` × `height` pixels.
|
||
- **Source**: the track a participant is composed from (§4.2).
|
||
- **Compositor**: the principal that publishes a composed track (§7).
|
||
- **Rectangle**: (`x`, `y`, `w`, `h`): its left edge and top edge, in pixels from the top
|
||
left of the canvas (or of a source), and its width and height.
|
||
- A **short** field is a u8 length and that many bytes. Integers are little-endian. A
|
||
digest is SHA-256 (FIPS 180-4), 32 bytes.
|
||
|
||
## 3. The composition descriptor
|
||
|
||
### 3.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCD1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id (live-rooms §3.2) |
|
||
| `signer` | 32 | the Ed25519 key that signs the descriptor (§3.5) |
|
||
| `effective` | 8 | u64: the room-clock instant from which the descriptor applies |
|
||
| `compositor` | 32 | the principal that may publish the composed tracks (§7); 32 zero bytes for none |
|
||
| `width`, `height` | 2 each | u16: the canvas, in pixels (§3.4) |
|
||
| `fps` | 1 | the canvas's frame rate, in frames per second (§3.4) |
|
||
| `background` | 3 | the canvas's colour: red, green and blue, 8 bits each, in sRGB (IEC 61966-2-1) |
|
||
| `layer` | 1 | the highest layer of a source composed (§4.2) |
|
||
| `layout` | 1 | 1 grid, 2 picture in picture (§5) |
|
||
| `gap` | 2 | u16: the pixels between two grid tiles |
|
||
| `aspect` | 1 | the grid tiles' aspect: 0 auto, 1 video (16:9), 2 square (1:1), 3 portrait (3:4) (§5.3) |
|
||
| `fill` | 1 | 0 default, 1 fill, 2 cover, 3 contain (§5.6) |
|
||
| `featured_attr` | short | the attribute key that marks the featured participant; empty for none (§4.3) |
|
||
| `order_attr` | short | the attribute key that orders participants; empty for none |
|
||
| `omit_stopped` | 1 | 1: a participant whose video is stopped has no tile; 0: it has one (§5.2) |
|
||
| `pip_attr` | short | the attribute key that marks the PiP participant; empty for none |
|
||
| `corner` | 1 | the PiP window's corner: 1 top left, 2 top right, 3 bottom left, 4 bottom right |
|
||
| `pip_width`, `pip_height` | 2 each | u16: the PiP window's size; 0 derives it (§5.5) |
|
||
| `offset` | 2 | u16: the window's distance, in pixels, from both edges of its corner |
|
||
| `behaviour` | 1 | what the PiP participant does when it is the only one composed: 1 static, 2 dynamic (§5.5) |
|
||
| `channels` | 1 | the mix's channels: 1 mono, 2 stereo (§6) |
|
||
| `sample_rate` | 4 | u32: the rate, in Hz, every voice is decoded to before it is mixed |
|
||
| `gain_attr` | short | the attribute key of a participant's gain; empty for none (§6) |
|
||
| `pan_attr` | short | the attribute key of a participant's pan; empty for none |
|
||
| `max_tiles` | 1 | the most tiles (§5.2) |
|
||
| `max_voices` | 1 | the most voices of the mix (§6) |
|
||
| `sig` | 64 | §3.2 |
|
||
|
||
A grid's `pip_attr` is empty and its `corner`, `pip_width`, `pip_height`, `offset` and
|
||
`behaviour` are 0 (§3.3).
|
||
|
||
### 3.2 Digest and signature
|
||
|
||
The **digest** of a descriptor is SHA-256 over its record from `magic` through
|
||
`max_voices`: every byte but `sig`. The **signature** is Ed25519 (RFC 8032) by `signer`
|
||
over the 17 bytes `wai:live-compose` (ASCII) and 0x01, followed by the bytes the digest
|
||
covers. It is verified strictly, as [SPEC](../SPEC.md) §7 *Signatures* requires: a key
|
||
that fails step 1 or 2 is `bad_key`, and a signature that fails a later step is
|
||
`bad_signature`.
|
||
|
||
### 3.3 Reading and field rules
|
||
|
||
A reader reads exactly the bytes it is given as one descriptor, in the order of §3.1, and
|
||
refuses at the first of these that holds:
|
||
|
||
1. Fewer than 4 bytes: `truncated`. A magic other than `WCD1`: `malformed`. No byte after
|
||
the magic: `truncated`. A version other than 1: `unknown_version`.
|
||
2. The bytes end before a field the layout requires: `truncated`.
|
||
3. Bytes after `sig`: `malformed`.
|
||
|
||
It then holds the canvas to §3.4, and refuses one beyond its limits as
|
||
`canvas_out_of_range`. It then refuses, as `compose_invalid`, a descriptor that breaks any
|
||
of these rules:
|
||
|
||
- `layout` is 1 or 2, `aspect` at most 3, `fill` at most 3, and `omit_stopped` 0 or 1.
|
||
- Each attribute key is empty or a slot name of live-rooms §2 (1 to 32 bytes of `a`–`z`,
|
||
`0`–`9` and `_`), which is what a grant attribute's key is (live-rooms §4.4).
|
||
- A grid has an empty `pip_attr`, and `corner`, `pip_width`, `pip_height`, `offset` and
|
||
`behaviour` 0. A picture in picture has a `corner` of 1 to 4 and a `behaviour` of 1 or 2,
|
||
an `offset` below both `width` and `height`, `pip_width` + `offset` at most `width`,
|
||
and `pip_height` + `offset` at most `height`.
|
||
- `channels` is 1 or 2, and `sample_rate` 8 000 to 192 000.
|
||
- `max_tiles` and `max_voices` are each 1 to 64.
|
||
- With *k* the least integer whose square is at least `max_tiles`: `gap` is below
|
||
`height`; `width` is at least *k* + (*k* − 1) × `gap`; and the band under a featured
|
||
tile (§5.4), `height` − `gap` − ⌊(`height` − `gap`) × 3 / 4⌋, is at least *k* +
|
||
(*k* − 1) × `gap`. Every grid the layout makes then has a candidate whose cells are at
|
||
least a pixel each way (§5.3).
|
||
|
||
Then the signature (§3.2): `bad_key` or `bad_signature`.
|
||
|
||
### 3.4 The canvas
|
||
|
||
`width` and `height` are each even, and each 16 to 1920; `width` × `height` is at most
|
||
2 073 600, the pixels of 1920 × 1080; and `fps` is 1 to 60. The edges are even so that a
|
||
picture subsampled 4:2:0 has whole chroma samples. A portrait 1080 × 1920 and a square
|
||
1440 × 1440 canvas are within these limits.
|
||
|
||
### 3.5 Who may sign
|
||
|
||
Against a room, a descriptor that reads (§3.3) is refused as `wrong_room` when its `room`
|
||
is not the room id; and as `compose_not_permitted` unless its `signer` is the room
|
||
authority, or a principal present at `effective` whose grant in effect then (§4.1) has the
|
||
compose power.
|
||
|
||
The power is checked at `effective`, when the descriptor takes effect: a descriptor does
|
||
not lapse when its signer leaves or later loses the power. A composition has one
|
||
descriptor; a room that changes its layout signs another, and each composed track and
|
||
each receipt names the one it follows (§8).
|
||
|
||
## 4. The participant set
|
||
|
||
### 4.1 The room at an instant
|
||
|
||
A composition reads a room that verifies (live-rooms §9): one refused is refused here
|
||
with the room's reason, at `room`. A room whose outcome is a gap is composed only when no
|
||
gap is in a room key's chain: the lifecycle is then whole, and a gap in another
|
||
principal's chain (a chat not delivered) changes no room state. A gap in a room key's
|
||
chain is `lifecycle_incomplete`, at `room`: the state at no instant after it is known.
|
||
|
||
The **room at** *t* is the room state (live-rooms §9.3) replayed from the lifecycle events
|
||
whose `t` is at most *t*, in the merged order: equally, the room each chain gives when it
|
||
is cut before its first record whose `t` is above *t*. Events at *t* itself are applied,
|
||
so a principal that joins at *t* is in the set at *t*.
|
||
|
||
The **join order** of a principal present at *t* is the number of the `join` that made it
|
||
present (its last), where the room's accepted `join` events up to *t* are numbered from 0
|
||
in the merged order, a duplicate (live-events §6.2 step 1) counted once.
|
||
|
||
### 4.2 Participants and their sources
|
||
|
||
The **participants** at *t* are the principals present at *t*, other than the
|
||
descriptor's `compositor`, that publish at *t*: each has an own track of any media kind
|
||
published, or is the live publisher of a slot (live-rooms §7.1). A standby is not
|
||
composed for its place in a slot, since its media is not delivered.
|
||
|
||
Each participant has:
|
||
|
||
- its **video source**: the first of its own `video` track; its own `screen` track; and
|
||
its live place in a slot of media kind `video` or `screen`, the first such slot in the
|
||
room descriptor's order. With none, its video is **stopped**.
|
||
- its **audio source**: its own `audio` track; or else its live place in the first slot of
|
||
media kind `audio`. With none, it has no voice.
|
||
- the attributes of its grant in effect at *t* (live-rooms §4.4).
|
||
|
||
A source's **profile** is the profile its track was published under (the `publish` or
|
||
`takeover` that made it, live-rooms §7.2), and its **layers** the number of layers
|
||
published. Its **composed layer** *L* is the lower of `layers` − 1 and the descriptor's
|
||
`layer`, and its width and height are those of layer *L* of the profile (live-rooms §8.1);
|
||
an audio source's are 0. A descriptor whose `layer` is below a publisher's top layer
|
||
composes from a layer the compositor's grant can subscribe to, and every sink lays the
|
||
room out in that layer's pixels.
|
||
|
||
### 4.3 Attributes
|
||
|
||
For each participant, each attribute key the descriptor names (not empty) is looked up
|
||
among the attributes of its grant in effect, and read:
|
||
|
||
| key | value | when absent |
|
||
|---|---|---|
|
||
| `featured_attr`, `pip_attr` | `true` or `false` | `false` |
|
||
| `order_attr` | a decimal integer from −2 147 483 648 to 2 147 483 647 | no order |
|
||
| `gain_attr` | a decimal integer from 0 to 16 384 | 4096 |
|
||
| `pan_attr` | a decimal integer from −4096 to 4096 | 0 |
|
||
|
||
A **decimal integer** is `0`, or an optional `-` followed by 1 to 10 of the digits `0`–`9`
|
||
of which the first is not `0`: no `+`, no space, no `-0`. Every attribute the descriptor
|
||
names is read for every participant, whether or not it has the source the attribute is
|
||
about; one whose value is not of its row is `attr_malformed`, and the composition at *t*
|
||
is refused, at `set`. The keys `featured`, `order` and `pip` that live-rooms §4.4 names
|
||
are these attributes under the keys a descriptor gives them; `gain` and `pan` are the
|
||
mix's.
|
||
|
||
### 4.4 Order
|
||
|
||
The participants are ordered: those with an order first, by order ascending, then those
|
||
with none; between two of one order (or none), by join order ascending; and between two
|
||
of one join order, by key, as unsigned bytes. This order is **S**.
|
||
|
||
### 4.5 The set's digest
|
||
|
||
The digest of the participant set at *t* is SHA-256 over the 21 bytes
|
||
`wai:live-compose-set` (ASCII) and 0x01, the room id, *t* (u64), the number of
|
||
participants (u32), and then, for each participant in ascending order of key:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `principal` | 32 | its key |
|
||
| `grant` | 32 | the digest of its grant in effect |
|
||
| `join` | 4 | u32: its join order |
|
||
| `video`, `audio` | 1 or 9 + *s* each | its source: one byte 0 for none; otherwise `media` (u8, the media kind of live-rooms §6.1), `slot` (short: empty for an own track), `profile` (u8, the profile's index in the room descriptor), `layers` (u8), the composed layer *L* (u8), `width` and `height` (u16 each) |
|
||
|
||
The grant's digest fixes its attributes, so the set's digest fixes everything §5 and §6
|
||
read of the room at *t*.
|
||
|
||
## 5. The layout
|
||
|
||
### 5.1 Integers
|
||
|
||
Every quantity of this section is an integer, and none is negative. ⌊*a* / *b*⌋, for *b*
|
||
above 0, rounds down. **fit**(*bw*, *bh*, *a*, *b*), the largest rectangle of aspect
|
||
*a*:*b* within *bw* × *bh*, is (*bw*, max(1, ⌊*bw* × *b* / *a*⌋)) when *bw* × *b* ≤ *bh*
|
||
× *a*, and (max(1, ⌊*bh* × *a* / *b*⌋), *bh*) otherwise. A rectangle of *w* × *h*
|
||
**centred** in (*x*, *y*, *W*, *H*) is (*x* + ⌊(*W* − *w*) / 2⌋, *y* + ⌊(*H* − *h*) / 2⌋,
|
||
*w*, *h*). No product here exceeds 2^63.
|
||
|
||
### 5.2 Which participants have tiles
|
||
|
||
From S (§4.4):
|
||
|
||
1. **V** is S, less every participant whose video is stopped when `omit_stopped` is 1.
|
||
2. For a picture in picture, the **PiP participant** P is the first of V whose `pip_attr`
|
||
attribute is `true`; with none there is no PiP window. For a grid there is none.
|
||
3. **G** is V without P, in order. The **featured participant** F is the first of G whose
|
||
`featured_attr` attribute is `true`; it is moved to the front of G.
|
||
4. G is cut to its first `max_tiles` members, less one when there is a P. F, being first,
|
||
is kept; when G is then empty there is no F.
|
||
|
||
### 5.3 The grid
|
||
|
||
A grid of *n* ≥ 1 tiles over a region (*X*, *Y*, *W*, *H*) is chosen by its **reference
|
||
aspect** *a*:*b*: 16:9 for auto and video, 1:1 for square, 3:4 for portrait.
|
||
|
||
- **Candidates.** For each number of columns *c* from 1 to *n*, with *r* = ⌈*n* / *c*⌉
|
||
rows: *c* is a candidate when (*c* − 1) × `gap` is below *W*, (*r* − 1) × `gap` below
|
||
*H*, and the cells, *cw* = ⌊(*W* − (*c* − 1) × `gap`) / *c*⌋ wide and *ch* = ⌊(*H* −
|
||
(*r* − 1) × `gap`) / *r*⌋ high, are each at least 1. Its **area** is *tw* × *th*, where
|
||
(*tw*, *th*) = fit(*cw*, *ch*, *a*, *b*).
|
||
- **The choice.** The candidate of the largest area; of several, the one with the most
|
||
columns. §3.3's rule on `gap` makes *c* = ⌈√*n*⌉ a candidate for every *n* a
|
||
descriptor allows.
|
||
- **Placement.** The rows are centred: the first row's top is *y*0 = *Y* + ⌊(*H* − (*r* ×
|
||
*ch* + (*r* − 1) × `gap`)) / 2⌋. Tile *k*, from 0, is in row *i* = ⌊*k* / *c*⌋ and column
|
||
*j* = *k* mod *c*. Every row holds *c* tiles but the last, which holds *n* − (*r* − 1) ×
|
||
*c*; a row of *m* tiles is centred, beginning at *x*0 = *X* + ⌊(*W* − (*m* × *cw* + (*m* −
|
||
1) × `gap`)) / 2⌋. Cell *k* is (*x*0 + *j* × (*cw* + `gap`), *y*0 + *i* × (*ch* + `gap`),
|
||
*cw*, *ch*).
|
||
- **The tile.** Under auto, tile *k* is its cell; otherwise it is fit(*cw*, *ch*, *a*, *b*)
|
||
centred in its cell.
|
||
|
||
With no tile the canvas is its background.
|
||
|
||
### 5.4 The featured tile
|
||
|
||
With F and *n* ≥ 2 members of G, the **featured tile** is (0, 0, `width`, *mh*), with *mh*
|
||
= ⌊(`height` − `gap`) × 3 / 4⌋, and the other *n* − 1 members are a grid (§5.3) over the
|
||
band below it, (0, *mh* + `gap`, `width`, `height` − *mh* − `gap`). The featured tile is its
|
||
whole region: `aspect` is the grid tiles'. With F alone the featured tile is the whole
|
||
canvas. With no F, the members of G are a grid over the whole canvas.
|
||
|
||
### 5.5 Picture in picture
|
||
|
||
With P and a G that is not empty, G is laid out over the canvas as §5.3 and §5.4 lay it out,
|
||
and P's **window** is drawn over it. With P and an empty G:
|
||
|
||
- **static**: P's window, and the background;
|
||
- **dynamic**: P is laid out as though it had no `pip_attr` attribute: a grid of P alone,
|
||
or, when P is featured, the featured tile, the whole canvas.
|
||
|
||
The window is `pip_width` × `pip_height` when both are above 0. Otherwise, with *a*:*b* the
|
||
width and height of P's video source, or the canvas's when P's video is stopped, it is
|
||
fit(*bw*, *bh*, *a*, *b*), where *bw* is `pip_width` when that is above 0, `width` −
|
||
`offset` when only `pip_height` is, and the lower of ⌊`width` / 4⌋ and `width` − `offset`
|
||
when neither is; and *bh* is `pip_height` when that is above 0 and `height` − `offset`
|
||
otherwise. With *o* the `offset` and *pw* × *ph* the window, it is at (*o*, *o*) in the top
|
||
left corner, (`width` − *o* − *pw*, *o*) in the top right, (*o*, `height` − *o* − *ph*) in
|
||
the bottom left, and (`width` − *o* − *pw*, `height` − *o* − *ph*) in the bottom right.
|
||
|
||
### 5.6 Fill
|
||
|
||
A tile (*tx*, *ty*, *tw*, *th*) shows a source of *sw* × *sh* by its fill mode. The source
|
||
is **wider** when *sw* × *th* ≥ *sh* × *tw*.
|
||
|
||
- **Fill**: the whole source, (0, 0, *sw*, *sh*), stretched over the whole tile.
|
||
- **Cover**: the source cropped to the tile's aspect, over the whole tile. A wider source's
|
||
crop is *cw* = max(1, ⌊*sh* × *tw* / *th*⌋) wide, at (⌊(*sw* − *cw*) / 2⌋, 0, *cw*, *sh*);
|
||
any other's is *ch* = max(1, ⌊*sw* × *th* / *tw*⌋) high, at (0, ⌊(*sh* − *ch*) / 2⌋, *sw*,
|
||
*ch*).
|
||
- **Contain**: the whole source, letterboxed in the tile. A wider source is drawn into
|
||
(*tx*, *ty* + ⌊(*th* − *dh*) / 2⌋, *tw*, *dh*) with *dh* = max(1, ⌊*tw* × *sh* / *sw*⌋);
|
||
any other into (*tx* + ⌊(*tw* − *dw*) / 2⌋, *ty*, *dw*, *th*) with *dw* = max(1, ⌊*th* ×
|
||
*sw* / *sh*⌋).
|
||
- **Default**: cover for a grid tile and the PiP window, contain for the featured tile.
|
||
|
||
Each mode gives a **source rectangle** and a **destination rectangle**: the part of the
|
||
source drawn, and where on the canvas it is drawn. A tile of stopped video has neither, and
|
||
shows the background, as does the part of a tile outside its destination. How the source
|
||
rectangle is scaled into the destination is the renderer's; a sink that holds a source at a
|
||
layer below its composed one scales it to the composed layer's width and height first.
|
||
|
||
### 5.7 What the layout gives
|
||
|
||
The layout of a descriptor and a participant set gives:
|
||
|
||
- **the order**: S, every participant, by key;
|
||
- **the tiles**, in drawing order: the featured tile, then the grid's in G's order, then
|
||
the PiP window. Each has its participant, its **place** (`grid`, `featured` or `pip`),
|
||
its **z** (0, or 1 for the PiP window, drawn over the others), the tile, and, unless its
|
||
video is stopped, its source and destination rectangles;
|
||
- **the voices** (§6).
|
||
|
||
A renderer paints the background over the canvas, then the tiles in order. No two tiles
|
||
of z 0 overlap.
|
||
|
||
### 5.8 Re-layout
|
||
|
||
The participant set changes only at the `t` of a lifecycle event: a `join`, `leave`,
|
||
`publish`, `unpublish`, `grant_exchange`, `takeover` or `failover` (an exchange always
|
||
changes it, since the set names each participant's grant). At every such instant at or
|
||
after `effective` the layout is computed again, and it changes when the sources, the
|
||
attributes or the order do; between two such instants it does not change. The layout at *t*
|
||
is the function of this section over the descriptor and the set at *t*, and of nothing a
|
||
sink sees otherwise, so a sink that joins late lays the room out as one that was there
|
||
throughout, and a grant exchange that changes an attribute re-lays the room out at the
|
||
exchange's tick, with no participant leaving.
|
||
|
||
## 6. The audio mix
|
||
|
||
The **voices** are the members of S (before any is omitted for its video) that have an
|
||
audio source, in S's order, the first `max_voices` of them. Each has its **gain** *g*,
|
||
from 0 to 16 384 in units of 1/4096 (4096 is unity), and its **pan** *p*, from −4096
|
||
(left) to 4096 (right) (§4.3).
|
||
|
||
The mixer mixes one block of *N* frames. Each voice's input is its decoded audio for the
|
||
block: *N* frames of signed 16-bit samples, of 1 or 2 channels (interleaved, left first),
|
||
at the descriptor's `sample_rate`. Each output sample is a sum over the voices in signed
|
||
64-bit integers, one term per voice:
|
||
|
||
- **mono output** (`channels` 1): *x* × 4096 × *g* for a mono voice's sample *x*; (*x*L +
|
||
*x*R) × 2048 × *g* for a stereo voice's samples *x*L and *x*R.
|
||
- **stereo output** (`channels` 2), with *a*L = 4096 − max(*p*, 0) and *a*R = 4096 +
|
||
min(*p*, 0): to the left channel *x* × *g* × *a*L and to the right *x* × *g* × *a*R for
|
||
a mono voice; *x*L × *g* × *a*L and *x*R × *g* × *a*R for a stereo one.
|
||
|
||
The output sample is ⌊(*s* + 2^23) / 2^24⌋ of the sum *s*, the arithmetic shift right by
|
||
24 bits of *s* + 2^23, which rounds half up; then saturated once to −32 768 … 32 767. A
|
||
term is at most 2^41 in magnitude and a mix has at most 64 voices, so the sum is exact,
|
||
the order of its terms changes nothing, and saturation applies to the whole once. The pan
|
||
is a balance: at the centre both channels are at the voice's gain, and panning attenuates
|
||
one channel only. A single voice at unity gain and the centre is passed through
|
||
unchanged. A mixer that holds no decoded block of a voice for the output mixes it as
|
||
silence.
|
||
|
||
A mixer is refused as `mix_input_malformed` when an output has other than 1 or 2
|
||
channels, or an input has other than 1 or 2 channels, has other than *N* frames, or a
|
||
gain or pan out of range.
|
||
|
||
The mix is byte-exact over the decoded PCM: two mixers given the same blocks give the
|
||
same samples. It is not byte-exact over what precedes and follows it. A decoder of a lossy
|
||
codec need not reproduce another decoder's PCM bit for bit (RFC 6716 §6 holds a decoder to
|
||
a quality measure, not to equality), and a lossy encode of the mix is not
|
||
byte-reproducible: two encoders, or one encoder under two settings, give different bytes.
|
||
The receipt (§8) therefore binds the input objects and the output object, not samples.
|
||
|
||
## 7. Where composition runs
|
||
|
||
**On each sink, by default.** A sink composes the room locally from the tracks it
|
||
subscribes to, by §5 and §6, with no server decode or encode. It needs no power: the
|
||
descriptor states how the room is laid out, and laying it out is reading it.
|
||
|
||
**On a server, under the compose power.** A principal that publishes a composed track (its
|
||
own `video` track for a layout, its own `audio` track for a mix, live-rooms §6.1) **MUST**
|
||
be the descriptor's `compositor`, **MUST** hold the compose power in its grant in effect,
|
||
and **MUST** sign a receipt (§8) for each object of a composed track it publishes. It is
|
||
not a participant of its own composition (§4.2), so a composed track is never composed
|
||
again. A subscriber subscribes to a composed track as to any other; the descriptor and the
|
||
receipts let it check that the track is the room's composition.
|
||
|
||
## 8. The composition receipt
|
||
|
||
### 8.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCR1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `compositor` | 32 | the compositor's key, which signs the receipt |
|
||
| `descriptor` | 32 | the digest of the descriptor it follows (§3.2) |
|
||
| `t` | 8 | u64: the room-clock instant of the output object |
|
||
| `state` | 32 | the digest of the participant set at `t` (§4.5) |
|
||
| `media` | 1 | 1 audio (a mix), 2 video (a layout) |
|
||
| `output` | 80 | the output object |
|
||
| `inputs` | 1 + 112 × *n* | a u8 count *n*, then each input: `principal` (32), the participant it is read from, and an object (80) |
|
||
| `sig` | 64 | Ed25519 by `compositor` |
|
||
|
||
An **object** is 80 bytes: `track` (32), the digest live-events §3.1 gives an anchor's
|
||
`track`, over the track's namespace as live-rooms §6.2 renders it, one zero byte, and its
|
||
name; `group` and `object` (u64 each), its MOQT Location (MOQT §8.2); and `digest` (32),
|
||
the SHA-256 of the object's payload. An input whose `digest` is 32 zero bytes states that
|
||
the compositor held no object of that track for the output: it drew the tile with no
|
||
video, or mixed the voice as silence.
|
||
|
||
The **digest** of a receipt is SHA-256 over its bytes before `sig`; its **signature** is
|
||
Ed25519 by `compositor` over the 25 bytes `wai:live-compose-receipt` (ASCII) and 0x01,
|
||
followed by the bytes the digest covers, verified strictly (SPEC §7 *Signatures*). A
|
||
reader reads exactly the bytes it is given, as §3.3 steps 1 to 3 read a descriptor with
|
||
`WCR1` for `WCD1` (`truncated`, `malformed`, `unknown_version`); refuses as
|
||
`receipt_invalid` a receipt whose `media` is not 1 or 2, or that names two inputs of one
|
||
participant and one track; and then verifies the signature (`bad_key`, `bad_signature`).
|
||
|
||
### 8.2 The inputs
|
||
|
||
The inputs of an output at *t* are, in this order:
|
||
|
||
- **video** (`media` 2): for each tile of the layout at *t* that shows video, in drawing
|
||
order (§5.7), the tracks of its participant's video source;
|
||
- **audio** (`media` 1): for each voice, in order (§6), the tracks of its audio source.
|
||
|
||
The **tracks** of a source are named as live-rooms §6.1 names them: the namespace is the
|
||
room, the owner (the participant's key, or `_` and the slot's name for a slot) and the
|
||
media kind; the names follow the profile's `layering` (live-rooms §6.3) at the composed
|
||
layer *L*: track `0` for `single` and `svc` (an `svc` track carries every layer), track
|
||
*L* for `simulcast`, and tracks `0` to *L* for `staged`, since a refinement is applied to
|
||
the layers below it ([staged-delivery](staged-delivery.md)). The receipt names, for each,
|
||
the object it read.
|
||
|
||
### 8.3 Verifying a receipt
|
||
|
||
A receipt verifier is given a room (as live-rooms §9.1), a descriptor and a receipt. In
|
||
this order, the first that fails names the refusal and where:
|
||
|
||
1. **The room** (§4.1): its refusal, at `room`.
|
||
2. **The descriptor** (§3.3, §3.5): at `descriptor`.
|
||
3. **The receipt** reads (§8.1), at `receipt`; and its `room` is the room id, otherwise
|
||
`wrong_room`.
|
||
4. Its `descriptor` is the descriptor's digest: otherwise `descriptor_mismatch`.
|
||
5. Its `t` is at or after the descriptor's `effective`: otherwise `not_yet_effective`.
|
||
6. The descriptor names a compositor, the receipt's `compositor` is it, and that principal
|
||
is present at `t` with a grant in effect that has the compose power: otherwise
|
||
`compositor_not_permitted`.
|
||
7. Its `output`'s `track` is a track of the compositor's own track of the receipt's media
|
||
kind, published at `t`, at a layer from 0 to its `layers` − 1: otherwise
|
||
`output_not_published`.
|
||
8. **The set and the layout** at `t` (§4, §5): their refusal, at `set`.
|
||
9. Its `state` is the set's digest: otherwise `state_mismatch`.
|
||
10. Its inputs, each its `principal` and its object's `track`, are §8.2's, in order:
|
||
otherwise `inputs_mismatch`.
|
||
|
||
A receipt that passes is **accepted**: its output object was composed under this
|
||
descriptor, from this set, out of objects of exactly the tracks the layout reads. Anyone
|
||
holding the room's events and the descriptor recomputes the layout and checks that the
|
||
inputs were the right ones; a party holding the objects checks their digests. The receipt
|
||
does not fix the output's pixels or samples (§6).
|
||
|
||
## 9. Verification
|
||
|
||
A **composition verifier** is given a room, a descriptor and a list of instants. It runs
|
||
these steps and stops at the first refusal, which it reports with the reason and where:
|
||
|
||
1. **The room** (§4.1): at `room`, with the room's own place (live-rooms §9.1) when the
|
||
room is refused.
|
||
2. **The descriptor** (§3.3, §3.5): at `descriptor`.
|
||
3. **Each instant**, in the list's order: the instant is at or after `effective`
|
||
(otherwise `not_yet_effective`), and the participants' attributes read (§4.3,
|
||
otherwise `attr_malformed`): at `set`, with the instant.
|
||
|
||
Its outcome is **refuse**, with the reason and the place; or the **composition**: the
|
||
descriptor's digest and, for each instant, the participants with their sources and
|
||
tracks, the set's digest, the order, the tiles and the voices. A receipt verifier (§8.3)
|
||
reports its refusals at the same places, and at `receipt`.
|
||
|
||
## 10. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `canvas_out_of_range` | §3.3, §3.4 |
|
||
| `compose_invalid` | §3.3 |
|
||
| `compose_not_permitted` | §3.5 |
|
||
| `not_yet_effective` | §8.3 step 5, §9 step 3 |
|
||
| `lifecycle_incomplete` | §4.1 |
|
||
| `attr_malformed` | §4.3 |
|
||
| `mix_input_malformed` | §6 |
|
||
| `receipt_invalid` | §8.1 |
|
||
| `descriptor_mismatch` | §8.3 step 4 |
|
||
| `compositor_not_permitted` | §7, §8.3 step 6 |
|
||
| `output_not_published` | §8.3 step 7 |
|
||
| `state_mismatch` | §8.3 step 9 |
|
||
| `inputs_mismatch` | §8.3 step 10 |
|
||
|
||
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version`
|
||
for a descriptor or a receipt that does not read; `bad_key` and `bad_signature` for their
|
||
signatures; `wrong_room` for one of another room (§3.5, §8.3); and every code a room
|
||
verifier raises (live-rooms §11 and the live-events codes it lists), at `room`.
|
||
|
||
## 11. Conformance
|
||
|
||
[`live-conformance/compose/`](../live-conformance/compose/) holds the corpus, in one file,
|
||
`vectors.json`, written from fixed seeds by `wai_live_compose_vectors gen`. It has:
|
||
|
||
- **layouts**: the layout function alone (§5, §6's voices), each case its parameters and
|
||
its participants with their join orders, video sizes, audio and attributes, and the
|
||
order, tiles and voices it gives, or its refusal. Grids of 0 to 12 tiles on a 16:9
|
||
canvas, and on portrait, square and auto ones; every fill mode over sources of four
|
||
shapes and of extreme aspects; a featured participant present, absent, kept past the
|
||
tile limit, and first in order of two; ties in order broken by join order and by key;
|
||
stopped video shown and omitted; picture in picture in every corner under both
|
||
behaviours, alone static and alone dynamic, with derived sizes and stopped video; and
|
||
attributes that do not read.
|
||
- **mixes**: blocks of PCM and the mix of them (§6): passing through, both widths into
|
||
both, saturation high and low and only at the end, pans and gains at their limits,
|
||
rounding at ties, silence, and inputs refused.
|
||
- **descriptors**: records read (§3), with their digest and fields, or refused with each
|
||
reason a descriptor alone can have.
|
||
- **rooms**: a studio room under the live-rooms descriptor, grants and chains that its
|
||
vectors name, and four variants of it: a grant whose order does not read, a lifecycle
|
||
record not delivered, a viewer's chat not delivered, and a viewer's record refused.
|
||
- **compositions**: a room, a descriptor and instants (§9), with the composition or the
|
||
refusal. The grid over the whole session re-lays the room out at each lifecycle
|
||
instant, among them a grant exchange that reorders it; the picture in picture composes
|
||
a lower layer.
|
||
- **receipts**: a room, a descriptor and a receipt (§8.3), accepted with its digest and
|
||
the set's, or refused with each reason a receipt can have.
|
||
|
||
A conforming verifier gives every case the outcome, the reason, the place and the data the
|
||
corpus states. `wai_live_compose_vectors verify` and the test `tests/live_compose.rs`
|
||
check that the file regenerates byte for byte, that `wai-rs` gives every case its
|
||
outcome, that every reason of §10 refuses a case, and that §10 lists exactly the
|
||
reference's codes. `live-conformance/live_compose_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`), verifies each room
|
||
with the live-rooms corpus's own second implementation (`live_rooms_verify.py`) and
|
||
replays its lifecycle to each instant itself, and gives every case the same outcome,
|
||
rectangle for rectangle and sample for sample; it also mutates copies of the corpus and
|
||
requires every copy to fail.
|
||
|
||
## 12. Security and privacy considerations
|
||
|
||
- The layout is computed from what the room authority signed: the descriptor, or a
|
||
principal's under the power the authority granted, and the attributes of grants the
|
||
authority signed. A participant cannot place itself; a relay that drops or reorders
|
||
lifecycle events is seen as a gap in a room key's chain (§4.1), not as another layout.
|
||
- A server compositor's receipt fixes which objects it composed and under which set. A
|
||
compositor that left a participant out, read a layer other than the composed one, or
|
||
composed a stale set is refused by §8.3. The receipt does not fix the output's pixels
|
||
or samples, which a lossy encode does not reproduce (§6); a party that needs them
|
||
composes locally from the inputs.
|
||
- Composition on each sink needs no server to decode the room's media, so a room whose
|
||
tracks are end-to-end encrypted ([confidentiality](confidentiality.md)) can still be
|
||
laid out: the layout reads the room's events and the grants, never media.
|
||
- The participant set names every publisher, its grant and its join order. Composing a
|
||
room reveals nothing a member of it does not already hold, but a set digest published
|
||
outside the room links the participants it names; a room that needs participation
|
||
unlinkable across rooms gives principals a key per room (live-events §11).
|
||
- Attributes are read as the descriptor's keys name them and nothing else is read of a
|
||
grant's attributes, so an attribute a descriptor does not name changes nothing.
|