reactor-runtime 2.3.0__tar.gz → 2.3.2__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.3.0 → reactor_runtime-2.3.2}/PKG-INFO +1 -1
  2. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/pyproject.toml +1 -1
  3. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/driver/pipeline_executor.py +11 -2
  4. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/model/reactor_model.py +30 -3
  5. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +17 -4
  6. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/profiler.py +15 -0
  7. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtime_api.py +0 -67
  8. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtimes/headless/headless_runtime.py +0 -2
  9. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtimes/http/http_runtime.py +0 -2
  10. reactor_runtime-2.3.2/src/reactor_runtime/transports/gstreamer/probes/__init__.py +5 -0
  11. reactor_runtime-2.3.2/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +119 -0
  12. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime.egg-info/PKG-INFO +1 -1
  13. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime.egg-info/SOURCES.txt +2 -0
  14. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/README.md +0 -0
  15. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/setup.cfg +0 -0
  16. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/api/__init__.py +0 -0
  17. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/commands/__init__.py +0 -0
  18. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/commands/init.py +0 -0
  19. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/commands/run.py +0 -0
  20. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/commands/schema.py +0 -0
  21. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/main.py +0 -0
  22. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/utils/__init__.py +0 -0
  23. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/utils/config.py +0 -0
  24. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/utils/runtime.py +0 -0
  25. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_cli/utils/version.py +0 -0
  26. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/__init__.py +0 -0
  27. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/config.py +0 -0
  28. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/__init__.py +0 -0
  29. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/defaults.py +0 -0
  30. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  31. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  32. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/events/__init__.py +0 -0
  33. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/events/connected.py +0 -0
  34. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/events/event.py +0 -0
  35. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/events/messages.py +0 -0
  36. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/events/upload.py +0 -0
  37. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  38. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  39. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  40. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  41. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/model/__init__.py +0 -0
  42. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/model/decorators.py +0 -0
  43. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/model/handlers.py +0 -0
  44. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  45. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  46. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  47. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  48. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  49. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/tracks/input.py +0 -0
  50. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/tracks/output.py +0 -0
  51. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/interface/upload.py +0 -0
  52. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/model_state.py +0 -0
  53. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/__init__.py +0 -0
  54. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  55. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/backends/base.py +0 -0
  56. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/backends/file.py +0 -0
  57. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  58. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/helpers.py +0 -0
  59. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  60. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  61. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/profiling/singleton.py +0 -0
  62. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  63. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
  64. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtimes/http/config.py +0 -0
  65. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/runtimes/http/types.py +0 -0
  66. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/schema.py +0 -0
  67. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/schema_validator.py +0 -0
  68. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/__init__.py +0 -0
  69. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  70. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
  71. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/aiortc/client.py +0 -0
  72. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  73. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
  74. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  75. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/config.py +0 -0
  76. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/events.py +0 -0
  77. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  78. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/client.py +0 -0
  79. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  80. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  81. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  82. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  83. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  84. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  85. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  86. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  87. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  88. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  89. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  90. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  91. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  92. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  93. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  94. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  95. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  96. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  97. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  98. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  99. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  100. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  101. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  102. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  103. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  104. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  105. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  106. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  107. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  108. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  109. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  110. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
  111. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
  112. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  113. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/ice_uris.py +0 -0
  114. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/interface.py +0 -0
  115. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/media.py +0 -0
  116. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/transports/types.py +0 -0
  117. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/utils/launch.py +0 -0
  118. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/utils/loader.py +0 -0
  119. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/utils/log.py +0 -0
  120. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/utils/messages.py +0 -0
  121. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime/utils/typing.py +0 -0
  122. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  123. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime.egg-info/entry_points.txt +0 -0
  124. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime.egg-info/requires.txt +0 -0
  125. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/reactor_runtime.egg-info/top_level.txt +0 -0
  126. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/README.md +0 -0
  127. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/__init__.py +0 -0
  128. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/config.yml +0 -0
  129. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/model.py +0 -0
  130. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/pipeline.py +0 -0
  131. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/reactor.yaml +0 -0
  132. {reactor_runtime-2.3.0 → reactor_runtime-2.3.2}/src/template/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.3.0
