Skip to content

Commit a20f85c

Browse files
lullabeeclairekixelatedclaude
authored
Adding data (aka json) to the py_lib (#1318)
Co-authored-by: claire <claire@rclaire.ai> Co-authored-by: Luke Curley <kixelated@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7a627f3 commit a20f85c

10 files changed

Lines changed: 720 additions & 10 deletions

File tree

justfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ ci:
6868
# Run the Python checks.
6969
uv run ruff check py/
7070
uv run ruff format --check py/
71-
uv run --package moq-lite pyright
71+
# Sync moq-lite's dev group (pytest) first, then override moq-ffi with a source build;
72+
# --no-sync on the pyright invocation keeps uv from reinstalling the registry moq-ffi.
73+
uv sync --package moq-lite
74+
uv run maturin develop -m rs/moq-ffi/Cargo.toml --uv
75+
uv run --package moq-lite --no-sync pyright
7276

7377
# Run the tofu checks.
7478
(cd cdn && just check)
@@ -120,8 +124,9 @@ test *args:
120124

121125
# Run the Python tests.
122126
if command -v uv &> /dev/null; then
127+
uv sync --package moq-lite
123128
uv run maturin develop -m rs/moq-ffi/Cargo.toml --uv
124-
uv run --package moq-lite pytest py/moq-lite/tests/
129+
uv run --package moq-lite --no-sync pytest py/moq-lite/tests/
125130
fi
126131

127132
# Automatically fix some issues.

py/moq-lite/moq_lite/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
from .client import Client
44
from .origin import Announced, AnnouncedBroadcast, Announcement, OriginConsumer, OriginProducer
5-
from .publish import BroadcastProducer, MediaProducer
6-
from .subscribe import BroadcastConsumer, CatalogConsumer, Container, MediaConsumer
5+
from .publish import BroadcastProducer, GroupProducer, MediaProducer, TrackProducer
6+
from .subscribe import BroadcastConsumer, CatalogConsumer, Container, GroupConsumer, MediaConsumer, TrackConsumer
77
from .types import Audio, Catalog, Dimensions, Frame, Video
88

99
__all__ = [
@@ -19,9 +19,13 @@
1919
"Client",
2020
"Dimensions",
2121
"Frame",
22+
"GroupConsumer",
23+
"GroupProducer",
2224
"MediaConsumer",
2325
"MediaProducer",
2426
"OriginConsumer",
2527
"OriginProducer",
28+
"TrackConsumer",
29+
"TrackProducer",
2630
"Video",
2731
]

py/moq-lite/moq_lite/publish.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
from __future__ import annotations
44

5-
from moq_ffi import MoqBroadcastProducer, MoqMediaProducer
5+
from typing import TYPE_CHECKING
6+
7+
from moq_ffi import MoqBroadcastProducer, MoqGroupProducer, MoqMediaProducer, MoqTrackProducer
8+
9+
if TYPE_CHECKING:
10+
from .subscribe import BroadcastConsumer, GroupConsumer, TrackConsumer
611

712

813
class MediaProducer:
@@ -18,6 +23,57 @@ def finish(self) -> None:
1823
self._inner.finish()
1924

2025

26+
class GroupProducer:
27+
"""Writes frames into a single group on a track."""
28+
29+
def __init__(self, inner: MoqGroupProducer) -> None:
30+
self._inner = inner
31+
32+
@property
33+
def sequence(self) -> int:
34+
"""The sequence number of this group within the track."""
35+
return self._inner.sequence()
36+
37+
def consume(self) -> GroupConsumer:
38+
"""Create a consumer that reads frames from this group."""
39+
from .subscribe import GroupConsumer
40+
41+
return GroupConsumer(self._inner.consume())
42+
43+
def write_frame(self, payload: bytes) -> None:
44+
self._inner.write_frame(payload)
45+
46+
def finish(self) -> None:
47+
self._inner.finish()
48+
49+
50+
class TrackProducer:
51+
"""Track producer — write arbitrary byte payloads with no codec required.
52+
53+
Same pattern as moq-boy's status/command tracks.
54+
"""
55+
56+
def __init__(self, inner: MoqTrackProducer) -> None:
57+
self._inner = inner
58+
59+
def append_group(self) -> GroupProducer:
60+
"""Start a new group; write frames into it, then finish()."""
61+
return GroupProducer(self._inner.append_group())
62+
63+
def write_frame(self, payload: bytes) -> None:
64+
"""Convenience: write a single-frame group in one call."""
65+
self._inner.write_frame(payload)
66+
67+
def consume(self) -> TrackConsumer:
68+
"""Create a consumer that reads directly from this producer's track."""
69+
from .subscribe import TrackConsumer
70+
71+
return TrackConsumer(self._inner.consume())
72+
73+
def finish(self) -> None:
74+
self._inner.finish()
75+
76+
2177
class BroadcastProducer:
2278
"""Wraps MoqBroadcastProducer with a cleaner interface."""
2379

@@ -27,5 +83,15 @@ def __init__(self) -> None:
2783
def publish_media(self, format: str, init: bytes) -> MediaProducer:
2884
return MediaProducer(self._inner.publish_media(format, init))
2985

86+
def publish_track(self, name: str) -> TrackProducer:
87+
"""Create a track — send any bytes, no codec validation."""
88+
return TrackProducer(self._inner.publish_track(name))
89+
90+
def consume(self) -> BroadcastConsumer:
91+
"""Create a consumer that reads from this broadcast's tracks."""
92+
from .subscribe import BroadcastConsumer
93+
94+
return BroadcastConsumer(self._inner.consume())
95+
3096
def finish(self) -> None:
3197
self._inner.finish()

py/moq-lite/moq_lite/subscribe.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
from __future__ import annotations
44

5-
from moq_ffi import Container, MoqBroadcastConsumer, MoqCatalogConsumer, MoqMediaConsumer
5+
from moq_ffi import (
6+
Container,
7+
MoqBroadcastConsumer,
8+
MoqCatalogConsumer,
9+
MoqGroupConsumer,
10+
MoqMediaConsumer,
11+
MoqTrackConsumer,
12+
)
613

714
from .types import Catalog, Frame
815

@@ -26,6 +33,84 @@ def cancel(self) -> None:
2633
self._inner.cancel()
2734

2835

36+
class GroupConsumer:
37+
"""Async iterator of byte payloads within a single group."""
38+
39+
def __init__(self, inner: MoqGroupConsumer) -> None:
40+
self._inner = inner
41+
42+
@property
43+
def sequence(self) -> int:
44+
"""The sequence number of this group within the track."""
45+
return self._inner.sequence()
46+
47+
def __aiter__(self):
48+
return self
49+
50+
async def __anext__(self) -> bytes:
51+
frame = await self._inner.read_frame()
52+
if frame is None:
53+
raise StopAsyncIteration
54+
return frame
55+
56+
def cancel(self) -> None:
57+
self._inner.cancel()
58+
59+
60+
class TrackConsumer:
61+
"""Async iterator of groups from a track.
62+
63+
Each group is itself an async iterator of byte payloads. Same pattern as
64+
moq-boy's status/command tracks (one frame per group), but multi-frame
65+
groups are also supported.
66+
"""
67+
68+
def __init__(self, inner: MoqTrackConsumer) -> None:
69+
self._inner = inner
70+
71+
def __aiter__(self):
72+
return self
73+
74+
async def __anext__(self) -> GroupConsumer:
75+
group = await self.recv_group()
76+
if group is None:
77+
raise StopAsyncIteration
78+
return group
79+
80+
async def recv_group(self) -> GroupConsumer | None:
81+
"""Return the next group in arrival order. Returns `None` when the track ends.
82+
83+
Groups are returned as they arrive on the wire, which may be out of sequence
84+
order. Use this for live consumption where latency matters more than order.
85+
"""
86+
group = await self._inner.recv_group()
87+
if group is None:
88+
return None
89+
return GroupConsumer(group)
90+
91+
async def next_group(self) -> GroupConsumer | None:
92+
"""Return the next group in sequence order, skipping forward if behind.
93+
94+
Returns `None` when the track ends. Use this when order matters more than
95+
latency; `recv_group` is preferred for live consumption.
96+
"""
97+
group = await self._inner.next_group()
98+
if group is None:
99+
return None
100+
return GroupConsumer(group)
101+
102+
async def read_frame(self) -> bytes | None:
103+
"""Read the first frame of the next group.
104+
105+
Convenience for tracks using one-frame-per-group (like moq-boy's
106+
status/command tracks). Returns `None` when the track ends.
107+
"""
108+
return await self._inner.read_frame()
109+
110+
def cancel(self) -> None:
111+
self._inner.cancel()
112+
113+
29114
class CatalogConsumer:
30115
"""Wraps MoqCatalogConsumer as an async iterator of Catalog."""
31116

@@ -54,6 +139,10 @@ def __init__(self, inner: MoqBroadcastConsumer) -> None:
54139
def subscribe_catalog(self) -> CatalogConsumer:
55140
return CatalogConsumer(self._inner.subscribe_catalog())
56141

142+
def subscribe_track(self, name: str) -> TrackConsumer:
143+
"""Subscribe to a track — receive arbitrary byte payloads."""
144+
return TrackConsumer(self._inner.subscribe_track(name))
145+
57146
def subscribe_media(self, name: str, container: Container, max_latency_ms: int) -> MediaConsumer:
58147
return MediaConsumer(self._inner.subscribe_media(name, container, max_latency_ms))
59148

py/moq-lite/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "moq-lite"
7-
version = "0.0.2"
7+
version = "0.0.3"
88
description = "Ergonomic Python wrapper for MoQ (Media over QUIC)"
99
readme = "README.md"
1010
license = "MIT OR Apache-2.0"

0 commit comments

Comments
 (0)