mediaway 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: mediaway
3
+ Version: 0.1.1
4
+ Summary: Idiomatic Python binding over Mediaway's C ABI (mux/demux, auto video/audio encode, camera/mic capture)
5
+ Author: Mediaway contributors
6
+ License: MIT OR Apache-2.0
7
+ Project-URL: Repository, https://github.com/nyxways/mediaway
8
+ Project-URL: Documentation, https://nyxways.github.io/mediaway/
9
+ Keywords: media,video,audio,mp4,h264,aac,ffi,mediaway
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Multimedia :: Video
20
+ Classifier: Topic :: Multimedia :: Sound/Audio
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Python binding
25
+
26
+ > **Status: ✅ verified** — the `mediaway` package in `mediaway/` is real
27
+ > (pure-Python `ctypes` over the C ABI) and the examples in `examples/` run against
28
+ > the native libraries: mux/demux roundtrip, real H.264 encode, real camera + mic
29
+ > capture. This README is the **DX contract** the package implements: context
30
+ > managers, exceptions, Rational-second timestamps, bytes for buffers.
31
+
32
+ See the [C binding README](../c/README.md) for the underlying C ABI contract (status
33
+ enums, ownership, thread confinement) — the Python package translates that ABI into
34
+ idiomatic Python.
35
+
36
+ ## What Mediaway is (the capabilities)
37
+
38
+ A streaming-first media stack. The C ABI currently covers three capabilities (full
39
+ detail in [`../c/README.md`](../c/README.md) and `docs/spec/c-ffi.md`):
40
+
41
+ 1. **Container — mux + demux**: sans-io fragmented-MP4 muxer (register video/audio
42
+ tracks, `begin()` → live, push packets, flush, `poll_bytes()`; the muxer never
43
+ touches files — the caller owns byte I/O) and demuxer (`push_bytes`, `streams()`,
44
+ `poll_packet()`, optional ClearKey key). Fully real.
45
+ 2. **Pipeline — auto video encode → fMP4**: one call picks the best available OS/GPU
46
+ encoder for a config, wires it into an internal MP4 muxer; `finish()` returns
47
+ complete MP4 bytes. **Video only** — the audio encoder is separate (ABI v2,
48
+ adr/0003): `AudioEncoder.open()` streams AAC packets for the caller's own muxer.
49
+ 3. **Device — capture**: camera (CPU frames), microphone/loopback (PCM), hotplug.
50
+ **Screen capture is `UNSUPPORTED` from C today** (needs a GPU device handle with no
51
+ C representation yet) — an honest gap, not a bug.
52
+
53
+ ## The real ABI beneath (what the wrapper wraps)
54
+
55
+ DLLs: `mediaway_ffi`, `mediaway_ffi`, `mediaway_ffi` (built
56
+ for `x86_64-pc-windows-gnu`, see the C README's build recipe). Headers
57
+ `crates/mediaway-*-ffi/include/mediaway/{container,pipeline,device}.h` are the
58
+ authoritative layout.
59
+
60
+ - Opaque handles, all **thread-confined** (no concurrent calls on one handle).
61
+ - Every status is a per-crate enum, `OK = 0`; a caught Rust panic poisons the handle.
62
+ `NO_BACKEND` / `UNSUPPORTED` are expected outcomes, not errors.
63
+ - Ownership: borrowed inputs valid for the call only (the wrapper must copy in);
64
+ owned outputs (`poll_bytes` buffers, demuxed packets/stream info, encode `finish`
65
+ buffers, polled device frames) must be released via the matching `_free` — the
66
+ wrapper's job is to make this automatic (context managers / finalizers).
67
+ - Handle-consumption traps the wrapper MUST hide: `mediaway_encode_session_open`
68
+ consumes the encoder unconditionally; `mediaway_encode_session_finish` consumes the
69
+ session. Python can hide this by folding `open` into `EncodeSession` construction and
70
+ making `finish` terminal.
71
+
72
+ ## Ideal API — the DX contract
73
+
74
+ A single `mediaway` package, pure-Python `ctypes` glue + idiomatic wrappers.
75
+ **snake_case everywhere** (Python convention beats the C names): `Rational`,
76
+ `VideoStreamInfo`, `AudioStreamInfo`, `Packet`, `Codec` (enum), `VideoFrame`;
77
+ classes `Muxer`, `Demuxer`, `EncodeSession`, `AutoVideoEncoder`, `VideoCapture`,
78
+ `AudioCapture`.
79
+
80
+ - **Context managers**: `with Muxer() as m:`, `with Demuxer() as d:`,
81
+ `with EncodeSession(...) as s:` — `__exit__` closes the underlying handle (and, for
82
+ capture sessions, joins the backend worker thread). This is the primary lifecycle
83
+ shape; explicit `.close()` exists for non-`with` users.
84
+ - **Exceptions**: a `MediawayError` base carrying the raw status code, with subclasses
85
+ for the expected outcomes (`EncoderUnavailableError`, `DeviceUnavailableError`,
86
+ `CaptureUnsupportedError`) so examples can catch-and-continue rather than crash on
87
+ missing hardware. No status-code checking in example bodies.
88
+ - **Typestate as two classes** (mirrors C++): `Muxer` (Open: `add_video_track` /
89
+ `add_audio_track`) → `.begin()` returns `LiveMuxer` (push_packet / flush /
90
+ poll_bytes). Calling track registration on a `LiveMuxer` is impossible, matching the
91
+ ABI's `INVALID_STATE`.
92
+ - **bytes for byte buffers**: `poll_bytes() -> bytes`; `push_bytes(bytes)`;
93
+ `packet.payload -> bytes`; `frame.data -> bytes` (NV12/BGRA8). The wrapper copies
94
+ out of borrowed/owned native buffers — no `memoryview` leaking into the API.
95
+ - `Rational(num, den)` as a small `dataclass(frozen=True)`; info structs as dataclasses.
96
+ - `EncodeSession(encoder)` takes ownership of the encoder object; `finish() -> bytes`
97
+ is terminal (no `close()` after it).
98
+
99
+ ## Example scenarios
100
+
101
+ `examples/` mirrors the Rust `examples/` layout — sector subfolders, one file per
102
+ scenario (English comments only; each file's header comment states real vs.
103
+ aspirational):
104
+
105
+ | File | Capability | Real today? |
106
+ |---|---|---|
107
+ | `container/mux_roundtrip.py` | mux 90 fake video + audio packets → fMP4 → demux back, count packets | ✅ run verified |
108
+ | `pipeline/encode_to_mp4.py` | auto H.264 encode of 90 synthetic NV12 frames → `out.mp4` | ✅ run verified |
109
+ | `pipeline/encode_audio.py` | auto AAC encode of 96 synthetic F32 stereo frames → audio-only fMP4 (ABI v2) | ✅ run verified (96 packets → 27372 bytes fMP4) |
110
+ | `device/camera_record.py` | camera + mic → H.264 + AAC → ONE two-track MP4 (remuxed; audio track registered with the encoder's AudioSpecificConfig) | ✅ run verified on real hardware (47 frames + 80 AAC packets → ~251 KB two-track MP4); video-only fallback without mic/audio backend |
111
+ | `device/capture_microphone.py` | microphone capture, raw PCM | ✅ run verified (real mic) |
112
+ | `pipeline/screen_record.py` | screen + mic → encode → MP4 | 🚧 aspirational — `VideoCapture.open(source="screen")` raises `CaptureUnsupportedError` today |
113
+ | `device/capture_screen.py` | screen capture only | 🚧 same gap, capture-only |
114
+
115
+ ## Rules
116
+
117
+ - English comments only.
118
+ - Map existing Rust surfaces; do not invent capabilities the Rust side doesn't have.
119
+ - Wrap the ABI fully: no `ctypes` types or raw handles visible in examples.
120
+ - Not part of the Cargo workspace; durable API changes require an ADR (ADR-0004).
@@ -0,0 +1,97 @@
1
+ # Python binding
2
+
3
+ > **Status: ✅ verified** — the `mediaway` package in `mediaway/` is real
4
+ > (pure-Python `ctypes` over the C ABI) and the examples in `examples/` run against
5
+ > the native libraries: mux/demux roundtrip, real H.264 encode, real camera + mic
6
+ > capture. This README is the **DX contract** the package implements: context
7
+ > managers, exceptions, Rational-second timestamps, bytes for buffers.
8
+
9
+ See the [C binding README](../c/README.md) for the underlying C ABI contract (status
10
+ enums, ownership, thread confinement) — the Python package translates that ABI into
11
+ idiomatic Python.
12
+
13
+ ## What Mediaway is (the capabilities)
14
+
15
+ A streaming-first media stack. The C ABI currently covers three capabilities (full
16
+ detail in [`../c/README.md`](../c/README.md) and `docs/spec/c-ffi.md`):
17
+
18
+ 1. **Container — mux + demux**: sans-io fragmented-MP4 muxer (register video/audio
19
+ tracks, `begin()` → live, push packets, flush, `poll_bytes()`; the muxer never
20
+ touches files — the caller owns byte I/O) and demuxer (`push_bytes`, `streams()`,
21
+ `poll_packet()`, optional ClearKey key). Fully real.
22
+ 2. **Pipeline — auto video encode → fMP4**: one call picks the best available OS/GPU
23
+ encoder for a config, wires it into an internal MP4 muxer; `finish()` returns
24
+ complete MP4 bytes. **Video only** — the audio encoder is separate (ABI v2,
25
+ adr/0003): `AudioEncoder.open()` streams AAC packets for the caller's own muxer.
26
+ 3. **Device — capture**: camera (CPU frames), microphone/loopback (PCM), hotplug.
27
+ **Screen capture is `UNSUPPORTED` from C today** (needs a GPU device handle with no
28
+ C representation yet) — an honest gap, not a bug.
29
+
30
+ ## The real ABI beneath (what the wrapper wraps)
31
+
32
+ DLLs: `mediaway_ffi`, `mediaway_ffi`, `mediaway_ffi` (built
33
+ for `x86_64-pc-windows-gnu`, see the C README's build recipe). Headers
34
+ `crates/mediaway-*-ffi/include/mediaway/{container,pipeline,device}.h` are the
35
+ authoritative layout.
36
+
37
+ - Opaque handles, all **thread-confined** (no concurrent calls on one handle).
38
+ - Every status is a per-crate enum, `OK = 0`; a caught Rust panic poisons the handle.
39
+ `NO_BACKEND` / `UNSUPPORTED` are expected outcomes, not errors.
40
+ - Ownership: borrowed inputs valid for the call only (the wrapper must copy in);
41
+ owned outputs (`poll_bytes` buffers, demuxed packets/stream info, encode `finish`
42
+ buffers, polled device frames) must be released via the matching `_free` — the
43
+ wrapper's job is to make this automatic (context managers / finalizers).
44
+ - Handle-consumption traps the wrapper MUST hide: `mediaway_encode_session_open`
45
+ consumes the encoder unconditionally; `mediaway_encode_session_finish` consumes the
46
+ session. Python can hide this by folding `open` into `EncodeSession` construction and
47
+ making `finish` terminal.
48
+
49
+ ## Ideal API — the DX contract
50
+
51
+ A single `mediaway` package, pure-Python `ctypes` glue + idiomatic wrappers.
52
+ **snake_case everywhere** (Python convention beats the C names): `Rational`,
53
+ `VideoStreamInfo`, `AudioStreamInfo`, `Packet`, `Codec` (enum), `VideoFrame`;
54
+ classes `Muxer`, `Demuxer`, `EncodeSession`, `AutoVideoEncoder`, `VideoCapture`,
55
+ `AudioCapture`.
56
+
57
+ - **Context managers**: `with Muxer() as m:`, `with Demuxer() as d:`,
58
+ `with EncodeSession(...) as s:` — `__exit__` closes the underlying handle (and, for
59
+ capture sessions, joins the backend worker thread). This is the primary lifecycle
60
+ shape; explicit `.close()` exists for non-`with` users.
61
+ - **Exceptions**: a `MediawayError` base carrying the raw status code, with subclasses
62
+ for the expected outcomes (`EncoderUnavailableError`, `DeviceUnavailableError`,
63
+ `CaptureUnsupportedError`) so examples can catch-and-continue rather than crash on
64
+ missing hardware. No status-code checking in example bodies.
65
+ - **Typestate as two classes** (mirrors C++): `Muxer` (Open: `add_video_track` /
66
+ `add_audio_track`) → `.begin()` returns `LiveMuxer` (push_packet / flush /
67
+ poll_bytes). Calling track registration on a `LiveMuxer` is impossible, matching the
68
+ ABI's `INVALID_STATE`.
69
+ - **bytes for byte buffers**: `poll_bytes() -> bytes`; `push_bytes(bytes)`;
70
+ `packet.payload -> bytes`; `frame.data -> bytes` (NV12/BGRA8). The wrapper copies
71
+ out of borrowed/owned native buffers — no `memoryview` leaking into the API.
72
+ - `Rational(num, den)` as a small `dataclass(frozen=True)`; info structs as dataclasses.
73
+ - `EncodeSession(encoder)` takes ownership of the encoder object; `finish() -> bytes`
74
+ is terminal (no `close()` after it).
75
+
76
+ ## Example scenarios
77
+
78
+ `examples/` mirrors the Rust `examples/` layout — sector subfolders, one file per
79
+ scenario (English comments only; each file's header comment states real vs.
80
+ aspirational):
81
+
82
+ | File | Capability | Real today? |
83
+ |---|---|---|
84
+ | `container/mux_roundtrip.py` | mux 90 fake video + audio packets → fMP4 → demux back, count packets | ✅ run verified |
85
+ | `pipeline/encode_to_mp4.py` | auto H.264 encode of 90 synthetic NV12 frames → `out.mp4` | ✅ run verified |
86
+ | `pipeline/encode_audio.py` | auto AAC encode of 96 synthetic F32 stereo frames → audio-only fMP4 (ABI v2) | ✅ run verified (96 packets → 27372 bytes fMP4) |
87
+ | `device/camera_record.py` | camera + mic → H.264 + AAC → ONE two-track MP4 (remuxed; audio track registered with the encoder's AudioSpecificConfig) | ✅ run verified on real hardware (47 frames + 80 AAC packets → ~251 KB two-track MP4); video-only fallback without mic/audio backend |
88
+ | `device/capture_microphone.py` | microphone capture, raw PCM | ✅ run verified (real mic) |
89
+ | `pipeline/screen_record.py` | screen + mic → encode → MP4 | 🚧 aspirational — `VideoCapture.open(source="screen")` raises `CaptureUnsupportedError` today |
90
+ | `device/capture_screen.py` | screen capture only | 🚧 same gap, capture-only |
91
+
92
+ ## Rules
93
+
94
+ - English comments only.
95
+ - Map existing Rust surfaces; do not invent capabilities the Rust side doesn't have.
96
+ - Wrap the ABI fully: no `ctypes` types or raw handles visible in examples.
97
+ - Not part of the Cargo workspace; durable API changes require an ADR (ADR-0004).
@@ -0,0 +1,58 @@
1
+ """mediaway — idiomatic Python binding over Mediaway's C ABI.
2
+
3
+ See bindings/python/README.md for the DX contract this package implements:
4
+ context managers, exceptions, Rational-second timestamps, bytes for buffers.
5
+
6
+ Capability truth (see the README's table): container mux/demux ✅ real,
7
+ auto video encode -> fMP4 ✅ real, camera/mic capture ✅ real (CPU frames),
8
+ Screen capture 🚧 unsupported by the ABI today (CaptureUnsupportedError).
9
+ """
10
+
11
+ from ._container import Demuxer, LiveMuxer, Muxer
12
+ from ._device import AudioCapture, VideoCapture
13
+ from ._encoder import AudioEncoder, AutoVideoEncoder, EncodeSession
14
+ from ._errors import (
15
+ CaptureUnsupportedError,
16
+ DeviceUnavailableError,
17
+ EncoderUnavailableError,
18
+ InvalidStateError,
19
+ MediawayError,
20
+ )
21
+ from ._ffi import lib_dir
22
+ from ._types import (
23
+ AudioStreamInfo,
24
+ Codec,
25
+ Packet,
26
+ PixelFormat,
27
+ Rational,
28
+ SampleFormat,
29
+ VideoFrame,
30
+ VideoStreamInfo,
31
+ )
32
+
33
+ __version__ = "0.1.0"
34
+
35
+ __all__ = [
36
+ "Muxer",
37
+ "LiveMuxer",
38
+ "Demuxer",
39
+ "AutoVideoEncoder",
40
+ "EncodeSession",
41
+ "AudioEncoder",
42
+ "VideoCapture",
43
+ "AudioCapture",
44
+ "MediawayError",
45
+ "EncoderUnavailableError",
46
+ "DeviceUnavailableError",
47
+ "CaptureUnsupportedError",
48
+ "InvalidStateError",
49
+ "Rational",
50
+ "Codec",
51
+ "PixelFormat",
52
+ "SampleFormat",
53
+ "VideoStreamInfo",
54
+ "AudioStreamInfo",
55
+ "Packet",
56
+ "VideoFrame",
57
+ "lib_dir",
58
+ ]
@@ -0,0 +1,303 @@
1
+ """Container capability: sans-io fragmented-MP4 mux + demux.
2
+
3
+ Wraps the `mediaway-ffi` C ABI (see `_ffi.py` for the raw layer and
4
+ `../README.md` for the DX contract). Typestate is two classes: `Muxer` (Open —
5
+ tracks can be registered) and `LiveMuxer` (streaming — packets can be pushed),
6
+ returned by `Muxer.begin()`. The muxer never touches files or sockets: the
7
+ caller owns all byte I/O.
8
+
9
+ Timestamps are `Rational` seconds; the ABI works in integer units of each
10
+ track's time base, so packets are converted on the way in/out.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import ctypes
16
+ import math
17
+ from ctypes import byref, c_bool, c_size_t, c_ubyte, cast, create_string_buffer
18
+
19
+ from . import _ffi
20
+ from ._errors import InvalidStateError, MediawayError
21
+ from ._types import AudioStreamInfo, Codec, Packet, Rational, VideoStreamInfo
22
+
23
+ __all__ = ["Muxer", "LiveMuxer", "Demuxer"]
24
+
25
+
26
+ def _to_units(seconds: Rational, time_base: Rational) -> int:
27
+ """Convert Rational seconds to integer ticks of `time_base` (rounded)."""
28
+ # ticks = seconds / (tb.num / tb.den) = seconds.num*tb.den / (seconds.den*tb.num)
29
+ num = seconds.num * time_base.den
30
+ den = seconds.den * time_base.num
31
+ return (num + den // 2) // den
32
+
33
+
34
+ def _from_units(ticks: int, time_base: Rational) -> Rational:
35
+ """Convert integer ticks of `time_base` to Rational seconds (reduced)."""
36
+ num = ticks * time_base.num
37
+ den = time_base.den
38
+ g = math.gcd(num, den)
39
+ return Rational(num // g, den // g)
40
+
41
+
42
+ def _copy_bytes(ptr, length: int) -> bytes:
43
+ if not ptr or length == 0:
44
+ return b""
45
+ return ctypes.string_at(ptr, length)
46
+
47
+
48
+ def _check_container(status: int) -> None:
49
+ if status == _ffi.MEDIAWAY_OK:
50
+ return
51
+ names = {
52
+ _ffi.MEDIAWAY_STATUS_INVALID_ARGUMENT: "invalid argument",
53
+ _ffi.MEDIAWAY_STATUS_INVALID_STATE: "invalid state",
54
+ _ffi.MEDIAWAY_STATUS_INVALID_TRACK: "invalid or duplicate track id",
55
+ _ffi.MEDIAWAY_STATUS_INVALID_PACKET: "packet does not match a registered track",
56
+ _ffi.MEDIAWAY_STATUS_INVALID_DATA: "truncated or malformed container data",
57
+ _ffi.MEDIAWAY_STATUS_UNKNOWN_ERROR: "unknown error",
58
+ _ffi.MEDIAWAY_STATUS_INTERNAL_PANIC: "internal panic (handle poisoned)",
59
+ _ffi.MEDIAWAY_STATUS_HANDLE_POISONED: "handle poisoned by an earlier panic",
60
+ }
61
+ message = names.get(status, "unknown status")
62
+ cls = InvalidStateError if status == _ffi.MEDIAWAY_STATUS_INVALID_STATE else MediawayError
63
+ raise cls(status, message)
64
+
65
+
66
+ class Muxer:
67
+ """A muxer in the track-registration (Open) state.
68
+
69
+ Stream ids are assigned in registration order: the first `add_*_track`
70
+ call returns 0, the second 1, and so on. `begin()` consumes this object
71
+ (its handle moves into the returned `LiveMuxer`), making "add_track after
72
+ begin" a Python AttributeError instead of the ABI's INVALID_STATE.
73
+ """
74
+
75
+ def __init__(self, fragment_batch: int | None = None):
76
+ dll = _ffi.container.dll
77
+ if fragment_batch is None:
78
+ self._handle = dll.mediaway_muxer_create()
79
+ else:
80
+ self._handle = dll.mediaway_muxer_create_with_fragment_batch(fragment_batch)
81
+ if not self._handle:
82
+ raise MediawayError(_ffi.MEDIAWAY_STATUS_INTERNAL_PANIC, "muxer creation panicked")
83
+ self._next_id = 0
84
+ self._time_bases: dict[int, Rational] = {} # stream id -> ABI time base
85
+
86
+ def _assign_id(self) -> int:
87
+ track_id = self._next_id
88
+ self._next_id += 1
89
+ return track_id
90
+
91
+ def add_video_track(self, info: VideoStreamInfo) -> int:
92
+ track_id = self._assign_id()
93
+ raw = _ffi.VideoTrackInfo(
94
+ id=track_id,
95
+ codec=int(info.codec),
96
+ time_base=_ffi.Rational(info.frame_rate.num, info.frame_rate.den),
97
+ width=info.width,
98
+ height=info.height,
99
+ extra_data=None,
100
+ extra_data_len=0,
101
+ )
102
+ if info.extra_data:
103
+ buf = create_string_buffer(info.extra_data, len(info.extra_data))
104
+ raw.extra_data = cast(buf, _ffi.U8P)
105
+ raw.extra_data_len = len(info.extra_data)
106
+ _check_container(_ffi.container.dll.mediaway_muxer_add_video_track(self._handle, byref(raw)))
107
+ self._time_bases[track_id] = info.frame_rate
108
+ return track_id
109
+
110
+ def add_audio_track(self, info: AudioStreamInfo) -> int:
111
+ track_id = self._assign_id()
112
+ raw = _ffi.AudioTrackInfo(
113
+ id=track_id,
114
+ codec=int(info.codec),
115
+ time_base=_ffi.Rational(1, info.sample_rate),
116
+ sample_rate=info.sample_rate,
117
+ channels=info.channels,
118
+ extra_data=None,
119
+ extra_data_len=0,
120
+ )
121
+ if info.extra_data:
122
+ buf = create_string_buffer(info.extra_data, len(info.extra_data))
123
+ raw.extra_data = cast(buf, _ffi.U8P)
124
+ raw.extra_data_len = len(info.extra_data)
125
+ _check_container(_ffi.container.dll.mediaway_muxer_add_audio_track(self._handle, byref(raw)))
126
+ self._time_bases[track_id] = Rational(1, info.sample_rate)
127
+ return track_id
128
+
129
+ def begin(self) -> "LiveMuxer":
130
+ _check_container(_ffi.container.dll.mediaway_muxer_begin(self._handle))
131
+ live = LiveMuxer(self._handle, self._time_bases)
132
+ self._handle = None # ownership transferred to the LiveMuxer
133
+ return live
134
+
135
+ def __enter__(self) -> "Muxer":
136
+ return self
137
+
138
+ def __exit__(self, exc_type, exc, tb) -> None:
139
+ self.close()
140
+
141
+ def close(self) -> None:
142
+ if self._handle:
143
+ _ffi.container.dll.mediaway_muxer_close(self._handle)
144
+ self._handle = None
145
+
146
+
147
+ class LiveMuxer:
148
+ """A muxer in the streaming (Live) state, from `Muxer.begin()`."""
149
+
150
+ def __init__(self, handle: int, time_bases: dict[int, Rational]):
151
+ self._handle = handle
152
+ self._time_bases = time_bases # stream id -> ABI time base
153
+
154
+ def push_packet(self, packet: Packet) -> None:
155
+ dts = packet.dts if packet.dts is not None else packet.pts
156
+ duration = packet.duration
157
+ try:
158
+ tb = self._time_bases[packet.stream_index]
159
+ except KeyError:
160
+ raise MediawayError(
161
+ _ffi.MEDIAWAY_STATUS_INVALID_TRACK,
162
+ f"no track with stream_index {packet.stream_index}",
163
+ ) from None
164
+ raw = _ffi.PacketView(
165
+ stream_id=packet.stream_index,
166
+ pts=0,
167
+ dts=0,
168
+ duration=0,
169
+ is_keyframe=packet.key,
170
+ is_discard=False,
171
+ payload=None,
172
+ payload_len=0,
173
+ )
174
+ if packet.payload:
175
+ buf = create_string_buffer(packet.payload, len(packet.payload))
176
+ raw.payload = cast(buf, _ffi.U8P)
177
+ raw.payload_len = len(packet.payload)
178
+ # Convert Rational seconds -> integer ticks of the stream's time base.
179
+ raw.pts = _to_units(packet.pts, tb)
180
+ raw.dts = _to_units(dts, tb)
181
+ if duration is not None:
182
+ raw.duration = _to_units(duration, tb)
183
+ _check_container(_ffi.container.dll.mediaway_muxer_push_packet(self._handle, byref(raw)))
184
+
185
+ def flush(self) -> None:
186
+ _check_container(_ffi.container.dll.mediaway_muxer_flush(self._handle))
187
+
188
+ def poll_bytes(self) -> bytes | None:
189
+ """Drain whatever fMP4 bytes are ready now; None when nothing is ready."""
190
+ out_data = _ffi.U8P()
191
+ out_len = c_size_t(0)
192
+ _check_container(
193
+ _ffi.container.dll.mediaway_muxer_poll_bytes(self._handle, byref(out_data), byref(out_len))
194
+ )
195
+ length = out_len.value
196
+ if length == 0:
197
+ return None
198
+ data = _copy_bytes(out_data, length)
199
+ _ffi.container.dll.mediaway_buffer_free(out_data, out_len)
200
+ return data
201
+
202
+ def __enter__(self) -> "LiveMuxer":
203
+ return self
204
+
205
+ def __exit__(self, exc_type, exc, tb) -> None:
206
+ self.close()
207
+
208
+ def close(self) -> None:
209
+ if self._handle:
210
+ _ffi.container.dll.mediaway_muxer_close(self._handle)
211
+ self._handle = None
212
+
213
+
214
+ class Demuxer:
215
+ """A streaming demuxer: feed container bytes, poll streams and packets."""
216
+
217
+ def __init__(self, clear_key: bytes | None = None):
218
+ self._handle = _ffi.container.dll.mediaway_demuxer_create()
219
+ if not self._handle:
220
+ raise MediawayError(_ffi.MEDIAWAY_STATUS_INTERNAL_PANIC, "demuxer creation panicked")
221
+ self._time_bases: dict[int, Rational] = {}
222
+ if clear_key is not None:
223
+ self.set_decryption_key(clear_key)
224
+
225
+ def push_bytes(self, data: bytes) -> None:
226
+ buf = create_string_buffer(data, len(data))
227
+ _check_container(
228
+ _ffi.container.dll.mediaway_demuxer_push_bytes(self._handle, cast(buf, _ffi.U8P), len(data))
229
+ )
230
+
231
+ def stream_count(self) -> int:
232
+ return _ffi.container.dll.mediaway_demuxer_stream_count(self._handle)
233
+
234
+ def streams(self) -> list[VideoStreamInfo | AudioStreamInfo]:
235
+ out: list[VideoStreamInfo | AudioStreamInfo] = []
236
+ for index in range(self.stream_count()):
237
+ raw = _ffi.StreamInfo()
238
+ _check_container(_ffi.container.dll.mediaway_demuxer_stream_at(self._handle, index, byref(raw)))
239
+ extra = _copy_bytes(raw.extra_data, raw.extra_data_len)
240
+ _ffi.container.dll.mediaway_stream_info_free(byref(raw))
241
+ time_base = Rational(raw.time_base.num, raw.time_base.den)
242
+ self._time_bases[raw.id] = time_base
243
+ codec = Codec(raw.codec)
244
+ if raw.has_geometry:
245
+ out.append(
246
+ VideoStreamInfo(
247
+ codec=codec,
248
+ width=raw.width,
249
+ height=raw.height,
250
+ frame_rate=time_base,
251
+ extra_data=extra,
252
+ )
253
+ )
254
+ else:
255
+ out.append(
256
+ AudioStreamInfo(
257
+ codec=codec,
258
+ sample_rate=raw.sample_rate,
259
+ channels=raw.channels,
260
+ extra_data=extra,
261
+ )
262
+ )
263
+ return out
264
+
265
+ def poll_packet(self) -> Packet | None:
266
+ raw = _ffi.Packet()
267
+ has = c_bool(False)
268
+ _check_container(_ffi.container.dll.mediaway_demuxer_poll_packet(self._handle, byref(raw), byref(has)))
269
+ if not has.value:
270
+ return None
271
+ payload = _copy_bytes(raw.payload, raw.payload_len)
272
+ _ffi.container.dll.mediaway_packet_free(byref(raw))
273
+ tb = self._time_bases.get(raw.stream_id, Rational(1, 30))
274
+ return Packet(
275
+ stream_index=raw.stream_id,
276
+ pts=_from_units(raw.pts, tb),
277
+ dts=_from_units(raw.dts, tb),
278
+ payload=payload,
279
+ key=raw.is_keyframe,
280
+ duration=_from_units(raw.duration, tb) if raw.duration else None,
281
+ )
282
+
283
+ def set_decryption_key(self, key: bytes) -> None:
284
+ if len(key) != 16:
285
+ raise ValueError("decryption key must be exactly 16 bytes")
286
+ buf = create_string_buffer(key, 16)
287
+ _check_container(
288
+ _ffi.container.dll.mediaway_demuxer_set_decryption_key(self._handle, cast(buf, _ffi.U8P), 16)
289
+ )
290
+
291
+ def clear_decryption_key(self) -> None:
292
+ _check_container(_ffi.container.dll.mediaway_demuxer_clear_decryption_key(self._handle))
293
+
294
+ def __enter__(self) -> "Demuxer":
295
+ return self
296
+
297
+ def __exit__(self, exc_type, exc, tb) -> None:
298
+ self.close()
299
+
300
+ def close(self) -> None:
301
+ if self._handle:
302
+ _ffi.container.dll.mediaway_demuxer_close(self._handle)
303
+ self._handle = None