reactor-runtime 2.8.0__tar.gz → 2.9.0__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 (132) hide show
  1. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/PKG-INFO +1 -1
  2. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/pyproject.toml +2 -2
  3. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/runtime_api.py +9 -1
  4. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/commands/run.py +59 -5
  5. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/config.py +2 -1
  6. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/events.py +1 -0
  7. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/client.py +42 -2
  8. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +8 -1
  9. reactor_runtime-2.9.0/src/reactor_runtime/transports/gstreamer/quality.py +71 -0
  10. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sender/video.py +10 -0
  11. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime.egg-info/PKG-INFO +1 -1
  12. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime.egg-info/SOURCES.txt +1 -0
  13. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/README.md +0 -0
  14. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/setup.cfg +0 -0
  15. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/api/__init__.py +0 -0
  16. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/__init__.py +0 -0
  17. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/config.py +0 -0
  18. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/experiment/__init__.py +0 -0
  19. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/experiment/session.py +0 -0
  20. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/__init__.py +0 -0
  21. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/client.py +0 -0
  22. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/defaults.py +0 -0
  23. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  24. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
  25. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  26. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
  27. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/events/connected.py +0 -0
  28. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/events/event.py +0 -0
  29. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/events/messages.py +0 -0
  30. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/events/upload.py +0 -0
  31. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  32. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  33. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  34. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  35. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
  36. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/model/decorators.py +0 -0
  37. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/model/handlers.py +0 -0
  38. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  39. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  40. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  41. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  42. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
  43. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  44. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  45. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
  46. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
  47. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/interface/upload.py +0 -0
  48. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/model_state.py +0 -0
  49. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/__init__.py +0 -0
  50. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  51. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/backends/base.py +0 -0
  52. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/backends/file.py +0 -0
  53. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  54. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/helpers.py +0 -0
  55. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/nvml_sampler.py +0 -0
  56. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  57. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  58. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/profiler.py +0 -0
  59. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/singleton.py +0 -0
  60. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/profiling/torch_chunk_profiler.py +0 -0
  61. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/__init__.py +0 -0
  62. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  63. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
  64. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/config.py +0 -0
  65. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/markers.py +0 -0
  66. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/session_recorder.py +0 -0
  67. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/sinks.py +0 -0
  68. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/recording/track_resolver.py +0 -0
  69. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/runtimes/http/config.py +0 -0
  70. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/runtimes/http/http_runtime.py +0 -0
  71. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/runtimes/http/types.py +0 -0
  72. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/schema.py +0 -0
  73. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/schema_validator.py +0 -0
  74. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/__init__.py +0 -0
  75. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/__main__.py +0 -0
  76. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/commands/__init__.py +0 -0
  77. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/commands/schema.py +0 -0
  78. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/main.py +0 -0
  79. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/utils/__init__.py +0 -0
  80. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/utils/config.py +0 -0
  81. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/serve/utils/runtime.py +0 -0
  82. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/__init__.py +0 -0
  83. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  84. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  85. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  86. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  87. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  88. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  89. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  90. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
  91. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  92. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  93. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  94. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  95. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  96. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  97. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  98. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  99. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  100. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  101. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  102. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  103. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
  104. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
  105. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  106. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  107. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  108. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  109. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  110. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  111. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  112. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  113. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  114. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  115. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  116. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  117. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
  118. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  119. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/ice_uris.py +0 -0
  120. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/interface.py +0 -0
  121. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/media.py +0 -0
  122. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/transports/types.py +0 -0
  123. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/launch.py +0 -0
  124. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/loader.py +0 -0
  125. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/log.py +0 -0
  126. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/messages.py +0 -0
  127. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/paths.py +0 -0
  128. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/ports.py +0 -0
  129. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime/utils/typing.py +0 -0
  130. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  131. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime.egg-info/requires.txt +0 -0
  132. {reactor_runtime-2.8.0 → reactor_runtime-2.9.0}/src/reactor_runtime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactor_runtime"