3
+ Version: 2.3.2
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.3.0"
7
+ version = "2.3.2"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import asyncio
7
+ import dataclasses
7
8
  import inspect
8
9
  from typing import Any, Dict, Iterator, List, Optional, Type
9
10
 
@@ -144,7 +145,7 @@ class PipelineExecutor(Iterator[StepResult]):
144
145
  raise ValueError(f"Unknown event {name!r}. Available events: {available}")
145
146
 
146
147
  try:
147
- entry.event_cls(**kwargs)
148
+ event = entry.event_cls(**kwargs)
148
149
  except TypeError as exc:
149
150
  raise TypeError(f"Invalid arguments for event {name!r}: {exc}") from None
150
151
 
@@ -155,7 +156,15 @@ class PipelineExecutor(Iterator[StepResult]):
155
156
  if not ok:
156
157
  raise ValueError(f"Validation failed for event {name!r}: {reason}")
157
158
 
158
- return self._run(self._send_event(entry, **kwargs))
159
+ # Mirror production dispatch (reactor_model._invoke): splat the
160
+ # full set of event dataclass fields into the handler kwargs so
161
+ # callers that omit optional fields get the dataclass default
162
+ # (e.g. ``None``), not the raw ``FieldInfo`` sentinel that is
163
+ # still present in the handler function's ``__defaults__``.
164
+ handler_kwargs = {
165
+ f.name: getattr(event, f.name) for f in dataclasses.fields(event)
166
+ }
167
+ return self._run(self._send_event(entry, **handler_kwargs))
159
168
 
160
169
  def push_media(self, track_name: str, data: Any) -> None:
161
170
  """Push a media frame into an input buffer.
@@ -12,7 +12,8 @@ from __future__ import annotations
12
12
 
13
13
  import asyncio
14
14
  import dataclasses
15
- from typing import Any, Callable, List, Tuple, Type
15
+ import os
16
+ from typing import Any, Callable, List, Optional, Tuple, Type
16
17
 
17
18
  from reactor_runtime.interface.events.connected import Connected, Disconnected
18
19
  from reactor_runtime.interface.events.event import Event
@@ -96,12 +97,22 @@ class ReactorModel(ReactorCore):
96
97
  dispatch_table = build_dispatch_table(handlers)
97
98
 
98
99
  dispatcher = asyncio.create_task(self._dispatcher(handlers, dispatch_table))
100
+ crash: Optional[BaseException] = None
99
101
  try:
100
102
  await self.run()
101
103
  except asyncio.CancelledError:
102
104
  logger.info("Model cancelled")
103
- except Exception:
104
- logger.exception("run() crashed")
105
+ except Exception as exc:
106
+ # Don't swallow-and-return: log, clean up via finally, then
107
+ # fail-fast (REA-1740). ``ReactorPipeline.run()`` contains
108
+ # its own per-session crashes, so this only fires for custom
109
+ # ``run()`` overrides.
110
+ crash = exc
111
+ logger.exception(
112
+ "run() crashed — terminating the process",
113
+ model=type(self).__name__,
114
+ exc_type=type(exc).__name__,
115
+ )
105
116
  finally:
106
117
  dispatcher.cancel()
107
118
  try:
@@ -109,6 +120,22 @@ class ReactorModel(ReactorCore):
109
120
  except asyncio.CancelledError:
110
121
  pass
111
122
 
123
+ if crash is not None:
124
+ self._fail_fast(crash)
125
+
126
+ def _fail_fast(self, exc: BaseException) -> None:
127
+ """Terminate the process after an unrecoverable ``run()`` error.
128
+
129
+ Uses :func:`os._exit` (not ``sys.exit``) so the whole process
130
+ goes away regardless of thread. Override in subclasses or
131
+ tests.
132
+ """
133
+ logger.critical(
134
+ "Terminating process after unhandled run() exception",
135
+ exc_type=type(exc).__name__,
136
+ )
137
+ os._exit(1)
138
+
112
139
  # ------------------------------------------------------------------
113
140
  # run() — override with your generation loop
114
141
  # ------------------------------------------------------------------
@@ -241,6 +241,10 @@ class ReactorPipeline(ReactorModel):
241
241
  except BufferClosed:
242
242
  logger.debug("Input buffer closed, ending session")
243
243
  break
244
+ # Any other exception propagates up through the outer
245
+ # try/finally (gen close + state reset) and out of
246
+ # ``run()`` — ``_lifecycle`` then fails-fast via
247
+ # ``_fail_fast`` (REA-1740).
244
248
 
245
249
  if output is Idle:
246
250
  await asyncio.sleep(0.005)
@@ -251,10 +255,19 @@ class ReactorPipeline(ReactorModel):
251
255
  else:
252
256
  await self.emit(output)
253
257
  finally:
254
- if is_async:
255
- await gen.aclose()
256
- else:
257
- gen.close()
258
+ # Swallow exceptions from ``gen.close()`` only so a buggy
259
+ # ``inference()`` ``finally`` cannot mask the original
260
+ # exception on its way up to ``_lifecycle``.
261
+ try:
262
+ if is_async:
263
+ await gen.aclose()
264
+ else:
265
+ gen.close()
266
+ except Exception:
267
+ logger.exception(
268
+ "generator.close() raised during teardown",
269
+ model=type(self).__name__,
270
+ )
258
271
  self.state = None
259
272
  for buf in self._input_buffers.values():
260
273
  buf.reset()
@@ -613,6 +613,18 @@ class ProfilerTimer:
613
613
  )
614
614
  self._start = None
615
615
 
616
+ def cancel(self) -> None:
617
+ """Discard the timer run without recording a measurement.
618
+
619
+ Safe to call whether or not the timer is currently running. Used to
620
+ reset state when the caller's scope is aborted before ``.stop()``
621
+ can fire (e.g. generator exit, session interruption before first
622
+ output). After ``cancel()``, the timer is idle and ``.start()``
623
+ will succeed.
624
+ """
625
+ self._start = None
626
+ self.elapsed_ms = None
627
+
616
628
 
617
629
  class _NoOpTimer:
618
630
  """No-op timer for when profiling is disabled."""
@@ -625,6 +637,9 @@ class _NoOpTimer:
625
637
  def stop(self) -> None:
626
638
  pass
627
639
 
640
+ def cancel(self) -> None:
641
+ pass
642
+
628
643
 
629
644
  class _NoOpSection:
630
645
  """No-op context manager for when profiling is disabled."""
@@ -41,7 +41,6 @@ logger = get_logger(__name__)
41
41
 
42
42
  _IDLING_INTERVAL_ENV_VAR = "IDLING_INTERVAL_SECONDS"
43
43
  _DEFAULT_IDLING_INTERVAL = 30.0
44
- _MAX_SESSION_DURATION_ENV_VAR = "MAX_SESSION_DURATION_SECONDS"
45
44
 
46
45
 
47
46
  # ---------------------------------------------------------------------------
@@ -70,10 +69,6 @@ class RuntimeConfig:
70
69
  # Priority: ENV > CLI > default. Set to None to use default.
71
70
  idling_interval: Optional[float] = None
72
71
 
73
- # Maximum session duration in seconds. If set, sessions will be automatically
74
- # stopped after this duration. Priority: ENV > CLI. Set to None to disable.
75
- max_session_duration: Optional[float] = None
76
-
77
72
  profiling_output_dir: Optional[str] = None
78
73
 
79
74
  def __post_init__(self) -> None:
@@ -102,20 +97,6 @@ class RuntimeConfig:
102
97
  if self.idling_interval is None:
103
98
  self.idling_interval = _DEFAULT_IDLING_INTERVAL
104
99
 
105
- # Resolve max_session_duration: ENV > CLI (no default - disabled if not set)
106
- max_env = os.getenv(_MAX_SESSION_DURATION_ENV_VAR)
107
- if max_env:
108
- try:
109
- self.max_session_duration = float(max_env)
110
- except ValueError:
111
- logger.warning(
112
- "Invalid max session duration env value",
113
- env_var=_MAX_SESSION_DURATION_ENV_VAR,
114
- value=max_env,
115
- )
116
- if self.max_session_duration is not None and self.max_session_duration <= 0:
117
- self.max_session_duration = None
118
-
119
100
  @staticmethod
120
101
  def parser() -> argparse.ArgumentParser:
121
102
  """Return an argument parser for runtime-specific CLI arguments.
