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.
- mediaway-0.1.1/PKG-INFO +120 -0
- mediaway-0.1.1/README.md +97 -0
- mediaway-0.1.1/mediaway/__init__.py +58 -0
- mediaway-0.1.1/mediaway/_container.py +303 -0
- mediaway-0.1.1/mediaway/_device.py +273 -0
- mediaway-0.1.1/mediaway/_encoder.py +283 -0
- mediaway-0.1.1/mediaway/_errors.py +70 -0
- mediaway-0.1.1/mediaway/_ffi.py +682 -0
- mediaway-0.1.1/mediaway/_native/mediaway_ffi.dll +0 -0
- mediaway-0.1.1/mediaway/_types.py +117 -0
- mediaway-0.1.1/mediaway/py.typed +0 -0
- mediaway-0.1.1/mediaway.egg-info/PKG-INFO +120 -0
- mediaway-0.1.1/mediaway.egg-info/SOURCES.txt +16 -0
- mediaway-0.1.1/mediaway.egg-info/dependency_links.txt +1 -0
- mediaway-0.1.1/mediaway.egg-info/top_level.txt +1 -0
- mediaway-0.1.1/pyproject.toml +36 -0
- mediaway-0.1.1/setup.cfg +4 -0
- mediaway-0.1.1/setup.py +11 -0
mediaway-0.1.1/PKG-INFO
ADDED
|
@@ -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).
|
mediaway-0.1.1/README.md
ADDED
|
@@ -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
|