Skip to content

Commit ab563be

Browse files
kixelatedclaude
andcommitted
Merge remote-tracking branch 'origin/dev' into claude/loving-dubinsky-32ccb0
Brings the branch up to date with dev (HEVC #1802, native H.264 decode #1796, async device capture #1807, srt/rtmp/rtc egress) and keeps this PR's deltas: NVENC/VAAPI always-on for Linux, openh264 always-on (no `software` feature), publish=false removed, the nvidia-video-codec-sdk fork repinned to the hardened dynamic-loading commit (default-features off), and the NVIDIA-probe fallback. Note: restores dev's H.265/HEVC codec-aware encode path (Codec enum, codec-aware backend selection, hev1 producer) that the previous branch merge had dropped by taking "ours" wholesale. Verified: clippy -D clean, 19 moq-video tests pass (incl. H.265 roundtrips), sort/taplo/fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents 35c7452 + aa8bb7a commit ab563be

63 files changed

Lines changed: 3994 additions & 1247 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/bin/cli.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,18 @@ moq-cli publish --url https://relay.example.com --broadcast cam.hang \
9191
# One medium only:
9292
moq-cli publish --url https://relay.example.com --broadcast cam.hang capture --no-audio
9393
moq-cli publish --url https://relay.example.com --broadcast cam.hang capture --no-video
94+
95+
# Pick a codec (default h264). h265 is hardware-only:
96+
moq-cli publish --url https://relay.example.com --broadcast cam.hang capture --codec h265
9497
```
9598

9699
Video capture uses a native per-platform backend (AVFoundation on macOS, V4L2 on
97-
Linux, Media Foundation on Windows) and a hardware H.264 encoder (VideoToolbox on
98-
macOS, NVENC on Linux NVIDIA, VAAPI on Linux Intel/AMD). There is no software
99-
encoder in the CLI build, so a machine with no usable hardware encoder errors
100-
rather than falling back. `--hardware` turns a missing encoder into an error
101-
instead of trying the next candidate. `--camera` takes a bare integer as a device index, otherwise a
100+
Linux, Media Foundation on Windows). The codec is chosen with `--codec`
101+
(`h264` default, or `h265`). For H.264 it picks a hardware encoder
102+
(VideoToolbox on macOS, NVENC on Linux NVIDIA, VAAPI on Linux Intel/AMD) when one
103+
is present, falling back to the built-in software encoder (openh264); force either
104+
with `--hardware` / `--software`. H.265 is hardware-only (VideoToolbox on macOS,
105+
Media Foundation on Windows). `--camera` takes a bare integer as a device index, otherwise a
102106
device path (Linux) or name (a friendly-name substring on Windows, the
103107
AVFoundation `uniqueID` on macOS). Audio capture uses cpal (CoreAudio / WASAPI /
104108
ALSA) and encodes Opus.

doc/bin/rtc.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ peers see a black screen until the next natural keyframe arrives.
3131
`KeyframeRequest` events from the peer are logged but not propagated
3232
upstream; PLI-to-MoQ back-pressure is a future enhancement.
3333

34-
AV1 / H.265 aren't in str0m 0.19's codec enum, so they're not negotiated;
35-
this is tracked as a follow-up. Use H.264 or VP9 for now.
34+
The egress paths (WHEP server, WHIP client) negotiate H.264, H.265, VP8,
35+
VP9, AV1, and Opus. The ingest paths (WHIP server, WHEP client) currently
36+
accept H.264, VP8, VP9, and Opus; H.265 / AV1 ingest is a follow-up.
3637

3738
## CLI shape
3839

@@ -78,16 +79,26 @@ moq-rtc --relay https://relay.example.com --broadcast my-stream \
7879

7980
## Codec mapping
8081

81-
| WebRTC codec | MoQ catalog |
82-
|--------------|-------------|
83-
| Opus | `AudioCodec::Opus`, 48 kHz / stereo |
84-
| H.264 | `H264 { inline: true }` (Annex-B in catalog, no `avcC`) |
85-
| VP8 | `VideoCodec::VP8` |
86-
| VP9 | `VideoCodec::VP9` |
87-
88-
H.264 input is reassembled by str0m as Annex-B; `moq-mux`'s H.264 importer
89-
in `Avc3` mode publishes the inline-parameter shape directly, which lines
90-
up with what the WebCodecs decoder in `@moq/watch` already expects. No
82+
| WebRTC codec | MoQ catalog | Egress | Ingest |
83+
|--------------|-------------|--------|--------|
84+
| Opus | `AudioCodec::Opus`, 48 kHz / stereo | yes | yes |
85+
| H.264 | `VideoCodec::H264` (avc3 inline or avc1 + `avcC`) | yes | yes (avc3) |
86+
| H.265 | `VideoCodec::H265` (hev1 inline or hvc1 + `hvcC`) | yes | no |
87+
| VP8 | `VideoCodec::VP8` | yes | yes |
88+
| VP9 | `VideoCodec::VP9` | yes | yes |
89+
| AV1 | `VideoCodec::AV1` | yes | no |
90+
91+
On egress, `codec::Track` reshapes each rendition into what str0m's Frame
92+
API expects. Opus / VP8 / VP9 / AV1 and inline-parameter H.264 (avc3) /
93+
H.265 (hev1) pass through untouched. Out-of-band-parameter H.264 (avc1) and
94+
H.265 (hvc1) are rewritten from length-prefixed NALU to Annex-B with the
95+
parameter sets (SPS/PPS, plus VPS for H.265) prepended to each keyframe.
96+
This reuses `moq-mux`'s `parse_avcc_param_sets` / `parse_hvcc_param_sets`
97+
and `annexb` helpers, the same logic as its `h264::Export` / `h265::Export`.
98+
99+
On ingest, H.264 is reassembled by str0m as Annex-B; `moq-mux`'s H.264
100+
importer in `Avc3` mode publishes the inline-parameter shape directly, which
101+
lines up with what the WebCodecs decoder in `@moq/watch` already expects. No
91102
extra conversion needed in the gateway.
92103

93104
(Written by Claude)

doc/bin/rtmp.md

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
---
22
title: moq-rtmp
3-
description: RTMP / enhanced-RTMP -> MoQ contribution ingest gateway
3+
description: RTMP / enhanced-RTMP <-> MoQ gateway (ingest and egress)
44
---
55

66
# moq-rtmp
77

8-
`moq-rtmp` ingests [RTMP](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol)
9-
(the protocol OBS, ffmpeg, and most hardware encoders speak) and publishes each
10-
stream into Media over QUIC as an ordinary broadcast.
8+
`moq-rtmp` bridges [RTMP](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol)
9+
(the protocol OBS, ffmpeg, and most hardware encoders and players speak) and
10+
Media over QUIC, in **both directions**:
11+
12+
- **Publish (ingest):** an encoder pushes a stream in, and `moq-rtmp` publishes it
13+
into MoQ as an ordinary broadcast.
14+
- **Play (egress):** a player pulls `rtmp://host/<app>/<key>`, and `moq-rtmp`
15+
subscribes to that broadcast from MoQ and streams it back down. VLC, ffplay, and
16+
mpv can play it (browsers can't -- Flash is dead).
1117

1218
RTMP carries media as FLV-format audio/video messages. `moq-rtmp` runs the RTMP
1319
handshake and chunk/AMF session (via the pure-Rust
14-
[`rml_rtmp`](https://crates.io/crates/rml_rtmp), no librtmp), re-wraps each
15-
message as an FLV tag, and feeds it to `moq-mux`'s FLV demuxer, which routes the
16-
media onto MoQ tracks. It's the contribution-ingest sibling of `moq-srt`
17-
(SRT/MPEG-TS) and `moq-rtc` (WHIP).
20+
[`rml_rtmp`](https://crates.io/crates/rml_rtmp), no librtmp). On ingest it re-wraps
21+
each message as an FLV tag and feeds it to `moq-mux`'s FLV demuxer; on egress it
22+
muxes the broadcast back to FLV with `moq-mux` and sends the tags out as RTMP
23+
messages. It's the sibling of `moq-srt` (SRT/MPEG-TS) and `moq-rtc` (WHIP/WHEP).
1824

1925
Both **legacy RTMP** (H.264 + AAC) and **enhanced RTMP** (E-RTMP: the HEVC, AV1,
20-
VP9, Opus, and AC-3 FourCC payloads) are supported, because all codec handling
21-
lives in the `moq-mux` FLV demuxer.
26+
VP9, Opus, and AC-3 FourCC payloads) are supported in each direction, because all
27+
codec handling lives in the `moq-mux` FLV demuxer/muxer. Legacy players that speak
28+
only H.264 + AAC will reject the E-RTMP codecs on the play path.
2229

2330
## CLI shape
2431

@@ -42,6 +49,15 @@ and the stream key to `cam0`; with ffmpeg:
4249
ffmpeg -re -i input.mp4 -c copy -f flv rtmp://127.0.0.1:1935/live/cam0
4350
```
4451

52+
Then play the same broadcast back out over RTMP from any player:
53+
54+
```bash
55+
# Pulls broadcast `live/cam0` (the same URL it was pushed to).
56+
ffplay rtmp://127.0.0.1:1935/live/cam0
57+
mpv rtmp://127.0.0.1:1935/live/cam0
58+
vlc rtmp://127.0.0.1:1935/live/cam0
59+
```
60+
4561
### `serve` flags
4662

4763
- `--server-bind`: QUIC/WebTransport bind address (default `[::]:443`). Also
@@ -81,23 +97,32 @@ moq-rtmp serve --server-bind [::]:443 --tls-generate localhost \
8197

8298
Each connection's broadcast path is `<app>/<key>` from the RTMP app and stream
8399
key (`rtmp://host/<app>/<key>`), falling back to just the app when the key is
84-
empty, with `--rtmp-prefix` prepended. First publisher on a path wins; a second
85-
connection to the same path is rejected.
100+
empty, with `--rtmp-prefix` prepended. The same routing applies to both
101+
directions, so the URL round-trips: push to `rtmp://host/live/cam0`, then pull it
102+
back from `rtmp://host/live/cam0`.
103+
104+
A play waits for the broadcast to be announced, so a player can connect slightly
105+
before the publisher. First **publisher** on a path wins (a second publish to a
106+
live path is rejected); **plays** don't claim a path, so any number of players can
107+
pull the same broadcast at once. In `serve` mode plays are served from the same
108+
origin the server exposes, so anything in it -- RTMP ingests and otherwise -- can
109+
be pulled back out over RTMP.
86110

87111
## Notes and limitations
88112

89113
- **Auth.** The binary (and the `moq_rtmp::run` convenience) is unauthenticated:
90-
anyone who can reach the TCP port can publish. Gate it with a host firewall or
91-
a private network. To authenticate, embed the library and drive its
92-
`Server` / `Request` API: `Server::accept` yields a pending publish, and you
93-
verify the app / stream key (e.g. the stream key as a moq-token JWT) before
94-
calling `request.accept(origin, path)` or `request.reject(reason)` -- no
95-
callback, the policy lives in your loop.
96-
- **Embedding.** A relay can run ingest in-process by depending on the `moq-rtmp`
97-
library (`default-features = false`). Call `moq_rtmp::run` against its own
98-
origin for the unauthenticated case, or use `Server` / `Request` to plug in the
99-
relay's existing JWT/path auth and scope the origin per token. Either way the
100-
media is published locally with no extra hop.
114+
anyone who can reach the TCP port can publish or play. Gate it with a host
115+
firewall or a private network. To authenticate, embed the library and drive its
116+
`Server` / `Request` API: `Server::accept` yields a `Request` that is either a
117+
`Publish` or a `Play`, and you verify the app / stream key (e.g. the stream key
118+
as a moq-token JWT) before accepting it into / out of an origin at a path of your
119+
choosing, or rejecting it -- no callback, the policy lives in your loop.
120+
- **Embedding.** A relay can run the gateway in-process by depending on the
121+
`moq-rtmp` library (`default-features = false`). Call `moq_rtmp::run` against its
122+
own origin for the unauthenticated case (publishers ingest into it, players are
123+
served out of it), or use `Server` / `Request` to plug in the relay's existing
124+
JWT/path auth and scope the origin per token. Either way the media stays local
125+
with no extra hop.
101126
- **RTMPS.** Embedders can terminate TLS themselves: set `Config::tls` (or
102127
`Server::with_tls`) with a `rustls::ServerConfig`, or accept the connection and
103128
finish the TLS handshake by hand and hand the stream to `moq_rtmp::accept_stream`

rs/libmoq/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Native video decode C API: `moq_consume_video_raw` (+ `_close`, `_frame`,
13+
`_frame_free`) subscribes to an H.264 track and hands back decoded I420 frames,
14+
the video counterpart to `moq_consume_audio_raw`. Decoding happens inside
15+
libmoq (VideoToolbox / openh264), so consumers no longer need ffmpeg.
16+
1017
## [0.3.7](https://github.com/moq-dev/moq/compare/libmoq-v0.3.6...libmoq-v0.3.7) - 2026-06-19
1118

1219
### Fixed

rs/libmoq/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ moq-audio = { workspace = true }
2424
moq-mux = { workspace = true }
2525
moq-native = { workspace = true, default-features = true }
2626
moq-net = { workspace = true, features = ["serde"] }
27+
moq-video = { workspace = true }
2728
thiserror = "2"
2829
tokio = { workspace = true, features = ["macros"] }
2930
tracing = "0.1"

rs/libmoq/src/consume.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,25 @@ impl Consume {
492492
Ok(())
493493
}
494494

495+
/// Look up a video rendition by catalog index, returning the
496+
/// (broadcast, config, name) tuple needed to subscribe — mirrors
497+
/// the index-based selection in `video_ordered`.
498+
pub fn video_rendition(
499+
&self,
500+
catalog: Id,
501+
index: usize,
502+
) -> Result<(moq_net::BroadcastConsumer, hang::catalog::VideoConfig, String), Error> {
503+
let consume = self.catalog.get(catalog).ok_or(Error::CatalogNotFound)?;
504+
let (name, config) = consume
505+
.catalog
506+
.video
507+
.renditions
508+
.iter()
509+
.nth(index)
510+
.ok_or(Error::NoIndex)?;
511+
Ok((consume.broadcast.clone(), config.clone(), name.clone()))
512+
}
513+
495514
/// Look up an audio rendition by catalog index, returning the
496515
/// (broadcast, config, name) tuple needed to subscribe — mirrors
497516
/// the index-based selection in `audio_ordered`.

rs/libmoq/src/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ pub enum Error {
142142
/// Error from the moq-audio codec layer.
143143
#[error("audio error: {0}")]
144144
Audio(Arc<moq_audio::AudioError>),
145+
146+
/// Error from the moq-video codec layer.
147+
#[error("video error: {0}")]
148+
Video(Arc<moq_video::Error>),
145149
}
146150

147151
impl From<moq_audio::AudioError> for Error {
@@ -150,6 +154,12 @@ impl From<moq_audio::AudioError> for Error {
150154
}
151155
}
152156

157+
impl From<moq_video::Error> for Error {
158+
fn from(err: moq_video::Error) -> Self {
159+
Error::Video(Arc::new(err))
160+
}
161+
}
162+
153163
impl From<tracing::metadata::ParseLevelError> for Error {
154164
fn from(err: tracing::metadata::ParseLevelError) -> Self {
155165
Error::Level(Arc::new(err))
@@ -195,6 +205,7 @@ impl ffi::ReturnCode for Error {
195205
Error::Native(_) => -33,
196206
Error::Unauthorized => -34,
197207
Error::Forbidden => -35,
208+
Error::Video(_) => -36,
198209
}
199210
}
200211
}

rs/libmoq/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ mod origin;
2626
mod publish;
2727
mod session;
2828
mod state;
29+
mod video;
2930

3031
pub use api::*;
3132
pub use audio::*;
3233
pub use error::*;
3334
pub use id::*;
35+
pub use video::*;
3436

3537
pub(crate) use consume::*;
3638
pub(crate) use origin::*;

rs/libmoq/src/state.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use std::sync::{LazyLock, Mutex, MutexGuard};
22

3-
use crate::{Consume, Origin, Publish, Session, audio::Audio};
3+
use crate::{Consume, Origin, Publish, Session, audio::Audio, video::Video};
44

55
pub struct State {
66
pub session: Session,
77
pub origin: Origin,
88
pub publish: Publish,
99
pub consume: Consume,
1010
pub audio: Audio,
11+
pub video: Video,
1112
}
1213

1314
impl State {
@@ -18,6 +19,7 @@ impl State {
1819
publish: Publish::default(),
1920
consume: Consume::default(),
2021
audio: Audio::default(),
22+
video: Video::default(),
2123
}
2224
}
2325

0 commit comments

Comments
 (0)