7
- version = "2.8.0"
7
+ version = "2.9.0"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -90,4 +90,4 @@ gst = [
90
90
  # Legacy releases used `X.Y.Z-g<short-sha>` (e.g. `1.4.1-g5660eed`); the wheel
91
91
  # stripped the `-g<sha>` suffix. fetch-proto.py still understands that shape
92
92
  # for back-compat with old pins, but new bumps should track the calendar format.
93
- version = "1.20260603.15086"
93
+ version = "1.20260625.16626"
@@ -379,8 +379,16 @@ class Runtime(ModelStateMachine, ABC):
379
379
  self._session_moderator: Optional["SessionModerator"] = None
380
380
  moderation_enabled = self.config.reactor_config.moderation.enabled
381
381
  if _MODERATION_AVAILABLE:
382
+ # REA-2601: pass the subclass-owned SupervisorClient (if any)
383
+ # so the default ``supervisor`` backend can proxy moderation
384
+ # over the existing gRPC channel. Subclasses without a
385
+ # Supervisor connection (e.g. HttpRuntime) simply leave the
386
+ # attribute unset; ``from_reactor_config`` raises a
387
+ # ``ModerationConfigError`` at startup so the misconfiguration
388
+ # never reaches a live session.
382
389
  self._moderator = ContentModerator.from_reactor_config(
383
- self.config.reactor_config
390
+ self.config.reactor_config,
391
+ supervisor_client=getattr(self, "_supervisor_client", None),
384
392
  )
385
393
  if self._moderator is not None:
386
394
  logger.info("Content moderation enabled")
@@ -23,12 +23,37 @@ from reactor_runtime.utils.paths import _set_configured_weights_path
23
23
  logger = get_logger(__name__)
24
24
 
25
25
  _MODEL_NAME_ENV_VAR = "MODEL_NAME"
26
- _MODEL_NAME_MAX_LENGTH = 128
27
- _MODEL_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9-]*$")
26
+ _MODEL_NAME_MAX_LENGTH = 129
27
+ _MODEL_NAME_PATTERN = re.compile(
28
+ r"^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)?$"
29
+ )
28
30
 
29
31
  _REACTOR_YAML = "reactor.yaml"
30
32
 
31
33
 
34
+ def _advertised_name_matches(advertised: str, canonical: str) -> bool:
35
+ """Check a reactor.yaml-advertised name against the assigned MODEL_NAME.
36
+
37
+ The Coordinator assigns a fully-qualified canonical name
38
+ (``namespace/slug``, e.g. ``reactor/echo``). A model author is allowed
39
+ to advertise either that canonical name or the bare slug — the
40
+ namespace is derived from the authenticated account, so omitting it is
41
+ legitimate. Any other value (a different slug, or a namespace the
42
+ author does not own) is a mismatch.
43
+
44
+ Args:
45
+ advertised: ``model.name`` from the partner-authored reactor.yaml.
46
+ canonical: The platform-assigned ``MODEL_NAME``.
47
+
48
+ Returns:
49
+ True if ``advertised`` resolves to ``canonical``.
50
+ """
51
+ if advertised == canonical:
52
+ return True
53
+ _, _, slug = canonical.partition("/")
54
+ return bool(slug) and advertised == slug
55
+
56
+
32
57
  class RunCommand:
33
58
  @staticmethod
34
59
  def register_subcommand(subparsers):
@@ -126,10 +151,27 @@ class RunCommand:
126
151
  return
127
152
 
128
153
  # ---------------------------------------------------------
129
- # Model name (from YAML or env override)
154
+ # Model identity
155
+ #
156
+ # In production the Coordinator assigns the canonical model name
157
+ # (e.g. "reactor/echo") and the Supervisor injects it as the
158
+ # MODEL_NAME env var on the generated deployment. That value is the
159
+ # trusted identity: the runtime keys its streams, logs and metering
160
+ # off it, never off the partner-authored reactor.yaml.
161
+ #
162
+ # A model author may still advertise a name in reactor.yaml
163
+ # (model.name). When MODEL_NAME is set we treat that as a claim that
164
+ # must be honoured — it has to resolve to the assigned name (the
165
+ # canonical name, or the bare slug since the namespace is derived
166
+ # from the authenticated account). A mismatch means untrusted model
167
+ # code is trying to run under a name it was not granted, so we
168
+ # refuse to start.
169
+ #
170
+ # Without MODEL_NAME (local `reactor run`) there is no assigning
171
+ # authority, so the advertised name — then the entrypoint spec — is
172
+ # used as a best-effort label.
130
173
  # ---------------------------------------------------------
