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.
Files changed (139) hide show
  1. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/PKG-INFO +17 -17
  2. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/pyproject.toml +21 -18
  3. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/config.py +5 -4
  4. reactor_runtime-2.7.4/src/reactor_runtime/recording/markers.py +86 -0
  5. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/session_recorder.py +45 -125
  6. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/http_runtime.py +6 -0
  7. reactor_runtime-2.7.4/src/reactor_runtime/serve/__main__.py +13 -0
  8. reactor_runtime-2.7.4/src/reactor_runtime/utils/launch.py +200 -0
  9. reactor_runtime-2.7.4/src/reactor_runtime/utils/log.py +424 -0
  10. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/PKG-INFO +17 -17
  11. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/SOURCES.txt +1 -0
  12. reactor_runtime-2.7.4/src/reactor_runtime.egg-info/requires.txt +21 -0
  13. reactor_runtime-2.7.2/src/reactor_runtime/recording/markers.py +0 -115
  14. reactor_runtime-2.7.2/src/reactor_runtime/utils/launch.py +0 -107
  15. reactor_runtime-2.7.2/src/reactor_runtime/utils/log.py +0 -148
  16. reactor_runtime-2.7.2/src/reactor_runtime.egg-info/requires.txt +0 -21
  17. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/README.md +0 -0
  18. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/setup.cfg +0 -0
  19. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/api/__init__.py +0 -0
  20. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/__init__.py +0 -0
  21. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/config.py +0 -0
  22. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/__init__.py +0 -0
  23. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/defaults.py +0 -0
  24. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  25. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
  26. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  27. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/__init__.py +0 -0
  28. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/connected.py +0 -0
  29. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/event.py +0 -0
  30. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/messages.py +0 -0
  31. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/events/upload.py +0 -0
  32. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  33. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  34. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  35. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  36. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/__init__.py +0 -0
  37. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/decorators.py +0 -0
  38. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/handlers.py +0 -0
  39. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  40. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  41. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  42. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  43. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
  44. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  45. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  46. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/input.py +0 -0
  47. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/tracks/output.py +0 -0
  48. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/interface/upload.py +0 -0
  49. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/model_state.py +0 -0
  50. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/__init__.py +0 -0
  51. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  52. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/base.py +0 -0
  53. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/file.py +0 -0
  54. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  55. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/helpers.py +0 -0
  56. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  57. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  58. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/profiler.py +0 -0
  59. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/profiling/singleton.py +0 -0
  60. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/__init__.py +0 -0
  61. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  62. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
  63. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/sinks.py +0 -0
  64. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/recording/track_resolver.py +0 -0
  65. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtime_api.py +0 -0
  66. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  67. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/headless/headless_runtime.py +0 -0
  68. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
  69. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/config.py +0 -0
  70. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/runtimes/http/types.py +0 -0
  71. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/schema.py +0 -0
  72. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/schema_validator.py +0 -0
  73. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/__init__.py +0 -0
  74. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/commands/__init__.py +0 -0
  75. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/commands/run.py +0 -0
  76. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/commands/schema.py +0 -0
  77. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/main.py +0 -0
  78. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/utils/__init__.py +0 -0
  79. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/utils/config.py +0 -0
  80. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/serve/utils/runtime.py +0 -0
  81. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/__init__.py +0 -0
  82. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  83. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
  84. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/client.py +0 -0
  85. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  86. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
  87. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  88. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/config.py +0 -0
  89. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/events.py +0 -0
  90. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  91. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/client.py +0 -0
  92. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  93. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  94. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  95. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  96. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  97. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  98. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
  99. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  100. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  101. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  102. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  103. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  104. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  105. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  106. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  107. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  108. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  109. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  110. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  111. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  112. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
  113. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
  114. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  115. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  116. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  117. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  118. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  119. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  120. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  121. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  122. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  123. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  124. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  125. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  126. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
  127. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
  128. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  129. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/ice_uris.py +0 -0
  130. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/interface.py +0 -0
  131. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/media.py +0 -0
  132. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/transports/types.py +0 -0
  133. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/loader.py +0 -0
  134. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/messages.py +0 -0
  135. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/paths.py +0 -0
  136. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/ports.py +0 -0
  137. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime/utils/typing.py +0 -0
  138. {reactor_runtime-2.7.2 → reactor_runtime-2.7.4}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  139. {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.2
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>=1.24.0
9
- Requires-Dist: pydantic>=2.0.0
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.100.0
14
- Requires-Dist: uvicorn[standard]>=0.23.0
15
- Requires-Dist: aiohttp>=3.9.0
16
- Requires-Dist: httpx>=0.27.0
17
- Requires-Dist: redis
18
- Requires-Dist: jsonschema>=4.20.0
19
- Requires-Dist: opentelemetry-api~=1.39
20
- Requires-Dist: opentelemetry-sdk~=1.39
21
- Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.39
22
- Requires-Dist: opentelemetry-exporter-prometheus~=0.60b0
23
- Requires-Dist: grpcio>=1.60.0
24
- Requires-Dist: grpcio-health-checking
25
- Requires-Dist: opentelemetry-instrumentation-grpc~=0.60b0
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.40.0; extra == "gst"
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.2"
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
- "numpy>=1.24.0",
17
- "pydantic>=2.0.0",
18
- "omegaconf>=2.3.0",
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.100.0",
22
- "uvicorn[standard]>=0.23.0",
23
- "aiohttp>=3.9.0",
24
- "httpx>=0.27.0",
25
- "redis",
26
- "jsonschema>=4.20.0",
27
- "opentelemetry-api~=1.39",
28
- "opentelemetry-sdk~=1.39",
29
- "opentelemetry-exporter-otlp-proto-http~=1.39",
30
- "opentelemetry-exporter-prometheus~=0.60b0",
31
- "grpcio>=1.60.0",
32
- "grpcio-health-checking",
33
- "opentelemetry-instrumentation-grpc~=0.60b0"
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
- "PyGObject>=3.40.0",
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
- # Trim the session-start pre-roll from clip playlists by clamping
95
- # ``start_marker`` forward to the first real (non-duplicate) frame
96
- # the recorder observes. Set to ``False`` to keep that pre-roll
97
- # in clips.
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
@@ -1,16 +1,14 @@
1
1
  # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
- """Public entrypoint that ties encoder, uploader, markers, and sink together.
3
-
4
- A :class:`SessionRecorder` is built per session by the runtime layer
5
- (``HttpRuntime`` for now, ``RedisRuntime`` later). The runtime
6
- registers ``recorder._on_bundle_sync`` as an OutputBuffer callback so
7
- the recorder sees every frame the buffer emits including gap-fill
8
- duplicates, which are recorded so the resulting file's wall-clock
9
- duration matches the live wire.
10
-
11
- ``request_clip`` / ``request_recording`` are answered synchronously
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 session-
63
- relative seconds (since recorder start). ``predicted_ready_at_ms``
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
- """Per-tick callback registered on the model's :class:`OutputBuffer`.
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 duplicate and self._markers.first_real_frame_marker is None:
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
- # Update the keepalive watermark only when the bundle was
347
- # actually accepted onto the queue. If we update it on
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
- """Worker thread: drains ``_feed_queue`` into the ffmpeg pipes.
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
- start, end = self._markers.compute_clip_range(
476
- capped, skip_leading_black=self._config.skip_leading_black
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` and
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
- start, end = self._markers.compute_recording_range(
489
- skip_leading_black=self._config.skip_leading_black
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,
@@ -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()