reactor-runtime 2.7.2__tar.gz → 2.7.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-2.7.2 → reactor_runtime-2.7.4}/PKG-INFO +17 -17
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/pyproject.toml +21 -18
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/config.py +5 -4
- reactor_runtime-2.7.4/src/reactor_runtime/recording/markers.py +86 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/session_recorder.py +45 -125
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/http_runtime.py +6 -0
- reactor_runtime-2.7.4/src/reactor_runtime/serve/__main__.py +13 -0
- reactor_runtime-2.7.4/src/reactor_runtime/utils/launch.py +200 -0
- reactor_runtime-2.7.4/src/reactor_runtime/utils/log.py +424 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/PKG-INFO +17 -17
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/SOURCES.txt +1 -0
- reactor_runtime-2.7.4/src/reactor_runtime.egg-info/requires.txt +21 -0
- reactor_runtime-2.7.2/src/reactor_runtime/recording/markers.py +0 -115
- reactor_runtime-2.7.2/src/reactor_runtime/utils/launch.py +0 -107
- reactor_runtime-2.7.2/src/reactor_runtime/utils/log.py +0 -148
- reactor_runtime-2.7.2/src/reactor_runtime.egg-info/requires.txt +0 -21
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/README.md +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/setup.cfg +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/api/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/config.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/defaults.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/driver/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/driver/step_result.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/connected.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/event.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/upload.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/decorators.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/handlers.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/upload.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/model_state.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/base.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/file.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/helpers.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/profiler.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/singleton.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/sinks.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/track_resolver.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtime_api.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/headless/config.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/headless/headless_runtime.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/config.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/types.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/schema_validator.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/commands/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/commands/run.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/commands/schema.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/main.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/utils/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/utils/config.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/utils/runtime.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/client.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/config.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/events.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/client.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/ice_uris.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/interface.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/media.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/types.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/loader.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/messages.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/paths.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/ports.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/typing.py +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
- {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/top_level.txt +0 -0
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor_runtime
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.4
|
|
4
4
|
Summary: Reactor runtime with public model API
|
|
5
5
|
Author-email: Reactor <team@reactor.inc>
|
|
6
6
|
Requires-Python: >=3.9
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: numpy>=
|
|
9
|
-
Requires-Dist: pydantic>=2.
|
|
8
|
+
Requires-Dist: numpy>=2.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.13.0
|
|
10
10
|
Requires-Dist: omegaconf>=2.3.0
|
|
11
11
|
Requires-Dist: av>=14.0.0
|
|
12
12
|
Requires-Dist: aiortc>=1.14.0
|
|
13
|
-
Requires-Dist: fastapi>=0.
|
|
14
|
-
Requires-Dist: uvicorn[standard]>=0.
|
|
15
|
-
Requires-Dist: aiohttp>=3.
|
|
16
|
-
Requires-Dist: httpx>=0.
|
|
17
|
-
Requires-Dist: redis
|
|
18
|
-
Requires-Dist: jsonschema>=4.
|
|
19
|
-
Requires-Dist: opentelemetry-api~=1.
|
|
20
|
-
Requires-Dist: opentelemetry-sdk~=1.
|
|
21
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.
|
|
22
|
-
Requires-Dist: opentelemetry-exporter-prometheus~=0.
|
|
23
|
-
Requires-Dist: grpcio>=1.
|
|
24
|
-
Requires-Dist: grpcio-health-checking
|
|
25
|
-
Requires-Dist: opentelemetry-instrumentation-grpc~=0.
|
|
13
|
+
Requires-Dist: fastapi>=0.136.1
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.47.0
|
|
15
|
+
Requires-Dist: aiohttp>=3.13.0
|
|
16
|
+
Requires-Dist: httpx>=0.28.0
|
|
17
|
+
Requires-Dist: redis>=7.0.0
|
|
18
|
+
Requires-Dist: jsonschema>=4.26.0
|
|
19
|
+
Requires-Dist: opentelemetry-api~=1.42
|
|
20
|
+
Requires-Dist: opentelemetry-sdk~=1.42
|
|
21
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.42
|
|
22
|
+
Requires-Dist: opentelemetry-exporter-prometheus~=0.63b0
|
|
23
|
+
Requires-Dist: grpcio>=1.80.0
|
|
24
|
+
Requires-Dist: grpcio-health-checking>=1.80.0
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-grpc~=0.63b0
|
|
26
26
|
Provides-Extra: gst
|
|
27
|
-
Requires-Dist: PyGObject>=3.
|
|
27
|
+
Requires-Dist: PyGObject>=3.56.0; extra == "gst"
|
|
28
28
|
|
|
29
29
|
# Reactor Runtime
|
|
30
30
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactor_runtime"
|
|
7
|
-
version = "2.7.
|
|
7
|
+
version = "2.7.4"
|
|
8
8
|
description = "Reactor runtime with public model API"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Reactor", email = "team@reactor.inc" }
|
|
@@ -13,29 +13,32 @@ readme = "README.md"
|
|
|
13
13
|
requires-python = ">=3.9"
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
"numpy>=2.0.0",
|
|
17
|
+
"pydantic>=2.13.0",
|
|
18
|
+
"omegaconf>=2.3.0",
|
|
19
|
+
# PyAV is upper-bounded transitively by aiortc (<17). Keep our floor
|
|
20
|
+
# aligned with aiortc's own floor so any direct `av` resolution stays
|
|
21
|
+
# inside the supported range.
|
|
19
22
|
"av>=14.0.0",
|
|
20
23
|
"aiortc>=1.14.0",
|
|
21
|
-
"fastapi>=0.
|
|
22
|
-
"uvicorn[standard]>=0.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
"fastapi>=0.136.1",
|
|
25
|
+
"uvicorn[standard]>=0.47.0",
|
|
26
|
+
"aiohttp>=3.13.0",
|
|
27
|
+
"httpx>=0.28.0",
|
|
28
|
+
"redis>=7.0.0",
|
|
29
|
+
"jsonschema>=4.26.0",
|
|
30
|
+
"opentelemetry-api~=1.42",
|
|
31
|
+
"opentelemetry-sdk~=1.42",
|
|
32
|
+
"opentelemetry-exporter-otlp-proto-http~=1.42",
|
|
33
|
+
"opentelemetry-exporter-prometheus~=0.63b0",
|
|
34
|
+
"grpcio>=1.80.0",
|
|
35
|
+
"grpcio-health-checking>=1.80.0",
|
|
36
|
+
"opentelemetry-instrumentation-grpc~=0.63b0",
|
|
34
37
|
]
|
|
35
38
|
|
|
36
39
|
[project.optional-dependencies]
|
|
37
40
|
gst = [
|
|
38
|
-
|
|
41
|
+
"PyGObject>=3.56.0",
|
|
39
42
|
]
|
|
40
43
|
|
|
41
44
|
# No `[project.scripts]` entry. The runtime is launched via
|
|
@@ -91,10 +91,11 @@ class RecordingConfig:
|
|
|
91
91
|
# ``reactor.yaml``.
|
|
92
92
|
chunk_seconds: int = 4
|
|
93
93
|
clip_max_seconds: int = 300
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
94
|
+
# Drop OutputBuffer gap-fill duplicates before the first real frame
|
|
95
|
+
# and anchor the recording timeline (markers + ffmpeg chunk 0) at
|
|
96
|
+
# that frame. Clip requests before any real frame fail with
|
|
97
|
+
# ``no media generated yet``. Set to ``False`` to record from
|
|
98
|
+
# session arm time and include the leading pre-roll in clips.
|
|
98
99
|
skip_leading_black: bool = True
|
|
99
100
|
video_track: Optional[str] = None
|
|
100
101
|
audio_track: Optional[str] = None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
|
|
2
|
+
"""Wall-clock marker bookkeeping for the recording subsystem.
|
|
3
|
+
|
|
4
|
+
Markers are wall-clock seconds aligned with ffmpeg's
|
|
5
|
+
``-use_wallclock_as_timestamps`` input. When ``anchor_at_first_frame``
|
|
6
|
+
is set (``RecordingConfig.skip_leading_black``), ``t=0`` is the first
|
|
7
|
+
real frame fed to the encoder, not recorder arm time.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from typing import Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MarkerBookkeeper:
|
|
18
|
+
"""Tracks ``now_marker`` and the latest fully-uploaded chunk index.
|
|
19
|
+
|
|
20
|
+
Thread-safe — ``mark_chunk_uploaded`` is called from the uploader
|
|
21
|
+
thread while ``compute_*_range`` is called from the runtime's
|
|
22
|
+
asyncio loop on the inbound runtime-message dispatch path.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, anchor_at_first_frame: bool = False) -> None:
|
|
26
|
+
self._anchor_at_first_frame = anchor_at_first_frame
|
|
27
|
+
self._session_start = time.monotonic()
|
|
28
|
+
self._last_uploaded_chunk_idx: int = -1
|
|
29
|
+
self._recording_start: Optional[float] = None
|
|
30
|
+
self._first_real_frame_marker: Optional[float] = None
|
|
31
|
+
self._lock = threading.Lock()
|
|
32
|
+
|
|
33
|
+
def now_marker(self) -> float:
|
|
34
|
+
"""Seconds since the active timeline origin."""
|
|
35
|
+
with self._lock:
|
|
36
|
+
origin = (
|
|
37
|
+
self._recording_start
|
|
38
|
+
if self._recording_start is not None
|
|
39
|
+
else self._session_start
|
|
40
|
+
)
|
|
41
|
+
return time.monotonic() - origin
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def last_uploaded_chunk_idx(self) -> int:
|
|
45
|
+
"""Index of the most recent chunk known to be in the sink, or -1."""
|
|
46
|
+
with self._lock:
|
|
47
|
+
return self._last_uploaded_chunk_idx
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def first_real_frame_marker(self) -> Optional[float]:
|
|
51
|
+
"""Session-relative time of the first real frame, or ``None``."""
|
|
52
|
+
with self._lock:
|
|
53
|
+
return self._first_real_frame_marker
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def recording_started(self) -> bool:
|
|
57
|
+
"""False until the first real frame when ``anchor_at_first_frame``."""
|
|
58
|
+
if not self._anchor_at_first_frame:
|
|
59
|
+
return True
|
|
60
|
+
with self._lock:
|
|
61
|
+
return self._recording_start is not None
|
|
62
|
+
|
|
63
|
+
def mark_chunk_uploaded(self, idx: int) -> None:
|
|
64
|
+
"""Advance ``last_uploaded_chunk_idx`` monotonically."""
|
|
65
|
+
with self._lock:
|
|
66
|
+
if idx > self._last_uploaded_chunk_idx:
|
|
67
|
+
self._last_uploaded_chunk_idx = idx
|
|
68
|
+
|
|
69
|
+
def mark_first_real_frame(self) -> None:
|
|
70
|
+
"""Latch the first real frame; re-anchor timeline when configured."""
|
|
71
|
+
with self._lock:
|
|
72
|
+
if self._first_real_frame_marker is None:
|
|
73
|
+
self._first_real_frame_marker = time.monotonic() - self._session_start
|
|
74
|
+
if self._anchor_at_first_frame and self._recording_start is None:
|
|
75
|
+
self._recording_start = time.monotonic()
|
|
76
|
+
|
|
77
|
+
def compute_clip_range(self, duration_seconds: float) -> Tuple[float, float]:
|
|
78
|
+
"""``(start_marker, end_marker)`` for a snap clip ending at ``now_marker``."""
|
|
79
|
+
end_marker = self.now_marker()
|
|
80
|
+
start_marker = max(0.0, end_marker - duration_seconds)
|
|
81
|
+
return start_marker, end_marker
|
|
82
|
+
|
|
83
|
+
def compute_recording_range(self) -> Tuple[float, float]:
|
|
84
|
+
"""``(0, now_marker)`` for a full-session recording request."""
|
|
85
|
+
end_marker = self.now_marker()
|
|
86
|
+
return 0.0, end_marker
|
{reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/session_recorder.py
RENAMED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
# Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from the recorder's local state (no Supervisor RPC at clip-request
|
|
13
|
-
time).
|
|
2
|
+
"""Per-session recorder: encoder, uploader, markers, and sink.
|
|
3
|
+
|
|
4
|
+
The runtime registers ``_on_bundle_sync`` on the model's
|
|
5
|
+
:class:`~reactor_runtime.interface.output_buffer.OutputBuffer`.
|
|
6
|
+
With ``skip_leading_black`` (default), gap-fill duplicates before the
|
|
7
|
+
first real frame are dropped; after that, keepalive duplicates may be
|
|
8
|
+
fed at a reduced rate during model idle periods.
|
|
9
|
+
|
|
10
|
+
``request_clip`` / ``request_recording`` resolve from local marker
|
|
11
|
+
state and return immediately (the SDK polls ``/clips`` for chunks).
|
|
14
12
|
"""
|
|
15
13
|
|
|
16
14
|
from __future__ import annotations
|
|
@@ -47,6 +45,14 @@ class RecorderDisabledError(RuntimeError):
|
|
|
47
45
|
"""
|
|
48
46
|
|
|
49
47
|
|
|
48
|
+
class NoMediaYetError(RecorderDisabledError):
|
|
49
|
+
"""``request_clip`` / ``request_recording`` before the first real frame.
|
|
50
|
+
|
|
51
|
+
Distinct subclass so callers can disambiguate from encoder-crash
|
|
52
|
+
failures without string matching.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
50
56
|
@dataclass(frozen=True)
|
|
51
57
|
class ClipResult:
|
|
52
58
|
"""Resolved outcome of a ``requestClip`` / ``requestRecording`` call.
|
|
@@ -59,8 +65,9 @@ class ClipResult:
|
|
|
59
65
|
``202 Retry-After`` while that chunk is in flight; the SDK polls
|
|
60
66
|
until ``200``.
|
|
61
67
|
|
|
62
|
-
``start_marker`` / ``end_marker`` / ``now_marker`` are
|
|
63
|
-
|
|
68
|
+
``start_marker`` / ``end_marker`` / ``now_marker`` are seconds on the
|
|
69
|
+
recording timeline (``t=0`` at the first real frame when
|
|
70
|
+
``skip_leading_black`` is on). ``predicted_ready_at_ms``
|
|
64
71
|
is a **Unix epoch in milliseconds** — the runtime's estimate of
|
|
65
72
|
when the clip will be servable by ``/clips``. An epoch lets the
|
|
66
73
|
client decide independently whether to keep polling (epoch is
|
|
@@ -86,10 +93,6 @@ class ClipResult:
|
|
|
86
93
|
now_marker: float
|
|
87
94
|
predicted_ready_at_ms: int
|
|
88
95
|
playlist_url: str
|
|
89
|
-
# Wall-clock of the first real (non-duplicate) frame the recorder
|
|
90
|
-
# observed, or ``None`` if none has landed yet. Surfaced for
|
|
91
|
-
# observability; ``start_marker`` already uses it when
|
|
92
|
-
# ``RecordingConfig.skip_leading_black`` is on.
|
|
93
96
|
first_real_frame_marker: Optional[float] = None
|
|
94
97
|
|
|
95
98
|
def to_dict(self) -> dict:
|
|
@@ -134,7 +137,9 @@ class SessionRecorder:
|
|
|
134
137
|
self._work_dir = Path(work_dir)
|
|
135
138
|
self._work_dir.mkdir(parents=True, exist_ok=True)
|
|
136
139
|
|
|
137
|
-
self._markers = MarkerBookkeeper(
|
|
140
|
+
self._markers = MarkerBookkeeper(
|
|
141
|
+
anchor_at_first_frame=config.skip_leading_black
|
|
142
|
+
)
|
|
138
143
|
self._encoder = ChunkEncoder(
|
|
139
144
|
output_dir=self._work_dir,
|
|
140
145
|
config=config,
|
|
@@ -151,10 +156,6 @@ class SessionRecorder:
|
|
|
151
156
|
self._started = False
|
|
152
157
|
self._disabled = False
|
|
153
158
|
|
|
154
|
-
# Bounded feed queue decouples the OutputBuffer emission thread
|
|
155
|
-
# from ffmpeg's blocking ``os.write``: when the encoder
|
|
156
|
-
# backpressures we drop from the *recording*, never the wire.
|
|
157
|
-
# Small on purpose — we want recent loss, not stale buffering.
|
|
158
159
|
self._feed_queue: "queue.Queue[Optional[Tuple[np.ndarray, Optional[np.ndarray]]]]" = queue.Queue(
|
|
159
160
|
maxsize=4
|
|
160
161
|
)
|
|
@@ -162,27 +163,9 @@ class SessionRecorder:
|
|
|
162
163
|
self._feed_stop = threading.Event()
|
|
163
164
|
self._dropped_frames: int = 0
|
|
164
165
|
|
|
165
|
-
# Keepalive throttle for gap-fill duplicates. Real frames are
|
|
166
|
-
# always passed through; duplicates (the OutputBuffer's own
|
|
167
|
-
# gap-fills when the model yields ``Idle`` or its inference
|
|
168
|
-
# generator stalls) are passed through at most once every
|
|
169
|
-
# ``keepalive_interval`` seconds — enough to keep ffmpeg's
|
|
170
|
-
# wallclock-stamped PTS advancing across HLS chunk boundaries
|
|
171
|
-
# so the marker math holds during long model pauses, without
|
|
172
|
-
# paying full-rate I/O for visually-identical frames.
|
|
173
166
|
self._keepalive_interval = max(0.0, float(keepalive_interval))
|
|
174
167
|
self._last_fed_t: float = 0.0
|
|
175
|
-
# Wall-clock of the previous video frame fed to the encoder.
|
|
176
|
-
# Used to compute the audio-fill sample count per video tick
|
|
177
|
-
# (``audio_sample_rate * dt``), which keeps audio DTS tracking
|
|
178
|
-
# video PTS 1:1 regardless of the model's instantaneous FPS.
|
|
179
168
|
self._last_video_wall_t: float = 0.0
|
|
180
|
-
|
|
181
|
-
# Audio jitter buffer: model audio batches are variably sized;
|
|
182
|
-
# we accumulate them and emit ``round(sample_rate * dt)``
|
|
183
|
-
# samples per video tick (silence-pad on underrun, queue the
|
|
184
|
-
# surplus on overrun). Capped at ~1 s so the recording can't
|
|
185
|
-
# accumulate unbounded audio latency.
|
|
186
169
|
self._audio_jitter_buf: List[np.ndarray] = []
|
|
187
170
|
self._audio_buffered_samples: int = 0
|
|
188
171
|
self._audio_buffer_cap_samples: int = self._audio_sample_rate
|
|
@@ -230,32 +213,20 @@ class SessionRecorder:
|
|
|
230
213
|
"""
|
|
231
214
|
if not self._started:
|
|
232
215
|
return
|
|
233
|
-
# Disable callbacks first so a racing _on_bundle_sync becomes
|
|
234
|
-
# a no-op even before its OutputBuffer subscription is removed.
|
|
235
216
|
self._disabled = True
|
|
236
|
-
# Signal the feed loop to drain and exit; sentinel unblocks .get().
|
|
237
217
|
self._feed_stop.set()
|
|
238
218
|
try:
|
|
239
219
|
self._feed_queue.put_nowait(None)
|
|
240
220
|
except queue.Full:
|
|
241
|
-
# Queue full → worker will see _feed_stop on its next iteration.
|
|
242
221
|
pass
|
|
243
222
|
feed_thread = self._feed_thread
|
|
244
223
|
self._feed_thread = None
|
|
245
|
-
# Stop the encoder before joining the feed worker: the worker's
|
|
246
|
-
# blocking call is ``os.write()`` into the ffmpeg pipe and only
|
|
247
|
-
# unblocks when the pipe is closed.
|
|
248
224
|
self._encoder.stop()
|
|
249
225
|
if feed_thread is not None:
|
|
250
226
|
feed_thread.join(timeout=2.0)
|
|
251
227
|
feed_leaked = feed_thread is not None and feed_thread.is_alive()
|
|
252
228
|
uploader_clean = self._uploader.stop(flush_remaining=True)
|
|
253
229
|
self._started = False
|
|
254
|
-
# Cleanup the encoder's transient working dir. Chunks that
|
|
255
|
-
# made it to the sink are already copied to the served dir;
|
|
256
|
-
# whatever's left here is leftover ffmpeg scratch (manifest.mpd,
|
|
257
|
-
# un-promoted .tmp segments, etc.) that we don't need. Done
|
|
258
|
-
# AFTER ffmpeg exits so the muxer can't trip on the rmtree.
|
|
259
230
|
try:
|
|
260
231
|
shutil.rmtree(self._work_dir, ignore_errors=True)
|
|
261
232
|
except Exception:
|
|
@@ -280,37 +251,18 @@ class SessionRecorder:
|
|
|
280
251
|
# ------------------------------------------------------------------
|
|
281
252
|
|
|
282
253
|
def _on_bundle_sync(self, bundle: MediaBundle, duplicate: bool) -> None:
|
|
283
|
-
"""
|
|
284
|
-
|
|
285
|
-
Non-blocking: pushes onto the feed queue and returns. The
|
|
286
|
-
``recording-feed`` worker thread does the blocking ``os.write``
|
|
287
|
-
to ffmpeg.
|
|
288
|
-
|
|
289
|
-
**Real frames** (``duplicate=False``) are always passed through.
|
|
290
|
-
|
|
291
|
-
**Duplicates** (``duplicate=True``) are the OutputBuffer's
|
|
292
|
-
gap-fill emissions when the model yields :data:`Idle` or
|
|
293
|
-
otherwise fails to emit on a tick. We rate-limit them to one
|
|
294
|
-
per ``_keepalive_interval`` seconds: enough to keep ffmpeg's
|
|
295
|
-
wallclock-stamped PTS advancing across HLS chunk boundaries
|
|
296
|
-
(so ``last_uploaded_chunk_idx`` keeps progressing during a
|
|
297
|
-
long model pause and the marker math holds), but cheap enough
|
|
298
|
-
not to burn full-rate pipe I/O on visually-identical frames.
|
|
299
|
-
"""
|
|
254
|
+
"""Enqueue a bundle for the feed worker; non-blocking."""
|
|
300
255
|
if self._disabled or not self._started:
|
|
301
256
|
return
|
|
302
257
|
now = time.monotonic()
|
|
303
|
-
if not duplicate:
|
|
304
|
-
# Idempotent latch; only the first call takes effect.
|
|
305
|
-
self._markers.mark_first_real_frame()
|
|
306
258
|
if duplicate:
|
|
259
|
+
if self._config.skip_leading_black and not self._markers.recording_started:
|
|
260
|
+
return
|
|
307
261
|
if (
|
|
308
262
|
self._keepalive_interval <= 0.0
|
|
309
263
|
or (now - self._last_fed_t) < self._keepalive_interval
|
|
310
264
|
):
|
|
311
265
|
return
|
|
312
|
-
# Resolve tracks on the emission thread (cheap dict lookups);
|
|
313
|
-
# only the heavy os.write happens on the feed worker.
|
|
314
266
|
video_td = bundle.tracks.get(self._video_track)
|
|
315
267
|
if video_td is None:
|
|
316
268
|
videos = bundle.get_tracks_by_kind(TrackKind.VIDEO)
|
|
@@ -322,12 +274,15 @@ class SessionRecorder:
|
|
|
322
274
|
audio_td = bundle.tracks.get(self._audio_track)
|
|
323
275
|
if audio_td is not None:
|
|
324
276
|
audio_data = audio_td.data
|
|
325
|
-
# The OutputBuffer's cache is per-model, so the first
|
|
326
|
-
# duplicates of a new session can carry the previous session's
|
|
327
|
-
# last frame. Black-fill them until a real frame lands so the
|
|
328
|
-
# leading pre-roll is genuinely black rather than stale content.
|
|
329
277
|
video_data = video_td.data
|
|
330
|
-
if
|
|
278
|
+
if (
|
|
279
|
+
duplicate
|
|
280
|
+
and not self._config.skip_leading_black
|
|
281
|
+
and self._markers.first_real_frame_marker is None
|
|
282
|
+
):
|
|
283
|
+
# Legacy mode still records pre-roll; zero-fill so the
|
|
284
|
+
# OutputBuffer's per-model cache cannot leak the previous
|
|
285
|
+
# session's last frame into chunk 0.
|
|
331
286
|
video_data = np.zeros_like(video_data)
|
|
332
287
|
if audio_data is not None:
|
|
333
288
|
audio_data = np.zeros_like(audio_data)
|
|
@@ -335,33 +290,18 @@ class SessionRecorder:
|
|
|
335
290
|
self._feed_queue.put_nowait((video_data, audio_data))
|
|
336
291
|
except queue.Full:
|
|
337
292
|
self._dropped_frames += 1
|
|
338
|
-
# Cold-start can fill the queue for a few hundred frames;
|
|
339
|
-
# throttle to avoid log spam.
|
|
340
293
|
if self._dropped_frames == 1 or self._dropped_frames % 300 == 0:
|
|
341
294
|
logger.warning(
|
|
342
295
|
"Recorder feed queue full; dropping frame to keep wire path unblocked",
|
|
343
296
|
dropped_total=self._dropped_frames,
|
|
344
297
|
)
|
|
345
298
|
return
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# ``queue.Full`` we'd silently extend the keepalive window
|
|
349
|
-
# past the point where ffmpeg is healthy enough to drain.
|
|
299
|
+
if not duplicate:
|
|
300
|
+
self._markers.mark_first_real_frame()
|
|
350
301
|
self._last_fed_t = now
|
|
351
302
|
|
|
352
303
|
def _feed_loop(self) -> None:
|
|
353
|
-
"""
|
|
354
|
-
|
|
355
|
-
No artificial pacing — ffmpeg uses
|
|
356
|
-
``-use_wallclock_as_timestamps`` so it stamps PTS at pipe-read
|
|
357
|
-
time; we simply hand frames over as fast as the queue produces
|
|
358
|
-
them. Per-frame audio fill is computed from wall-clock dt
|
|
359
|
-
between consecutive video frames so audio's byte-derived DTS
|
|
360
|
-
tracks video's PTS 1:1 regardless of dynamic FPS.
|
|
361
|
-
|
|
362
|
-
An encoder failure flips ``_disabled`` and exits the loop —
|
|
363
|
-
subsequent ``request_*`` calls will return ``clipFailed``.
|
|
364
|
-
"""
|
|
304
|
+
"""Drain ``_feed_queue`` into ffmpeg; disable on encoder failure."""
|
|
365
305
|
while True:
|
|
366
306
|
if self._feed_stop.is_set() and self._feed_queue.empty():
|
|
367
307
|
return
|
|
@@ -373,9 +313,6 @@ class SessionRecorder:
|
|
|
373
313
|
return
|
|
374
314
|
video, audio = item
|
|
375
315
|
now = time.monotonic()
|
|
376
|
-
# On the first frame we have no prior anchor; fall back to
|
|
377
|
-
# one ``1/30`` slot so the audio fill is non-zero. After
|
|
378
|
-
# that, dt naturally tracks the model's dynamic cadence.
|
|
379
316
|
dt = (
|
|
380
317
|
now - self._last_video_wall_t
|
|
381
318
|
if self._last_video_wall_t > 0.0
|
|
@@ -394,7 +331,6 @@ class SessionRecorder:
|
|
|
394
331
|
"Recorder encoder feed failed; disabling for this session"
|
|
395
332
|
)
|
|
396
333
|
self._disabled = True
|
|
397
|
-
# Drain the queue so producer side won't keep dropping.
|
|
398
334
|
try:
|
|
399
335
|
while True:
|
|
400
336
|
self._feed_queue.get_nowait()
|
|
@@ -417,15 +353,11 @@ class SessionRecorder:
|
|
|
417
353
|
if target <= 0:
|
|
418
354
|
return None
|
|
419
355
|
|
|
420
|
-
# Append any incoming model audio to the jitter buffer.
|
|
421
356
|
if model_audio is not None and model_audio.size > 0:
|
|
422
357
|
flat = np.ascontiguousarray(model_audio, dtype=np.int16).reshape(-1)
|
|
423
358
|
self._audio_jitter_buf.append(flat)
|
|
424
359
|
self._audio_buffered_samples += flat.size
|
|
425
360
|
|
|
426
|
-
# Trim the front of the buffer if it has grown beyond the cap;
|
|
427
|
-
# this caps recording latency at ~1 s of audio when the model
|
|
428
|
-
# bursts faster than video.
|
|
429
361
|
while self._audio_buffered_samples > self._audio_buffer_cap_samples:
|
|
430
362
|
head = self._audio_jitter_buf[0]
|
|
431
363
|
drop = self._audio_buffered_samples - self._audio_buffer_cap_samples
|
|
@@ -436,7 +368,6 @@ class SessionRecorder:
|
|
|
436
368
|
self._audio_jitter_buf[0] = head[drop:]
|
|
437
369
|
self._audio_buffered_samples -= drop
|
|
438
370
|
|
|
439
|
-
# Pull exactly ``target`` samples; pad with silence on underrun.
|
|
440
371
|
out = np.empty(target, dtype=np.int16)
|
|
441
372
|
filled = 0
|
|
442
373
|
while filled < target and self._audio_jitter_buf:
|
|
@@ -465,38 +396,31 @@ class SessionRecorder:
|
|
|
465
396
|
``/clips`` manifest endpoint will ``202 Retry-After`` until
|
|
466
397
|
that chunk lands; the SDK polls until ``200``.
|
|
467
398
|
|
|
468
|
-
Honours :attr:`RecordingConfig.skip_leading_black`.
|
|
469
399
|
"""
|
|
470
400
|
if self.disabled:
|
|
471
401
|
raise RecorderDisabledError("recorder disabled or encoder crashed")
|
|
472
402
|
capped = min(float(duration_seconds), float(self._config.clip_max_seconds))
|
|
473
403
|
if capped <= 0:
|
|
474
404
|
raise ValueError("duration_seconds must be positive")
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
405
|
+
if not self._markers.recording_started:
|
|
406
|
+
raise NoMediaYetError("no media generated yet")
|
|
407
|
+
start, end = self._markers.compute_clip_range(capped)
|
|
478
408
|
return self._build_result(kind=kind, start=start, end=end)
|
|
479
409
|
|
|
480
410
|
def request_recording(self) -> ClipResult:
|
|
481
411
|
"""Resolve a full-recording request; ``end = now``.
|
|
482
412
|
|
|
483
|
-
Same promise-then-poll semantics as :meth:`request_clip
|
|
484
|
-
honours :attr:`RecordingConfig.skip_leading_black`.
|
|
413
|
+
Same promise-then-poll semantics as :meth:`request_clip`.
|
|
485
414
|
"""
|
|
486
415
|
if self.disabled:
|
|
487
416
|
raise RecorderDisabledError("recorder disabled or encoder crashed")
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
417
|
+
if not self._markers.recording_started:
|
|
418
|
+
raise NoMediaYetError("no media generated yet")
|
|
419
|
+
start, end = self._markers.compute_recording_range()
|
|
491
420
|
return self._build_result(kind="recording", start=start, end=end)
|
|
492
421
|
|
|
493
422
|
def _build_result(self, kind: str, start: float, end: float) -> ClipResult:
|
|
494
423
|
now = self._markers.now_marker()
|
|
495
|
-
# Estimate when the in-progress chunk will close. The
|
|
496
|
-
# dominant term is the time until the next chunk boundary; sink
|
|
497
|
-
# copy after that is sub-second on local fs and not modeled.
|
|
498
|
-
# If chunk_seconds isn't configured the estimate degenerates
|
|
499
|
-
# to "now" (the SDK should still poll /clips with backoff).
|
|
500
424
|
cs = float(self._config.chunk_seconds)
|
|
501
425
|
if cs > 0:
|
|
502
426
|
next_boundary = (math.floor(now / cs) + 1) * cs
|
|
@@ -504,10 +428,6 @@ class SessionRecorder:
|
|
|
504
428
|
else:
|
|
505
429
|
wait_s = 0.0
|
|
506
430
|
predicted_ready_at_ms = int(round((time.time() + wait_s) * 1000))
|
|
507
|
-
# ``kind`` is a body-only discriminator; keeping it out of the
|
|
508
|
-
# URL means identical content always has one canonical URL.
|
|
509
|
-
# The URL is path-only — the SDK prepends the runtime/Coordinator
|
|
510
|
-
# origin it already knows. See ``ClipResult.playlist_url``.
|
|
511
431
|
query = urlencode(
|
|
512
432
|
{
|
|
513
433
|
"session_id": self._session_id,
|
{reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/http_runtime.py
RENAMED
|
@@ -904,6 +904,12 @@ class HttpRuntime(Runtime):
|
|
|
904
904
|
asyncio.run_coroutine_threadsafe(self._stop_webrtc_client(), self.loop)
|
|
905
905
|
|
|
906
906
|
def on_before_cleanup_complete(self, **kwargs) -> None:
|
|
907
|
+
# Belt-and-braces stop: also tear the recorder down here so
|
|
908
|
+
# session-end paths that bypass STOP_SESSION (notably the
|
|
909
|
+
# orphan-timeout path ORPHANED → CLOSING via TIMEOUT) can't
|
|
910
|
+
# leak the recorder. ``_stop_recorder`` is idempotent so the
|
|
911
|
+
# normal STOP_SESSION path is a no-op here. REA-2338.
|
|
912
|
+
self._stop_recorder()
|
|
907
913
|
self._upload_store.clear()
|
|
908
914
|
|
|
909
915
|
# -----------------------------------------------------------------
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
|
|
2
|
+
"""Module entry point for `python -m reactor_runtime.serve`.
|
|
3
|
+
|
|
4
|
+
Container CMDs and the Go CLI's docker-run dispatcher invoke this; nobody
|
|
5
|
+
should call it as a console script (no `[project.scripts]` entry exists).
|
|
6
|
+
The actual subcommand registration lives in `main.py` so it can be unit-
|
|
7
|
+
tested without exec'ing the module.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from reactor_runtime.serve.main import main
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|