@@ -146,12 +127,6 @@ class RuntimeConfig:
146
127
  help=f"Idling event interval ({_IDLING_INTERVAL_ENV_VAR} env overrides). "
147
128
  f"Default: {_DEFAULT_IDLING_INTERVAL}s",
148
129
  )
149
- parser.add_argument(
150
- "--max-session-duration",
151
- type=float,
152
- default=None,
153
- help=f"Max session seconds ({_MAX_SESSION_DURATION_ENV_VAR} env overrides).",
154
- )
155
130
  parser.add_argument(
156
131
  "--profiling-dir",
157
132
  type=str,
@@ -196,9 +171,6 @@ class Runtime(ModelStateMachine, ABC):
196
171
  # Idling task - sends periodic IDLING events when in READY state
197
172
  self._idling_task: Optional[asyncio.Task] = None
198
173
 
199
- # Max session duration task - stops session after configured duration
200
- self._max_session_duration_task: Optional[asyncio.Task] = None
201
-
202
174
  # Graceful shutdown state (for SIGTERM handling)
203
175
  self._shutdown_pending: bool = False
204
176
  self._shutdown_event: asyncio.Event = asyncio.Event()
@@ -334,7 +306,6 @@ class Runtime(ModelStateMachine, ABC):
334
306
  self._cancel_orphan_timeout()
335
307
 
336
308
  if previous_state == ModelState.WAITING:
337
- self._start_max_session_duration_timeout()
338
309
  if self.model is not None:
339
310
  self.model._push_event(Connected())
340
311
  logger.info("Pushed Connected event to model")
@@ -373,7 +344,6 @@ class Runtime(ModelStateMachine, ABC):
373
344
  ``@disconnected`` asynchronously on its own thread.
374
345
  """
375
346
  self._cancel_orphan_timeout()
376
- self._cancel_max_session_duration_timeout()
377
347
 
378
348
  if self.model is not None:
379
349
  self.model._push_event(Disconnected())
@@ -805,43 +775,6 @@ class Runtime(ModelStateMachine, ABC):
805
775
  self._orphan_task.cancel()
806
776
  self._orphan_task = None
807
777
 
808
- # -----------------------------------------------------------------
809
- # Max session duration
810
- # -----------------------------------------------------------------
811
-
812
- def _start_max_session_duration_timeout(self) -> None:
813
- """Start the max session duration timeout task.
814
-
815
- If max_session_duration is configured, the session will be automatically
816
- stopped when the duration expires. This provides a hard limit on session
817
- lifetime regardless of client activity.
818
- """
819
- if (
820
- self.config.max_session_duration is None
821
- or self.config.max_session_duration <= 0
822
- ):
823
- return
824
- self._cancel_max_session_duration_timeout()
825
-
826
- async def coro() -> None:
827
- try:
828
- await asyncio.sleep(self.config.max_session_duration) # type: ignore[arg-type]
829
- if self.current_state in (
830
- ModelState.STREAMING,
831
- ModelState.ORPHANED,
832
- ):
833
- self.send(ModelEvent.STOP_SESSION)
834
- except asyncio.CancelledError:
835
- pass
836
-
837
- self._max_session_duration_task = asyncio.create_task(coro())
838
-
839
- def _cancel_max_session_duration_timeout(self) -> None:
840
- """Cancel the max session duration timeout if running."""
841
- if self._max_session_duration_task is not None:
842
- self._max_session_duration_task.cancel()
843
- self._max_session_duration_task = None
844
-
845
778
  # -----------------------------------------------------------------
846
779
  # Idling
847
780
  # -----------------------------------------------------------------
@@ -397,14 +397,12 @@ async def serve(
397
397
  - output_root: Root directory for output (default: "./output")
398
398
  - no_ignore_duplicates: If True, write all frames including duplicates (default: False)
399
399
  - orphan_timeout: Seconds to wait before stopping orphaned session (default: 30.0)
400
- - max_session_duration: Maximum session duration in seconds (default: None, disabled)
401
400
  """
402
401
  config = HeadlessRuntimeConfig(
403
402
  reactor_config=reactor_config,
404
403
  output_root=kwargs.get("output_root", "./output"),
405
404
  ignore_duplicates=not kwargs.get("no_ignore_duplicates", False),
406
405
  orphan_timeout=kwargs.get("orphan_timeout", 30.0),
407
- max_session_duration=kwargs.get("max_session_duration"),
408
406
  profiling_output_dir=kwargs.get("profiling_output_dir"),
409
407
  )
410
408
 
@@ -647,7 +647,6 @@ async def serve(
647
647
  - host: Host to bind to (default: "0.0.0.0")
648
648
  - port: Port to bind to (default: 8080)
649
649
  - orphan_timeout: Seconds to wait before stopping orphaned session (default: 30.0)
650
- - max_session_duration: Maximum session duration in seconds (default: None, disabled)
651
650
  - webrtc_port_range: WebRTC ICE UDP port range as tuple (min, max) (default: None)
652
651
  - stun_servers: List of STUN server URLs (default: None, uses Google STUN)
653
652
  - turn_servers: List of TURN servers in format "username;credential;url" (default: None)
@@ -669,7 +668,6 @@ async def serve(
669
668
  host=kwargs.get("host", "0.0.0.0"),
670
669
  port=kwargs.get("port", 8080),
671
670
  orphan_timeout=kwargs.get("orphan_timeout", 30.0),
672
- max_session_duration=kwargs.get("max_session_duration"),
673
671
  webrtc_port_range=kwargs.get("webrtc_port_range"),
674
672
  stun_servers=kwargs.get("stun_servers"),
675
673
  turn_servers=kwargs.get("turn_servers"),
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+
3
+ from reactor_runtime.transports.gstreamer.probes.fps_probe import FpsProbe
4
+
5
+ __all__ = ["FpsProbe"]
@@ -0,0 +1,119 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+
3
+ """
4
+ Buffer-count FPS probes on GStreamer pads.
5
+ Each :class:`FpsProbe` instance is independent: attach one probe per pad /
6
+ measurement site (e.g. after ``videoconvert``, after an encoder, on a queue src pad).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from typing import Any, Optional
13
+
14
+ from reactor_runtime.transports.gstreamer.gst import Gst
15
+ from reactor_runtime.utils.log import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class FpsProbe:
21
+ """
22
+ Install a ``Gst.PadProbeType.BUFFER`` probe that reports buffers per second
23
+ over a rolling wall-clock window (default 1s of monotonic time).
24
+ Instances do not share state. Typical usage::
25
+ probe_videoconvert = FpsProbe("video_after_videoconvert")
26
+ probe_videoconvert.attach(videoconvert.get_static_pad("src"))
27
+ probe_encoder = FpsProbe("video_after_encoder")
28
+ probe_encoder.attach(encoder_element.get_static_pad("src"))
29
+ ``last_fps`` is updated at the end of each completed window. Each completed
30
+ window is logged at INFO.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ name: str,
36
+ *,
37
+ window_ns: Optional[int] = None,
38
+ ) -> None:
39
+ self._name = name
40
+ self._window_ns = int(Gst.SECOND if window_ns is None else window_ns)
41
+ if self._window_ns <= 0:
42
+ raise ValueError("window_ns must be positive")
43
+
44
+ self._pad: Optional[Gst.Pad] = None
45
+ self._probe_id: Optional[int] = None
46
+ self._window_start_ns: Optional[int] = None
47
+ self._count = 0
48
+ self._last_fps: Optional[float] = None
49
+
50
+ @property
51
+ def name(self) -> str:
52
+ return self._name
53
+
54
+ @property
55
+ def last_fps(self) -> Optional[float]:
56
+ """Buffers per second from the last completed ``window_ns`` interval."""
57
+ return self._last_fps
58
+
59
+ def attach(self, pad: Gst.Pad) -> None:
60
+ """
61
+ Add this probe on *pad* (usually a src pad, downstream of the element under test).
62
+ Raises:
63
+ RuntimeError: if this instance already has a probe installed.
64
+ """
65
+ if self._probe_id:
66
+ raise RuntimeError(
67
+ f"FpsProbe({self._name!r}) is already attached; call detach() first"
68
+ )
69
+ self._pad = pad
70
+ self._window_start_ns = None
71
+ self._count = 0
72
+ self._probe_id = pad.add_probe(
73
+ Gst.PadProbeType.BUFFER,
74
+ self._buffer_probe,
75
+ None, # user_data (PyGObject passes this as the 3rd callback arg)
76
+ )
77
+ if not self._probe_id:
78
+ self._pad = None
79
+ raise RuntimeError(f"FpsProbe({self._name!r}): pad.add_probe() failed")
80
+
81
+ def detach(self) -> None:
82
+ """Remove the probe from the pad, if installed."""
83
+ if self._pad is not None and self._probe_id is not None:
84
+ self._pad.remove_probe(self._probe_id)
85
+ self._pad = None
86
+ self._probe_id = None
87
+ self._window_start_ns = None
88
+ self._count = 0
89
+ self._last_fps = None
90
+
91
+ def _buffer_probe(
92
+ self,
93
+ _pad: Gst.Pad,
94
+ info: Gst.PadProbeInfo,
95
+ _user_data: Any,
96
+ ) -> Gst.PadProbeReturn:
97
+ buf = info.get_buffer()
98
+ if buf is None:
99
+ return Gst.PadProbeReturn.OK
100
+
101
+ now = time.monotonic_ns()
102
+ window_start = self._window_start_ns
103
+ if window_start is None:
104
+ self._window_start_ns = now
105
+ return Gst.PadProbeReturn.OK
106
+
107
+ self._count += 1
108
+ elapsed = now - window_start
109
+ if elapsed >= self._window_ns:
110
+ # buffers / second for the window that just ended
111
+ fps = self._count * 1_000_000_000 / elapsed
112
+ self._last_fps = fps
113
+ logger.info(
114
+ f"gstreamer_fps_probe name={self._name} fps={fps:.2f} buffers={self._count} window_s={elapsed / 1e9:.3f}",
115
+ )
116
+ self._window_start_ns = now
117
+ self._count = 0
118
+
119
+ return Gst.PadProbeReturn.OK
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -101,6 +101,8 @@ src/reactor_runtime/transports/gstreamer/encoders/h265.py
101
101
  src/reactor_runtime/transports/gstreamer/encoders/opus.py
102
102
  src/reactor_runtime/transports/gstreamer/encoders/vp8.py
103
103
  src/reactor_runtime/transports/gstreamer/encoders/vp9.py
104
+ src/reactor_runtime/transports/gstreamer/probes/__init__.py
105
+ src/reactor_runtime/transports/gstreamer/probes/fps_probe.py
104
106
  src/reactor_runtime/transports/gstreamer/receiver/__init__.py
105
107
  src/reactor_runtime/transports/gstreamer/receiver/audio.py
106
108
  src/reactor_runtime/transports/gstreamer/receiver/base.py