reactor-runtime 3.2.2__tar.gz → 3.2.4__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.
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/PKG-INFO +7 -7
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/README.md +6 -6
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/pyproject.toml +1 -1
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/pyproject.toml.orig +1 -1
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/manifest.py +16 -10
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/recording/recorder.py +124 -19
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/runner/runner.py +16 -6
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/LICENSE +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/NOTICE +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/fields.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/model.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/service.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/session.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/transport.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/core/values.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/http/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/http/routes.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/http/server.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/client.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/decorators.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/internal/bridge.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/contract.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/log.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/message_gateway.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/metrics.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/runner/connection_manager.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/runner/state_machine.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/serve.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/service.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/acceptor.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/config.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/connection.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/peer.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/control_pb2.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/control_pb2.pyi +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/platform_pb2.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_wire/v1/track_pb2.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor-runtime
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.4
|
|
4
4
|
Summary: A Python framework for building real-time, interactive video models
|
|
5
5
|
Author: Reactor
|
|
6
6
|
Author-email: Reactor <team@reactor.inc>
|
|
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
**Build real-time AI models in Python.**
|
|
32
32
|
|
|
33
|
-
[📖 Documentation](https://
|
|
33
|
+
[📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/overview) · [🌐 Reactor](https://reactor.inc)
|
|
34
34
|
|
|
35
35
|
</div>
|
|
36
36
|
|
|
@@ -95,19 +95,19 @@ reactor run
|
|
|
95
95
|
|
|
96
96
|
## Install
|
|
97
97
|
|
|
98
|
-
Everything runs through the [`reactor` CLI](https://
|
|
98
|
+
Everything runs through the [`reactor` CLI](https://docs.reactor.inc/deploy/platform/installation). There is nothing to install on your host but the CLI and Docker; the runtime ships inside the image the CLI builds for your workspace.
|
|
99
99
|
|
|
100
100
|
```sh
|
|
101
101
|
brew install reactor-team/tools/reactor-cli
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Not on macOS, or pinning a release in CI? See [Install the CLI](https://
|
|
104
|
+
Not on macOS, or pinning a release in CI? See [Install the CLI](https://docs.reactor.inc/deploy/platform/installation).
|
|
105
105
|
|
|
106
106
|
## Learn more
|
|
107
107
|
|
|
108
|
-
- [Quickstart](https://
|
|
109
|
-
- [Model anatomy](https://
|
|
110
|
-
- [The run loop](https://
|
|
108
|
+
- [Quickstart](https://docs.reactor.inc/deploy/development/overview): from zero to a streaming model in 2 minutes
|
|
109
|
+
- [Model anatomy](https://docs.reactor.inc/deploy/development/reactor-model/model-anatomy): every piece of a Reactor model, line by line
|
|
110
|
+
- [The run loop](https://docs.reactor.inc/deploy/development/reactor-model/run-loop): emitting frames, batches, and frame rates
|
|
111
111
|
|
|
112
112
|
## Development
|
|
113
113
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Build real-time AI models in Python.**
|
|
6
6
|
|
|
7
|
-
[📖 Documentation](https://
|
|
7
|
+
[📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/overview) · [🌐 Reactor](https://reactor.inc)
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
@@ -69,19 +69,19 @@ reactor run
|
|
|
69
69
|
|
|
70
70
|
## Install
|
|
71
71
|
|
|
72
|
-
Everything runs through the [`reactor` CLI](https://
|
|
72
|
+
Everything runs through the [`reactor` CLI](https://docs.reactor.inc/deploy/platform/installation). There is nothing to install on your host but the CLI and Docker; the runtime ships inside the image the CLI builds for your workspace.
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
75
|
brew install reactor-team/tools/reactor-cli
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
Not on macOS, or pinning a release in CI? See [Install the CLI](https://
|
|
78
|
+
Not on macOS, or pinning a release in CI? See [Install the CLI](https://docs.reactor.inc/deploy/platform/installation).
|
|
79
79
|
|
|
80
80
|
## Learn more
|
|
81
81
|
|
|
82
|
-
- [Quickstart](https://
|
|
83
|
-
- [Model anatomy](https://
|
|
84
|
-
- [The run loop](https://
|
|
82
|
+
- [Quickstart](https://docs.reactor.inc/deploy/development/overview): from zero to a streaming model in 2 minutes
|
|
83
|
+
- [Model anatomy](https://docs.reactor.inc/deploy/development/reactor-model/model-anatomy): every piece of a Reactor model, line by line
|
|
84
|
+
- [The run loop](https://docs.reactor.inc/deploy/development/reactor-model/run-loop): emitting frames, batches, and frame rates
|
|
85
85
|
|
|
86
86
|
## Development
|
|
87
87
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The ``reactor.yaml`` manifest is the runtime's one configuration file:
|
|
4
4
|
``runtime.import`` names the model as a ``"module:Class"`` reference,
|
|
5
|
-
``runtime.config`` points at the model's own config file, and the
|
|
6
|
-
``recording
|
|
5
|
+
``runtime.config`` points at the model's own config file, and the
|
|
6
|
+
``runtime.recording`` block configures the recorder. This module turns that file into
|
|
7
7
|
a :class:`~reactor_runtime.core.RuntimeConfig` and the reference into the model
|
|
8
8
|
class — and it is the only code that does either, so every entry point resolves
|
|
9
9
|
the same model from the same directory.
|
|
@@ -32,8 +32,8 @@ def load_config(manifest: Path) -> RuntimeConfig:
|
|
|
32
32
|
|
|
33
33
|
``runtime.import`` — the ``"module:Class"`` model reference — and
|
|
34
34
|
``runtime.config`` — the path to the model's own config file — name the
|
|
35
|
-
model, ``model.name`` is the name it is published under, and the
|
|
36
|
-
``recording
|
|
35
|
+
model, ``model.name`` is the name it is published under, and the
|
|
36
|
+
``runtime.recording`` block configures the recorder; the rest of the manifest
|
|
37
37
|
describes the model to the platform and is not the runtime's concern. The
|
|
38
38
|
config path is passed to the model verbatim (resolved to an absolute path);
|
|
39
39
|
the runtime never parses its contents.
|
|
@@ -65,7 +65,7 @@ def load_config(manifest: Path) -> RuntimeConfig:
|
|
|
65
65
|
model_ref=model_ref,
|
|
66
66
|
model_name=_model_name(document.get("model")),
|
|
67
67
|
config_path=_resolve_config_path(runtime, manifest),
|
|
68
|
-
recording=_recording_from_manifest(document
|
|
68
|
+
recording=_recording_from_manifest(runtime, document),
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
|
|
@@ -110,19 +110,25 @@ def import_model_class(model_ref: str) -> type[ReactorCore]:
|
|
|
110
110
|
return model_cls
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def _recording_from_manifest(
|
|
113
|
+
def _recording_from_manifest(runtime: dict[str, Any], document: dict[str, Any]) -> RecordingConfig:
|
|
114
114
|
"""Parse the manifest's ``recording:`` block into a :class:`RecordingConfig`.
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
The block nests under ``runtime:``. A top-level ``recording:`` block is
|
|
117
|
+
still honored for older manifests, and the nested block wins when both are
|
|
118
|
+
present. A missing or non-mapping block leaves recording disabled at its
|
|
119
|
+
defaults. Unknown keys are ignored so a manifest can carry forward-looking
|
|
120
|
+
settings without breaking an older runtime.
|
|
119
121
|
|
|
120
122
|
Args:
|
|
121
|
-
|
|
123
|
+
runtime: The manifest's ``runtime`` section.
|
|
124
|
+
document: The whole manifest document, for the legacy top-level block.
|
|
122
125
|
|
|
123
126
|
Returns:
|
|
124
127
|
The parsed recorder configuration.
|
|
125
128
|
"""
|
|
129
|
+
block = runtime.get("recording")
|
|
130
|
+
if not isinstance(block, dict):
|
|
131
|
+
block = document.get("recording")
|
|
126
132
|
if not isinstance(block, dict):
|
|
127
133
|
return RecordingConfig()
|
|
128
134
|
raw_video = block.get("video")
|
|
@@ -15,7 +15,6 @@ director learns a clip is genuinely fetchable rather than merely requested.
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
import contextlib
|
|
19
18
|
import math
|
|
20
19
|
import queue
|
|
21
20
|
import re
|
|
@@ -25,6 +24,7 @@ import threading
|
|
|
25
24
|
import time
|
|
26
25
|
from collections.abc import Callable
|
|
27
26
|
from dataclasses import dataclass
|
|
27
|
+
from enum import Enum
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import Any
|
|
30
30
|
from urllib.parse import urlencode
|
|
@@ -45,6 +45,21 @@ logger = get_logger(__name__)
|
|
|
45
45
|
# model that runs slower or faster than real time still records at true duration.
|
|
46
46
|
RECORDING_FPS = 30
|
|
47
47
|
|
|
48
|
+
# How many grid frames may sit queued between the model thread and the encoder.
|
|
49
|
+
# Never applied below one emission's worth, so a model that batches always fits
|
|
50
|
+
# a whole emission: the queue absorbs a burst and drains it between emissions,
|
|
51
|
+
# rather than gating the burst at its own depth.
|
|
52
|
+
_FEED_DEPTH = 4
|
|
53
|
+
# How long one emission may wait, in total, for the encoder to make room. Bounds
|
|
54
|
+
# the producer's exposure to a wedged encoder: past the deadline the rest of the
|
|
55
|
+
# emission is counted and abandoned, so a stalled recording costs the recording
|
|
56
|
+
# rather than the session.
|
|
57
|
+
_FEED_WAIT_SECONDS = 1.0
|
|
58
|
+
# How often the feed reports dropped frames. An encoder that stays behind loses
|
|
59
|
+
# frames on every emission, so the count is carried on every recording's summary
|
|
60
|
+
# and only the periodic warning is rate-limited.
|
|
61
|
+
_DROP_LOG_INTERVAL_SECONDS = 5.0
|
|
62
|
+
|
|
48
63
|
_INIT_FILENAME = "init.mp4"
|
|
49
64
|
# Written into a recording's directory once it is finished, so its final segment
|
|
50
65
|
# (which has no successor to prove it closed) is recognised as fetchable.
|
|
@@ -72,6 +87,23 @@ _AudioArray = npt.NDArray[Any]
|
|
|
72
87
|
_FeedItem = tuple[npt.NDArray[Any], _AudioArray | None]
|
|
73
88
|
|
|
74
89
|
|
|
90
|
+
class _Slot(Enum):
|
|
91
|
+
"""The answer to asking the feed queue for room for one grid frame.
|
|
92
|
+
|
|
93
|
+
Separates the two reasons room is refused, because only one of them is a
|
|
94
|
+
fact about the recording's health: frames abandoned because the encoder is
|
|
95
|
+
behind are the recording losing media, while frames abandoned because the
|
|
96
|
+
recording is stopping are teardown and say nothing about the encoder.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
TAKEN = "taken"
|
|
100
|
+
"""There is room; the frame can be queued."""
|
|
101
|
+
FULL = "full"
|
|
102
|
+
"""The encoder is behind, and any wait the chunk asked for has run out."""
|
|
103
|
+
WINDING_DOWN = "winding_down"
|
|
104
|
+
"""The recording is stopping, so the rest of the emission is moot."""
|
|
105
|
+
|
|
106
|
+
|
|
75
107
|
class RecorderError(Exception):
|
|
76
108
|
"""Base for a clip request the recorder cannot serve."""
|
|
77
109
|
|
|
@@ -205,7 +237,13 @@ class Recorder:
|
|
|
205
237
|
self._started = False
|
|
206
238
|
self._disabled = False
|
|
207
239
|
|
|
208
|
-
|
|
240
|
+
# The queue is unbounded in itself; the depth is the bound, checked as
|
|
241
|
+
# each frame is queued. The effective capacity never sits below one
|
|
242
|
+
# emission, so a batching model always fits a whole emission.
|
|
243
|
+
self._feed_queue: queue.Queue[_FeedItem | None] = queue.Queue()
|
|
244
|
+
# Signalled by the feed worker after each dequeue so a producer waiting
|
|
245
|
+
# for room sleeps until the encoder takes a frame instead of polling.
|
|
246
|
+
self._feed_room = threading.Condition()
|
|
209
247
|
self._feed_thread: threading.Thread | None = None
|
|
210
248
|
self._feed_stop = threading.Event()
|
|
211
249
|
self._watch_thread: threading.Thread | None = None
|
|
@@ -227,6 +265,9 @@ class Recorder:
|
|
|
227
265
|
self._chunk_lock = threading.Lock()
|
|
228
266
|
|
|
229
267
|
self._dropped_frames = 0
|
|
268
|
+
# When the feed last warned about dropped frames. Zero until it has, so
|
|
269
|
+
# the first loss of a session is always reported.
|
|
270
|
+
self._dropped_logged_at = 0.0
|
|
230
271
|
# Fractional grid frames carried between chunks so resampling a chunk's
|
|
231
272
|
# own rate onto the fixed recording grid accumulates no rounding drift.
|
|
232
273
|
self._grid_debt = 0.0
|
|
@@ -311,8 +352,11 @@ class Recorder:
|
|
|
311
352
|
return
|
|
312
353
|
self._disabled = True
|
|
313
354
|
self._feed_stop.set()
|
|
314
|
-
|
|
315
|
-
|
|
355
|
+
self._feed_queue.put_nowait(None)
|
|
356
|
+
# Release a producer parked on a full queue, so teardown cannot wait
|
|
357
|
+
# behind an encoder that has already stopped draining.
|
|
358
|
+
with self._feed_room:
|
|
359
|
+
self._feed_room.notify_all()
|
|
316
360
|
feed_thread = self._feed_thread
|
|
317
361
|
self._feed_thread = None
|
|
318
362
|
if self._encoder is not None:
|
|
@@ -410,6 +454,7 @@ class Recorder:
|
|
|
410
454
|
self._audio_sample_rate = 48_000
|
|
411
455
|
self._has_audio = False
|
|
412
456
|
self._dropped_frames = 0
|
|
457
|
+
self._dropped_logged_at = 0.0
|
|
413
458
|
self._grid_debt = 0.0
|
|
414
459
|
self._audio_jitter_buf = []
|
|
415
460
|
self._audio_buffered_samples = 0
|
|
@@ -423,7 +468,7 @@ class Recorder:
|
|
|
423
468
|
# -- media fan-out tap ----------------------------------------------------
|
|
424
469
|
|
|
425
470
|
def on_chunk(self, chunk: MediaChunk) -> None:
|
|
426
|
-
"""Feed one emitted media chunk to the recording
|
|
471
|
+
"""Feed one emitted media chunk to the recording.
|
|
427
472
|
|
|
428
473
|
Called on the model thread by the runner's media fan-out. The chunk's
|
|
429
474
|
frames are resampled from the chunk's own rate onto the fixed recording
|
|
@@ -433,8 +478,17 @@ class Recorder:
|
|
|
433
478
|
duration. Fractional grid frames carry across chunks so the resampling
|
|
434
479
|
accumulates no drift. The timeline advances by the media actually fed,
|
|
435
480
|
not by wall-clock, so a pause simply stops advancing rather than
|
|
436
|
-
recording dead air.
|
|
437
|
-
|
|
481
|
+
recording dead air.
|
|
482
|
+
|
|
483
|
+
The feed queue holds at least a whole emission, so handing a batch over
|
|
484
|
+
costs the model thread nothing while the encoder keeps up. Once the
|
|
485
|
+
queue is full the chunk decides, exactly as it does at the wire: a chunk
|
|
486
|
+
that asks for backpressure (``chunk.wait``) makes this call wait for
|
|
487
|
+
room, bounded by :data:`_FEED_WAIT_SECONDS` across the emission, and one
|
|
488
|
+
that prefers skipping has its overflow dropped and counted. Only the
|
|
489
|
+
encoder falling behind is counted: an emission cut short because the
|
|
490
|
+
recording is stopping is teardown, and reporting it would put phantom
|
|
491
|
+
losses on the summary :meth:`stop` logs.
|
|
438
492
|
"""
|
|
439
493
|
if self.disabled:
|
|
440
494
|
return
|
|
@@ -456,24 +510,69 @@ class Recorder:
|
|
|
456
510
|
grid_frames = int(self._grid_debt)
|
|
457
511
|
self._grid_debt -= grid_frames
|
|
458
512
|
audio_target = round(self._audio_sample_rate / RECORDING_FPS) if self._has_audio else 0
|
|
513
|
+
# One authoritative bound, never below the emission being queued, so the
|
|
514
|
+
# burst a batching model hands over always fits.
|
|
515
|
+
capacity = max(_FEED_DEPTH, grid_frames)
|
|
516
|
+
deadline = time.monotonic() + _FEED_WAIT_SECONDS
|
|
459
517
|
fed = 0
|
|
518
|
+
outcome = _Slot.TAKEN
|
|
460
519
|
for i in range(grid_frames):
|
|
520
|
+
outcome = self._claim_slot(capacity, wait=chunk.wait, deadline=deadline)
|
|
521
|
+
if outcome is not _Slot.TAKEN:
|
|
522
|
+
break
|
|
461
523
|
video_data = frames[i * len(frames) // grid_frames]
|
|
462
524
|
audio_data = self._take_audio(audio_target) if self._has_audio else None
|
|
463
|
-
|
|
464
|
-
self._feed_queue.put_nowait((video_data, audio_data))
|
|
465
|
-
except queue.Full:
|
|
466
|
-
self._dropped_frames += 1
|
|
467
|
-
if self._dropped_frames == 1 or self._dropped_frames % 300 == 0:
|
|
468
|
-
logger.warning(
|
|
469
|
-
"recorder feed queue full; dropping a frame to keep the model unblocked",
|
|
470
|
-
dropped_total=self._dropped_frames,
|
|
471
|
-
)
|
|
472
|
-
break
|
|
525
|
+
self._feed_queue.put_nowait((video_data, audio_data))
|
|
473
526
|
fed += 1
|
|
527
|
+
if outcome is _Slot.FULL:
|
|
528
|
+
self._count_dropped(grid_frames - fed, grid_frames)
|
|
474
529
|
if fed:
|
|
475
530
|
markers.advance(fed / RECORDING_FPS)
|
|
476
531
|
|
|
532
|
+
def _claim_slot(self, capacity: int, *, wait: bool, deadline: float) -> _Slot:
|
|
533
|
+
"""Ask the feed queue for room for one more grid frame.
|
|
534
|
+
|
|
535
|
+
A chunk that asks for backpressure waits for the encoder to take a
|
|
536
|
+
frame, until *deadline* passes; one that does not reports the full queue
|
|
537
|
+
immediately so its caller can drop. A recording that is stopping answers
|
|
538
|
+
:attr:`_Slot.WINDING_DOWN` on either path, so teardown is never counted
|
|
539
|
+
against the encoder.
|
|
540
|
+
"""
|
|
541
|
+
if self._feed_queue.qsize() < capacity:
|
|
542
|
+
return _Slot.TAKEN
|
|
543
|
+
if self._feed_stop.is_set() or self._disabled:
|
|
544
|
+
return _Slot.WINDING_DOWN
|
|
545
|
+
if not wait:
|
|
546
|
+
return _Slot.FULL
|
|
547
|
+
with self._feed_room:
|
|
548
|
+
while self._feed_queue.qsize() >= capacity:
|
|
549
|
+
if self._feed_stop.is_set() or self._disabled:
|
|
550
|
+
return _Slot.WINDING_DOWN
|
|
551
|
+
remaining = deadline - time.monotonic()
|
|
552
|
+
if remaining <= 0:
|
|
553
|
+
return _Slot.FULL
|
|
554
|
+
self._feed_room.wait(timeout=min(remaining, 0.1))
|
|
555
|
+
return _Slot.TAKEN
|
|
556
|
+
|
|
557
|
+
def _count_dropped(self, dropped: int, offered: int) -> None:
|
|
558
|
+
"""Record the frames an emission could not hand over, and say so.
|
|
559
|
+
|
|
560
|
+
Every dropped frame reaches the count, which the recording reports when
|
|
561
|
+
it stops; the warning itself is rate-limited so an encoder that stays
|
|
562
|
+
behind reports the loss periodically rather than once per emission.
|
|
563
|
+
"""
|
|
564
|
+
self._dropped_frames += dropped
|
|
565
|
+
now = time.monotonic()
|
|
566
|
+
if now - self._dropped_logged_at < _DROP_LOG_INTERVAL_SECONDS:
|
|
567
|
+
return
|
|
568
|
+
self._dropped_logged_at = now
|
|
569
|
+
logger.warning(
|
|
570
|
+
"recorder feed queue full; dropping frames from an emission",
|
|
571
|
+
dropped=dropped,
|
|
572
|
+
offered=offered,
|
|
573
|
+
dropped_total=self._dropped_frames,
|
|
574
|
+
)
|
|
575
|
+
|
|
477
576
|
def _video_frames(self, bundle: MediaBundle) -> list[npt.NDArray[Any]]:
|
|
478
577
|
"""Split the recorded video track into single ``(H, W, 3)`` frames.
|
|
479
578
|
|
|
@@ -537,6 +636,10 @@ class Recorder:
|
|
|
537
636
|
item = self._feed_queue.get(timeout=0.1)
|
|
538
637
|
except queue.Empty:
|
|
539
638
|
continue
|
|
639
|
+
# Room opened the moment the frame left the queue, so a producer
|
|
640
|
+
# waiting on capacity is released before the encode, not after it.
|
|
641
|
+
with self._feed_room:
|
|
642
|
+
self._feed_room.notify_all()
|
|
540
643
|
if item is None:
|
|
541
644
|
return
|
|
542
645
|
encoder = self._encoder
|
|
@@ -598,12 +701,14 @@ class Recorder:
|
|
|
598
701
|
return out.reshape(1, -1)
|
|
599
702
|
|
|
600
703
|
def _drain_feed_queue(self) -> None:
|
|
601
|
-
"""Discard every queued feed item."""
|
|
704
|
+
"""Discard every queued feed item, releasing anyone waiting for room."""
|
|
602
705
|
while True:
|
|
603
706
|
try:
|
|
604
707
|
self._feed_queue.get_nowait()
|
|
605
708
|
except queue.Empty:
|
|
606
|
-
|
|
709
|
+
break
|
|
710
|
+
with self._feed_room:
|
|
711
|
+
self._feed_room.notify_all()
|
|
607
712
|
|
|
608
713
|
# -- clip / recording requests --------------------------------------------
|
|
609
714
|
|
|
@@ -888,17 +888,27 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
888
888
|
def _emit_media(self, chunk: MediaChunk) -> None:
|
|
889
889
|
"""Fan one emitted media chunk out to the recorder and the connections.
|
|
890
890
|
|
|
891
|
-
Called off the model loop (emit dispatches to a worker thread).
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
producer
|
|
895
|
-
|
|
891
|
+
Called off the model loop (emit dispatches to a worker thread). Both
|
|
892
|
+
consumers bound their queue the same way — never below the emission
|
|
893
|
+
being handed over — so a whole chunk fits each of them and the fan-out
|
|
894
|
+
costs the producer nothing while they keep up. A consumer that falls
|
|
895
|
+
behind honours ``chunk.wait``, and a chunk emitted with ``drop=True``
|
|
896
|
+
leaves every consumer non-blocking.
|
|
897
|
+
|
|
898
|
+
The connections are served first so the archive is never in front of
|
|
899
|
+
the session. A pacer that makes the producer wait is throttling it to
|
|
900
|
+
the playout rate it asked for, and drains on its own thread meanwhile;
|
|
901
|
+
the recorder's wait is bounded instead, because an encoder can stall
|
|
902
|
+
outright. Feeding the recorder second keeps that bounded stall off the
|
|
903
|
+
live path, and leaves its queue the whole broadcast to drain into.
|
|
896
904
|
"""
|
|
897
905
|
for track in chunk.bundle.tracks:
|
|
898
906
|
self._model_metrics.emitted(track, chunk.n_frames)
|
|
899
|
-
self._recorder.on_chunk(chunk)
|
|
900
907
|
generation = self._media_generation
|
|
901
908
|
self._connections.broadcast_media(chunk, abort=lambda: self._media_generation != generation)
|
|
909
|
+
# The archive takes the whole chunk even when a flush cut the broadcast
|
|
910
|
+
# short: a playout cut is not an archive boundary.
|
|
911
|
+
self._recorder.on_chunk(chunk)
|
|
902
912
|
|
|
903
913
|
def _flush_media(self) -> None:
|
|
904
914
|
"""Drop queued media in every connection and cut playout to black.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/decorators.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/errors.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/events/messages.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/internal/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/internal/bridge.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/contract.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/reactor_model.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/model/schema.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/pipeline/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/pipeline/idle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/descriptors.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/input.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/interface/tracks/output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/recording/chunk_encoder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/runner/connection_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/acceptor.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/config.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/connection.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/frames.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/pacer.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/peer.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/router.py
RENAMED
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/signaling.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/stats.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.2 → reactor_runtime-3.2.4}/src/reactor_runtime/transport/webrtc/version.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|