131
- if not reactor_config.name:
132
- reactor_config.name = reactor_config.model
174
+ advertised_name = reactor_config.name
133
175
  env_name = os.environ.get(_MODEL_NAME_ENV_VAR)
134
176
  if env_name:
135
177
  if len(env_name) > _MODEL_NAME_MAX_LENGTH:
@@ -138,7 +180,19 @@ class RunCommand:
138
180
  if not _MODEL_NAME_PATTERN.match(env_name):
139
181
  print(f"Error: {_MODEL_NAME_ENV_VAR} has invalid chars")
140
182
  return
183
+ if advertised_name and not _advertised_name_matches(
184
+ advertised_name, env_name
185
+ ):
186
+ print(
187
+ f"Error: reactor.yaml advertises model name "
188
+ f"'{advertised_name}', which does not match the "
189
+ f"assigned {_MODEL_NAME_ENV_VAR} '{env_name}'. A model "
190
+ f"may only run under the name the platform assigned it."
191
+ )
192
+ return
141
193
  reactor_config.name = env_name
194
+ elif not reactor_config.name:
195
+ reactor_config.name = reactor_config.model
142
196
 
143
197
  # ---------------------------------------------------------
144
198
  # Resolve model config path + CLI overrides
@@ -258,7 +258,8 @@ def resolve_default_transport() -> TransportType:
258
258
  bindings import and that every required element is present, raising
259
259
  a clear ``ImportError`` (with an install hint) when they are not.
260
260
  Failing loudly here surfaces a misconfigured environment at startup
