Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ test:
python3 sh/gh/gst-formula.test.py
python3 sh/changed.test.py
python3 sh/large-files.test.py
python3 sh/text.test.py
python3 sh/dispatch-query.test.py
python3 sh/dispatch-query.test.py --bench
python3 sh/gh/test-inventory.test.py
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/pullfrog.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED
# PULLFROG ACTION - DO NOT EDIT EXCEPT WHERE INDICATED
name: Pullfrog
run-name: ${{ inputs.name || github.workflow }}
on:
Expand Down Expand Up @@ -58,15 +58,15 @@ jobs:
# AZURE_RESOURCE_NAME: my-resource
# AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }}
# AZURE_DEPLOYMENT: <deployment-name>
# both limits are required — set them to the real limits of that model
# both limits are required - set them to the real limits of that model
# AZURE_CONTEXT: "400000"
# AZURE_MAX_OUTPUT: "128000"

# for any OpenAI-compatible endpoint — LiteLLM, Cloudflare AI Gateway,
# for any OpenAI-compatible endpoint - LiteLLM, Cloudflare AI Gateway,
# self-hosted vLLM (https://docs.pullfrog.com/openai-compatible)
# OPENAI_COMPATIBLE_BASE_URL: https://litellm.example.com/v1
# OPENAI_COMPATIBLE_API_KEY: ${{ secrets.OPENAI_COMPATIBLE_API_KEY }}
# OPENAI_COMPATIBLE_MODEL: <model-id>
# both limits are required — set them to the real limits of that model
# both limits are required - set them to the real limits of that model
# OPENAI_COMPATIBLE_CONTEXT: "128000"
# OPENAI_COMPATIBLE_MAX_OUTPUT: "16384"
2 changes: 1 addition & 1 deletion .github/workflows/release-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: Release Go
# (registry is the gate, like release-plz / the PyPI check in release.sh).
#
# Triggers:
# * push to release — a wrapper API or VERSION change. The ffi require is
# * push to release - a wrapper API or VERSION change. The ffi require is
# pinned to the current crate version, whose mirror tag
# already exists in steady state. If it's missing (a
# release is mid-flight) or rs/moq-ffi changed since
Expand Down
2 changes: 1 addition & 1 deletion demo/boy/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Cloudflare Worker for rom.moq.dev — serves Game Boy ROMs from R2.
// Cloudflare Worker for rom.moq.dev - serves Game Boy ROMs from R2.

