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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions doc/lib/py/moq-rs.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,15 @@ Missing track subscriptions are accepted while the `BroadcastDynamic` object is
```python
async for announcement in client.announced("live/"):
print(announcement.path)
print(announcement.hops) # relay origin ids the broadcast traversed, oldest first
...

# Or wait for a specific path:
broadcast = await client.announced_broadcast("live/cam1")
```

`announcement.hops` is the chain of relay origin ids (as `list[int]`) the broadcast passed through to reach you, oldest first. It is useful for routing decisions such as preferring a nearby edge or detecting how many relays a broadcast crossed.

## Examples

The repo ships [example scripts](https://github.com/moq-dev/moq/tree/main/py/moq-rs/examples) you can run end-to-end:
Expand Down
5 changes: 5 additions & 0 deletions py/moq-rs/moq/origin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def __init__(self, inner: MoqAnnouncement) -> None:
def path(self) -> str:
return self._inner.path()

@property
def hops(self) -> list[int]:
"""Origin ids of the relay hops this broadcast traversed, oldest first."""
return self._inner.hops()

@property
def broadcast(self) -> BroadcastConsumer:
return BroadcastConsumer(self._inner.broadcast())
Expand Down
7 changes: 4 additions & 3 deletions py/moq-rs/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ name = "moq-rs"
# isn't already there (no tag needed; the registry is the gate).
# Starts at 0.3.0 to open a new line above the old lockstep 0.2.x releases that
# bundled the bindings.
version = "0.3.0"
version = "0.3.1"
description = "Media over QUIC: real-time pub/sub with built-in caching, fan-out, and prioritization, with a Pythonic API. Import as `moq`."
readme = "README.md"
license = "MIT OR Apache-2.0"
Expand All @@ -30,8 +30,9 @@ classifiers = [
]
# Compatible-release pin: floats to the latest 0.2.x moq-ffi patch without a
# wrapper re-release. A moq-ffi minor bump (0.3) is the only thing that forces
# bumping this constraint.
dependencies = ["moq-ffi ~= 0.2.16"]
# bumping this constraint. Floor is 0.2.24, the first release exposing
# MoqAnnouncement.hops() that Announcement.hops depends on.
dependencies = ["moq-ffi ~= 0.2.24"]

[project.urls]
Homepage = "https://github.com/moq-dev/moq"
Expand Down
30 changes: 30 additions & 0 deletions py/moq-rs/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,36 @@ async def test_serve_helper_accepts_clients():
broadcast.finish()


async def test_announcement_hops_over_wire():
"""An announcement received over the wire exposes its relay hop chain as a list of ints."""
async with moq.Server("127.0.0.1:0", tls_generate=["localhost"]) as server:
broadcast = moq.BroadcastProducer()
server.publish("with-hops", broadcast)

serve_task = asyncio.create_task(server.serve())
try:
async with moq.Client(
f"https://{server.local_addr}",
tls_verify=False,
bind="127.0.0.1:0",
) as client:
async for announcement in client.announced():
assert announcement.path == "with-hops"
hops = announcement.hops
assert isinstance(hops, list)
assert all(isinstance(h, int) for h in hops)
# A broadcast crossing at least one session carries a non-empty hop chain.
assert len(hops) >= 1
break
finally:
serve_task.cancel()
try:
await serve_task
except asyncio.CancelledError:
pass
broadcast.finish()


async def test_serve_helper_with_on_request_rejection():
"""on_request returning False causes Server.serve() to reject the request."""
async with moq.Server("127.0.0.1:0", tls_generate=["localhost"]) as server:
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ authors = ["Luke Curley <kixelated@gmail.com>", "Brian Medley <bpm@bmedley.org>"
repository = "https://github.com/moq-dev/moq"
license = "MIT OR Apache-2.0"

version = "0.2.23"
version = "0.2.24"
edition = "2024"

keywords = ["quic", "http3", "webtransport", "media", "live"]
Expand Down
8 changes: 8 additions & 0 deletions rs/moq-ffi/src/origin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ impl Announced {
loop {
match self.inner.announced().await {
Some((path, Some(broadcast))) => {
let hops = broadcast.hops.iter().map(|origin| origin.id).collect();
return Ok(Some(Arc::new(MoqAnnouncement {
path: path.to_string(),
hops,
broadcast: Arc::new(MoqBroadcastConsumer::new(broadcast)),
})));
}
Expand Down Expand Up @@ -59,6 +61,7 @@ impl Announced {
#[derive(uniffi::Object)]
pub struct MoqAnnouncement {
path: String,
hops: Vec<u64>,
broadcast: Arc<MoqBroadcastConsumer>,
}

Expand Down Expand Up @@ -149,6 +152,11 @@ impl MoqAnnouncement {
self.path.clone()
}

/// The origin ids of the relay hops this broadcast traversed, oldest first.
pub fn hops(&self) -> Vec<u64> {
self.hops.clone()
}

/// The broadcast consumer.
pub fn broadcast(&self) -> Arc<MoqBroadcastConsumer> {
self.broadcast.clone()
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.