261
- rather than at the first ``WebRTCTransport.create`` call.
261
+ rather than at the first ``WebRTCTransport.create`` call, and
262
+ pre-warms Gst.init() so the first connection does not pay that cost.
262
263
  """
263
264
  try:
264
265
  validate_transport_available(TransportType.GSTREAMER)
@@ -178,6 +178,7 @@ class TrackStats:
178
178
  packets_received: Optional[int] = None
179
179
  packets_sent: Optional[int] = None
180
180
  jitter: Optional[float] = None
181
+ qos: Optional[float] = None
181
182
 
182
183
 
183
184
  @dataclass
@@ -70,6 +70,7 @@ from reactor_runtime.transports.gstreamer.gst_helpers import (
70
70
  from reactor_runtime.transports.gstreamer.signals import SignalManager
71
71
  from reactor_runtime.transports.gstreamer.sender import AudioSender, VideoSender
72
72
  from reactor_runtime.transports.gstreamer.receiver import VideoReceiver, AudioReceiver
73
+ from reactor_runtime.transports.gstreamer.quality import aggregate_qos_score
73
74
  from reactor_runtime.transports.gstreamer.settings import (
74
75
  SUPPORTED_RTP_HEADER_EXTENSION_URIS,
75
76
  VIDEO_BWE_MIN_BITRATE_KBPS,
@@ -104,6 +105,9 @@ ICE_GATHERING_TIMEOUT_MS = 3000
104
105
  # Frame queue drain timeout interval in milliseconds
105
106
  FRAME_Q_DRAIN_TIMEOUT_MS = 10
106
107
 
108
+ # Quality score measurement interval
109
+ QUALITY_METER_INTERVAL_MS = 1000
110
+
107
111
  # =============================================================================
108
112
  # Helpers for converting transport-agnostic types to GStreamer expected ones
109
113
  # =============================================================================
@@ -489,6 +493,38 @@ class GStreamerTransport(WebRTCTransport):
489
493
  src.set_callback(_drain)
490
494
  src.attach(self._main_ctx)
491
495
 
496
+ def _gst_arm_quality_meter(self) -> None:
497
+ """GLib thread: log a per-second video quality score for every active video sender."""
498
+ if self._main_ctx is None:
499
+ return
500
+
501
+ src = GLib.timeout_source_new(QUALITY_METER_INTERVAL_MS)
502
+ self._track_source(src)
503
+
504
+ def _tick(*_args) -> bool:
505
+ if self._stopping:
506
+ return GLib.SOURCE_REMOVE
507
+
508
+ scores = [
509
+ s
510
+ for sender in self._senders.values()
511
+ if isinstance(sender, VideoSender)
512
+ for s in [sender.qos()]
513
+ if s is not None
514
+ ]
515
+ score = aggregate_qos_score(scores)
516
+ if score is not None:
517
+ logger.info(
518
+ "video quality score",
519
+ score=score,
520
+ senders=len(scores),
521
+ )
522
+
523
+ return GLib.SOURCE_CONTINUE
524
+
525
+ src.set_callback(_tick)
526
+ src.attach(self._main_ctx)
527
+
492
528
  def _require_webrtc(self) -> Gst.Element:
493
529
  if self._webrtc is None:
494
530
  raise RuntimeError("webrtcbin element is not initialized")
@@ -874,6 +910,7 @@ class GStreamerTransport(WebRTCTransport):
874
910
  if not self._connected.is_set():
875
911
  self._connected.set()
876
912
  self._gst_emit_event(ConnectedEvent())
913
+ self._gst_arm_quality_meter()
877
914
  elif state in (
878
915
  GstWebRTC.WebRTCPeerConnectionState.FAILED,
879
916
  GstWebRTC.WebRTCPeerConnectionState.CLOSED,
@@ -906,10 +943,10 @@ class GStreamerTransport(WebRTCTransport):
906
943
 
907
944
  if label == "control":
908
945
  self._control_channel = data_channel
909
- self._run_on_gst_thread(self._setup_control_channel_handlers)
946
+ self._setup_control_channel_handlers()
910
947
  else:
911
948
  self._data_channel = data_channel
912
- self._run_on_gst_thread(self._setup_data_channel_handlers)
949
+ self._setup_data_channel_handlers()
913
950
 
914
951
  def _gst_on_pad_added(self, element, pad) -> None:
915
952
  if self._stopping or self._stop_event.is_set():
@@ -1642,6 +1679,7 @@ class GStreamerTransport(WebRTCTransport):
1642
1679
  width: Optional[int] = None
1643
1680
  height: Optional[int] = None
1644
1681
  codec_name: Optional[str] = None
1682
+ qos: Optional[float] = None
1645
1683
  sender = self._senders.get(mid)
1646
1684
  if isinstance(sender, VideoSender):
1647
1685
  bitrate_bps = sender.current_bitrate_kbps * 1000
@@ -1649,6 +1687,7 @@ class GStreamerTransport(WebRTCTransport):
1649
1687
  width = sender.width
1650
1688
  height = sender.height
1651
1689
  codec_name = sender.codec_name
1690
+ qos = sender.qos()
1652
1691
  elif isinstance(sender, AudioSender):
1653
1692
  bitrate_bps = sender.current_bitrate_kbps * 1000
1654
1693
  codec_name = sender.codec_name
@@ -1678,6 +1717,7 @@ class GStreamerTransport(WebRTCTransport):
1678
1717
  packet_loss=packet_loss,
1679
1718
  packets_sent=packets_sent,
1680
1719
  jitter=jitter,
1720
+ qos=qos,
1681
1721
  )
1682
1722
  )
1683
1723
 
@@ -75,7 +75,14 @@ def add_many(
75
75
  the element will remain in NULL state until explicitly set.
76
76
  """
77
77
  for e in elements:
78
- if not bin.add(e):
78
+ # Gst.Bin.add() returns the C gboolean on most PyGObject builds, but some
79
+ # return None even on success — so verify the element actually landed in
80
+ # this bin rather than trusting the binding's return value. Checking
81
+ # `is not bin` (not just `is None`) also catches a failed add of an element
82
+ # that already belongs to another bin, where get_parent() returns that
83
+ # other bin.
84
+ bin.add(e)
85
+ if e.get_parent() is not bin:
79
86
  raise RuntimeError(f"Failed to add {e.get_name()} to {bin.get_name()}")
80
87
  if sync_with_parent:
81
88
  e.sync_state_with_parent()
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+
3
+ """
4
+ Video quality scoring utilities.
5
+
6
+ Provides a formula-based estimate of the *expected* bitrate for a given
7
+ resolution, frame-rate, and codec, and a 0–10 quality score comparing
8
+ a measured bitrate against that expectation.
9
+ """
10
+
11
+ from typing import Optional
12
+
13
+ # =========================================================================
14
+ # Reference tables
15
+ # =========================================================================
16
+
17
+ # Bits-per-pixel for H.264 at "medium" quality (general content, no motion
18
+ # extremes). Other codecs scale from this baseline via _CODEC_EFFICIENCY.
19
+ _BPP_H264_MEDIUM = 0.08
20
+
21
+ # Compression efficiency relative to H.264 (lower → fewer bits needed for
22
+ # equal perceived quality). Keys are RTP encoding-name strings (uppercase),
23
+ # as returned by GStreamer and SDP negotiation.
24
+ _CODEC_EFFICIENCY: dict[str, float] = {
25
+ "H264": 1.00,
26
+ "H265": 0.55,
27
+ "VP8": 1.00,
28
+ "VP9": 0.65,
29
+ "AV1": 0.45,
30
+ }
31
+
32
+ # =========================================================================
33
+ # Per-track helpers
34
+ # =========================================================================
35
+
36
+
37
+ def expected_video_bitrate_bps(
38
+ width: int, height: int, fps: float, codec_name: str
39
+ ) -> int:
40
+ """Return the target bitrate in bps for a good-quality encode."""
41
+ efficiency = _CODEC_EFFICIENCY.get(codec_name.upper(), 1.0)
42
+ return int(width * height * fps * _BPP_H264_MEDIUM * efficiency)
43
+
44
+
45
+ def video_qos_score(
46
+ current_bps: int, width: int, height: int, fps: float, codec_name: str
47
+ ) -> float:
48
+ """Return a 0–10 quality score for a single video track.
49
+
50
+ 10.0 means at or above the expected bitrate for good quality.
51
+ Score scales linearly down to 0 as bitrate approaches zero.
52
+ """
53
+ expected = expected_video_bitrate_bps(width, height, fps, codec_name)
54
+ if expected <= 0:
55
+ return 10.0
56
+ return round(min(10.0, current_bps / expected * 10.0), 1)
57
+
58
+
59
+ # =========================================================================
60
+ # Aggregate score across multiple senders
61
+ # =========================================================================
62
+
63
+
64
+ def aggregate_qos_score(scores: list[float]) -> Optional[float]:
65
+ """Return the average 0–10 quality score across all video senders.
66
+
67
+ Returns None when the list is empty (no ready senders).
68
+ """
69
+ if not scores:
70
+ return None
71
+ return round(sum(scores) / len(scores), 1)
@@ -9,6 +9,7 @@ from typing import List, NamedTuple, Optional
9
9
  import numpy as np
10
10
 
11
11
  from reactor_runtime.transports.gstreamer.encoders import EncoderFactory
12
+ from reactor_runtime.transports.gstreamer.quality import video_qos_score
12
13
  from reactor_runtime.transports.gstreamer.settings import VIDEO_BWE_TARGET_BITRATE_KBPS
13
14
  from reactor_runtime.transports.gstreamer.sdp.codec import CodecEntry
14
15
  from reactor_runtime.transports.gstreamer.sdp.extmap import SdpExtmap
@@ -208,6 +209,15 @@ class VideoSender(_SenderStreamBase):
208
209
  """Forward target bitrate to the encoder bin."""
209
210
  self._encoder.set_target_bitrate_kbps(int(new_kbps))
210
211
 
212
+ def qos(self) -> Optional[float]:
213
+ """Return the 0–10 quality score for this sender, or None if not ready."""
214
+ w, h, fps = self.width, self.height, self.fps
215
+ if not w or not h or not fps:
216
+ return None
217
+ return video_qos_score(
218
+ self.current_bitrate_kbps * 1000, w, h, fps, self._codec_name
219
+ )
220
+
211
221
  def push_buffer(self, frame: np.ndarray) -> bool:
212
222
  """
213
223
  Extract dimensions and bytes from the frame (e.g. numpy array with .shape and .tobytes()),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -85,6 +85,7 @@ src/reactor_runtime/transports/gstreamer/__init__.py
85
85
  src/reactor_runtime/transports/gstreamer/client.py
86
86
  src/reactor_runtime/transports/gstreamer/gst.py
87
87
  src/reactor_runtime/transports/gstreamer/gst_helpers.py
88
+ src/reactor_runtime/transports/gstreamer/quality.py
88
89
  src/reactor_runtime/transports/gstreamer/settings.py
89
90
  src/reactor_runtime/transports/gstreamer/signals.py
90
91
  src/reactor_runtime/transports/gstreamer/decoders/__init__.py