interface Env {
ROM: R2Bucket;
Expand Down
4 changes: 2 additions & 2 deletions demo/web/src/viz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function graph(parent: Signals.Effect, title: string, opts?: GraphOptions
const value = document.createElement("span");
value.className = "font-mono";
value.style.color = color;
value.textContent = "—";
value.textContent = "-";
header.append(label, value);

const canvas = document.createElement("canvas");
Expand Down Expand Up @@ -134,7 +134,7 @@ export function graph(parent: Signals.Effect, title: string, opts?: GraphOptions
const push = (v: number | undefined) => {
samples.push(v !== undefined && Number.isFinite(v) ? Math.max(0, v) : 0);
while (samples.length > capacity) samples.shift();
value.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : "—";
value.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : "-";
draw();
};

Expand Down
6 changes: 3 additions & 3 deletions drafts/draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ A subscriber opens a Fetch Stream (0x3) to request a single Group from a Track.

The subscriber sends a FETCH message containing the broadcast path, track name, priority, group sequence, and the frame range within that group.
Unlike SUBSCRIBE, FETCH works on both live and ended broadcasts; it is the only way to read an ended one.
The publisher responds with FRAME messages directly on the same bidirectional stream — there is no response header.
The publisher responds with FRAME messages directly on the same bidirectional stream - there is no response header.
The Group Sequence and index of the first returned frame are implicit, taken from the original FETCH request.
Because there is no response header, a publisher that cannot serve the requested frame range in full MUST reset the stream rather than return a shorter run; the subscriber has no way to learn where a truncated response actually started.
As with a subscription, the subscriber MUST already have the track's [TRACK_INFO](#track-info) to parse the returned frames; because the properties are immutable, a single Track Stream lookup is reused across every FETCH of that track (group-by-group fetches do not re-fetch it).
Expand Down Expand Up @@ -1619,10 +1619,10 @@ The last frame to return (inclusive), encoded as the absolute frame index + 1.
A value of 0 means through the end of the group (default).
A `Frame End` below `Frame Start` once decoded is a protocol violation; equal bounds are a legal single-frame range.

The publisher responds with FRAME messages directly on the same stream — there is no response header.
The publisher responds with FRAME messages directly on the same stream - there is no response header.
The subscriber parses them using the track's [TRACK_INFO](#track-info), which it MUST already have (see the [Track Stream](#track-stream)); the group sequence and the index of the first frame are implicit from the FETCH request.
The publisher FINs the stream after the last frame, or resets on error.
There is no FETCH_ERROR message — the publisher signals failure by resetting the stream.
There is no FETCH_ERROR message - the publisher signals failure by resetting the stream.
A publisher holding fewer frames than requested MUST reset rather than truncate, since a short response is indistinguishable from one that started elsewhere.
A group that ends before `Frame End` is not a truncation: the publisher FINs after the last frame it has, provided the group is complete and it served everything from `Frame Start` onward.

Expand Down
2 changes: 1 addition & 1 deletion drafts/draft-lcurley-moq-probe.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ This means the available bandwidth is often underestimated, and the subscriber h

This is particularly problematic for adaptive bitrate (ABR) algorithms.
A viewer may get stuck at a low quality rendition indefinitely because the congestion window never grows to reflect the true link capacity.
If the viewer does attempt to switch to a higher rendition without first probing, they risk buffering — either because the congestion window has not been warmed up to support the higher bitrate, or because the network genuinely cannot sustain it.
If the viewer does attempt to switch to a higher rendition without first probing, they risk buffering - either because the congestion window has not been warmed up to support the higher bitrate, or because the network genuinely cannot sustain it.
Without probing, the subscriber cannot distinguish between these two cases.

{{moqt}} Section 3.7.2 suggests subscribing to additional tracks at low priority or sending padding to fill the congestion window during probing intervals.
Expand Down
18 changes: 9 additions & 9 deletions js/hang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Core media library for [Media over QUIC](https://moq.dev/) (MoQ). Provides share

## Features

- **Catalog** — JSON track describing other tracks and their codec properties (audio, video, chat, location, etc.)
- **Container** — Media framing in two formats: CMAF (fMP4) and Legacy (varint-timestamp + raw codec bitstream)
- **Utilities** — Hex encoding, Opus and AAC audio polyfill ([`@moq/libav`](../libav)), latency computation, browser detection workarounds
- **Catalog** - JSON track describing other tracks and their codec properties (audio, video, chat, location, etc.)
- **Container** - Media framing in two formats: CMAF (fMP4) and Legacy (varint-timestamp + raw codec bitstream)
- **Utilities** - Hex encoding, Opus and AAC audio polyfill ([`@moq/libav`](../libav)), latency computation, browser detection workarounds

Browser support detection is provided by [`<moq-watch-support>`](../watch) and [`<moq-publish-support>`](../publish).

Expand All @@ -32,10 +32,10 @@ bun add @moq/hang
```typescript
import * as Hang from "@moq/hang";

// Catalog — describes tracks and their codec properties
// Catalog - describes tracks and their codec properties
import * as Catalog from "@moq/hang/catalog";

// Container — Legacy and LOC framing
// Container - Legacy and LOC framing
import * as Container from "@moq/hang/container";

// CMAF decoding is a separate synchronous module, loaded only when needed.
Expand All @@ -53,10 +53,10 @@ import * as Publish from "@moq/publish";

## Related Packages

- **[@moq/watch](../watch)** — Subscribe to and render MoQ broadcasts
- **[@moq/publish](../publish)** — Publish media to MoQ broadcasts
- **[@moq/net](../lite)** — Core pub/sub transport protocol
- **[@moq/signals](../signals)** — Reactive signals library
- **[@moq/watch](../watch)** - Subscribe to and render MoQ broadcasts
- **[@moq/publish](../publish)** - Publish media to MoQ broadcasts
- **[@moq/net](../lite)** - Core pub/sub transport protocol
- **[@moq/signals](../signals)** - Reactive signals library

## License

Expand Down
4 changes: 2 additions & 2 deletions js/hang/src/cmaf/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ function extractAudioSpecificConfig(esds: Uint8Array): Uint8Array | undefined {
}

if (tag === 0x05) {
// Found DecoderSpecificInfo — payload is the AudioSpecificConfig
// Found DecoderSpecificInfo - payload is the AudioSpecificConfig
if (offset + size <= esds.length) {
return new Uint8Array(esds.buffer, esds.byteOffset + offset, size);
}
Expand All @@ -302,7 +302,7 @@ function extractAudioSpecificConfig(esds: Uint8Array): Uint8Array | undefined {
} else if (tag === 0x04) {
offset += 13; // objectTypeIndication (1) + streamType (1) + bufferSizeDB (3) + maxBitrate (4) + avgBitrate (4)
} else {
// Unknown tag — skip its payload entirely
// Unknown tag - skip its payload entirely
offset += size;
}
}
Expand Down
2 changes: 1 addition & 1 deletion js/moq-boy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm add @moq/boy

## Web Components

### `<moq-boy>` — Game Grid
### `<moq-boy>` - Game Grid

Shows a grid of all active Game Boy sessions. Click a game to expand and play.

Expand Down
10 changes: 5 additions & 5 deletions js/moq-boy/src/element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ const DEFAULT_PREFIX = Moq.Path.from("boy");
const cleanup = new FinalizationRegistry<Moq.Signals.Effect>((signals) => signals.close());

/**
* `<moq-boy>` web component — discovers and manages Game Boy streaming sessions,
* `<moq-boy>` web component - discovers and manages Game Boy streaming sessions,
* and renders the interactive UI in a Shadow DOM.
*
* Attributes:
* - `url` — MoQ relay URL
* - `prefix` — Base path prefix (default: "boy"). Derives prefix-game and prefix-viewer.
* - `prefix-game` — Path prefix for game broadcasts (default: `{prefix}/game`)
* - `prefix-viewer` — Path prefix for viewer broadcasts (default: `{prefix}/viewer`)
* - `url` - MoQ relay URL
* - `prefix` - Base path prefix (default: "boy"). Derives prefix-game and prefix-viewer.
* - `prefix-game` - Path prefix for game broadcasts (default: `{prefix}/game`)
* - `prefix-viewer` - Path prefix for viewer broadcasts (default: `{prefix}/viewer`)
*/
export default class MoqBoy extends HTMLElement {
/** The attributes the browser reports changes to. */
Expand Down
2 changes: 1 addition & 1 deletion js/moq-boy/src/ui/components/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function GameCardInner() {
game.canvas.set(canvasRef);
});

// Keyboard input — preventDefault when expanded or hovered.
// Keyboard input - preventDefault when expanded or hovered.
const isActive = () => game.expanded.peek() === game.sessionId || game.hovered.peek();

const onKeyDown = (e: KeyboardEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions js/publish/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ The simplest way to publish a stream:
| `url` | string | required | Relay server URL |
| `name` | string | required | Broadcast name |
| `versions` | string | stable versions | Comma-separated protocol versions to offer, most preferred first, like `moq-transport-18,moq-lite-06` |
| `source` | string | — | `"camera"`, `"screen"`, `"file"` |
| `source` | string | - | `"camera"`, `"screen"`, `"file"` |
| `muted` | boolean | false | Stop publishing audio; the microphone stays open |
| `invisible` | boolean | false | Stop publishing video; the camera stays open |
| `preview` | string | `"source"` | What the preview renders: `"source"`, `"encoded"`, `"none"` |
| `backend` | string | `"auto"` | Graphics API for a `<canvas>` preview: `"auto"`, `"webgpu"`, `"2d"` |
| `announce` | string | `"source"` | When to advertise: `"always"` or `"source"` (once media is captured). A camera source waits for every enabled track, and `"source"` waits until each captured track's config resolves or fails, so the first catalog lists every rendition. After its first announcement, a camera stays announced until deselected or disconnected, including mute, hide, device swaps, and capture retries. The broadcast is created while connected either way. To keep it out of discovery, use a hidden name with a `.`-prefixed segment; it is still subscribable by name. |
| `stats` | boolean | false | Publish a `stats.json` track, named in the catalog, reporting what each rendition sent and the PROBE round-trip time |
| `echo-path` | string | — | Solicit viewer feedback under this path, relative to the broadcast (from `room/live`, `viewers` is `room/viewers`) |
| `echo-path` | string | - | Solicit viewer feedback under this path, relative to the broadcast (from `room/live`, `viewers` is `room/viewers`) |

Property and built-in UI changes reflect into attributes. Invalid URLs, version lists, and
source/preview/backend/announce modes are refused: their property setters throw, and malformed markup
Expand Down
18 changes: 9 additions & 9 deletions js/publish/src/ui/components/stats-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,27 @@ export function statsTab(parent: Effect, publish: MoqPublish): HTMLElement {
const display = capture ? effect.get(capture.out.display) : undefined;
const cfg = effect.get(publish.video.out.catalog);
videoCard.el.style.display = display ? "" : "none";
vRes.textContent = display ? `${display.width}×${display.height}` : "—";
vCodec.textContent = cfg?.codec ?? "—";
vRes.textContent = display ? `${display.width}×${display.height}` : "-";
vCodec.textContent = cfg?.codec ?? "-";
});

parent.run((effect) => {
const cfg = effect.get(publish.audio.out.catalog);
audioCard.el.style.display = cfg ? "" : "none";
if (!cfg) return;
aCodec.textContent = cfg.codec ?? "—";
aRate.textContent = cfg.sampleRate ? formatHz(cfg.sampleRate) : "—";
aChannels.textContent = cfg.numberOfChannels ? `${cfg.numberOfChannels}` : "—";
aBitrate.textContent = cfg.bitrate ? formatBitrate(cfg.bitrate) : "—";
aCodec.textContent = cfg.codec ?? "-";
aRate.textContent = cfg.sampleRate ? formatHz(cfg.sampleRate) : "-";
aChannels.textContent = cfg.numberOfChannels ? `${cfg.numberOfChannels}` : "-";
aBitrate.textContent = cfg.bitrate ? formatBitrate(cfg.bitrate) : "-";
});

parent.run((effect) => {
const url = effect.get(publish.connection.url);
const status = effect.get(publish.connection.status);
const name = effect.get(publish.broadcast.in.name);
nStatus.textContent = status;
nServer.textContent = url?.host ?? "—";
nName.textContent = name?.toString() || "—";
nServer.textContent = url?.host ?? "-";
nName.textContent = name?.toString() || "-";
});

// Live graphs: frame rate from captured frames, bitrate measured from the bytes we encoded.
Expand Down Expand Up @@ -123,7 +123,7 @@ export function statsTab(parent: Effect, publish: MoqPublish): HTMLElement {
vFpsGraph.push(elapsed > 0 && frameDelta > 0 ? frameDelta / (elapsed / 1000) : undefined);

const stats = publish.video.out.stats.peek();
vTarget.textContent = stats.bitrate !== undefined ? formatBitrate(stats.bitrate) : "—";
vTarget.textContent = stats.bitrate !== undefined ? formatBitrate(stats.bitrate) : "-";
vSkipped.textContent = `${stats.skipped}`;

const bytes = encoded();
Expand Down
10 changes: 5 additions & 5 deletions js/qmux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ throws `NotSupportedError`.
### Servers

`Session.accept` runs the other role, over a WebSocket your server has already accepted. The host
performs the upgrade, so it — not this library — chooses the subprotocol; `selectSubprotocol` picks
performs the upgrade, so it - not this library - chooses the subprotocol; `selectSubprotocol` picks
the value to accept from what the client offered, and the session reads the wire-format version back
off the socket.

Expand All @@ -62,7 +62,7 @@ Deno.serve((req) => {
})
```

Any already-accepted `WebSocket` works — `Deno.upgradeWebSocket`, Node's `ws`, or a
Any already-accepted `WebSocket` works - `Deno.upgradeWebSocket`, Node's `ws`, or a
`WebSocketStream`. The session refuses missing or unsupported subprotocols. Reject the upgrade
when `selectSubprotocol` returns `undefined`. If your host doesn't expose the
negotiated value on the socket, pass it explicitly as `{ protocol }`.
Expand All @@ -76,8 +76,8 @@ the stream-id space each side owns.

- **Fulfills** with `{ closeCode, reason }` on a graceful end: either side called `close()`, which
puts an `APPLICATION_CLOSE` (`0x1d`) on the wire.
- **Rejects** on an abnormal end: a `CONNECTION_CLOSE` (`0x1c`) arrived — the transport variant a
peer sends when *it* caught a protocol violation — or no close frame arrived at all, because the
- **Rejects** on an abnormal end: a `CONNECTION_CLOSE` (`0x1c`) arrived - the transport variant a
peer sends when *it* caught a protocol violation - or no close frame arrived at all, because the
socket dropped or the peer went idle. This endpoint rejects the same way when it catches the peer
violating the protocol, and sends a `CONNECTION_CLOSE` so the peer rejects too.

Expand All @@ -91,7 +91,7 @@ try {
}
```

Don't reach for `closeCode` to tell the two apart — close codes are application-defined, so a
Don't reach for `closeCode` to tell the two apart - close codes are application-defined, so a
graceful close may carry any value, including one that looks like a violation, and an app closing
with `1006` is indistinguishable from a dropped socket. Which frame arrived is the signal, and the
settled state is how you read it.
Expand Down
2 changes: 1 addition & 1 deletion js/qmux/src/credit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
*
* Replenishes only when the remaining advertised window has dropped to at most
* half, and always sets the new limit relative to bytes the application has
* actually **consumed** (`consumed` is cumulative) — never relative to bytes
* actually **consumed** (`consumed` is cumulative) - never relative to bytes
* received. That keeps the peer's send credit bounded to one `window` ahead of
* the reader: a stalled reader stops advancing `consumed`, so the limit stops
* moving and the peer's credit drains.
Expand Down
4 changes: 2 additions & 2 deletions js/qmux/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* WebTransport spec requires `WebTransport.closed` to reject with.
*
* The native `WebTransportError` constructor only exists where the browser
* already implements WebTransport — which is precisely where this polyfill is
* not used — so mint our own rather than feature-detect a global that will not
* already implements WebTransport - which is precisely where this polyfill is
* not used - so mint our own rather than feature-detect a global that will not
* be there. `name`, `source`, and `streamErrorCode` match the interface, so a
* consumer branching on `err.source === "session"` works against the native API
* and this polyfill alike; `cause` carries the underlying failure (idle timeout,
Expand Down
Loading
Loading