reactor-runtime 2.9.2__tar.gz → 2.9.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 (135) hide show
  1. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/PKG-INFO +1 -1
  2. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/pyproject.toml +1 -1
  3. reactor_runtime-2.9.4/src/reactor_runtime/realtime/pipeline.py +426 -0
  4. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/client.py +38 -5
  5. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +2 -0
  6. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +19 -7
  7. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +18 -19
  8. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +2 -0
  9. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +2 -0
  10. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +2 -0
  11. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/video.py +25 -1
  12. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/settings.py +27 -0
  13. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/PKG-INFO +1 -1
  14. reactor_runtime-2.9.2/src/reactor_runtime/realtime/pipeline.py +0 -304
  15. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/README.md +0 -0
  16. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/setup.cfg +0 -0
  17. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/api/__init__.py +0 -0
  18. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/__init__.py +0 -0
  19. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/config.py +0 -0
  20. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/experiment/__init__.py +0 -0
  21. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/experiment/session.py +0 -0
  22. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/__init__.py +0 -0
  23. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/client.py +0 -0
  24. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/defaults.py +0 -0
  25. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  26. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
  27. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  28. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/__init__.py +0 -0
  29. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/connected.py +0 -0
  30. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/event.py +0 -0
  31. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/messages.py +0 -0
  32. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/upload.py +0 -0
  33. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  34. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  35. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  36. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  37. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/__init__.py +0 -0
  38. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/decorators.py +0 -0
  39. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/handlers.py +0 -0
  40. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  41. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  42. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  43. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  44. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
  45. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  46. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  47. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/input.py +0 -0
  48. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/output.py +0 -0
  49. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/upload.py +0 -0
  50. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/model_state.py +0 -0
  51. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/__init__.py +0 -0
  52. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  53. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/base.py +0 -0
  54. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/file.py +0 -0
  55. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  56. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/helpers.py +0 -0
  57. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/nvml_sampler.py +0 -0
  58. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  59. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  60. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/profiler.py +0 -0
  61. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/singleton.py +0 -0
  62. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/torch_chunk_profiler.py +0 -0
  63. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/realtime/__init__.py +0 -0
  64. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/__init__.py +0 -0
  65. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  66. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
  67. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/config.py +0 -0
  68. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/markers.py +0 -0
  69. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/session_recorder.py +0 -0
  70. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/sinks.py +0 -0
  71. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/track_resolver.py +0 -0
  72. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtime_api.py +0 -0
  73. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtimes/http/config.py +0 -0
  74. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtimes/http/http_runtime.py +0 -0
  75. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtimes/http/types.py +0 -0
  76. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/schema.py +0 -0
  77. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/schema_validator.py +0 -0
  78. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/__init__.py +0 -0
  79. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/__main__.py +0 -0
  80. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/commands/__init__.py +0 -0
  81. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/commands/run.py +0 -0
  82. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/commands/schema.py +0 -0
  83. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/main.py +0 -0
  84. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/utils/__init__.py +0 -0
  85. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/utils/config.py +0 -0
  86. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/utils/runtime.py +0 -0
  87. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/__init__.py +0 -0
  88. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/config.py +0 -0
  89. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/events.py +0 -0
  90. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  91. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  92. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  93. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  94. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  95. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  96. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  97. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
  98. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  99. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  100. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  101. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  102. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  103. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  104. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  105. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
  106. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
  107. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/quality.py +0 -0
  108. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  109. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  110. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  111. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  112. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  113. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  114. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  115. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  116. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  117. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  118. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  119. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  120. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  121. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/ice_uris.py +0 -0
  122. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/interface.py +0 -0
  123. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/media.py +0 -0
  124. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/types.py +0 -0
  125. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/launch.py +0 -0
  126. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/loader.py +0 -0
  127. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/log.py +0 -0
  128. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/messages.py +0 -0
  129. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/paths.py +0 -0
  130. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/ports.py +0 -0
  131. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/typing.py +0 -0
  132. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/SOURCES.txt +0 -0
  133. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  134. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/requires.txt +0 -0
  135. {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/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.9.2
3
+ Version: 2.9.4
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.9.2"
7
+ version = "2.9.4"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -0,0 +1,426 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """
3
+ :class:`RealtimePipeline` — the thin runtime bridge to a standalone engine.
4
+
5
+ The engine (the ``realtime_engine`` package) owns the scheduling loop and all
6
+ ML. It is driven only through inbox/outbox queues and has **zero**
7
+ ``reactor_runtime`` dependency. This bridge is the runtime-side caller: it
8
+ inherits everything from :class:`ReactorPipeline` — typed :class:`InputState`,
9
+ auto ``set_<field>`` events, :class:`Output` wrapping, ``emit`` / FPS pacing,
10
+ and the connection lifecycle — and adds only the queue plumbing::
11
+
12
+ connect -> Attach(session_id, attach_context)
13
+ per tick -> Action(session_id, conditioning) (state_to_conditioning, on change)
14
+ @event reset -> Reset(session_id, attach_context) (edge-triggered)
15
+ Chunk -> on_output (default: emit media; override to send a message)
16
+
17
+ **One conditioning path.** A single loop runs at the engine's ``rate_hz``,
18
+ snapshots :meth:`state_to_conditioning` under ``_gen_lock``, and enqueues an
19
+ ``Action`` only when the conditioning changed. Everything the engine conditions
20
+ on flows through this one path — control fields (``self.state``) *and* any input
21
+ media a model pulls from its input tracks inside ``state_to_conditioning``.
22
+ ``default_serve`` keeps the last conditioning between ticks and applies the
23
+ newest at the next chunk boundary, so tick-cadence sampling costs no more latency
24
+ than event-driven pushes (the engine only applies at boundaries either way) while
25
+ keeping a single, un-branched code path — matching the design doc (the runtime
26
+ snapshots state → inbox at chunk boundaries) and how vLLM-Omni / sglang drain
27
+ conditioning at chunk boundaries and generate from the last value otherwise.
28
+
29
+ The bridge owns **no** scheduling, **no** ML, and **no** state machine; if it
30
+ grows logic, that logic belongs in the engine (its ``serve`` loop) or in a
31
+ hook, not here.
32
+
33
+ Subclasses declare a typed ``state: MyState`` and an :class:`Output` subclass,
34
+ then override the small hooks: :meth:`build_engine`, :meth:`_attach_context`,
35
+ :meth:`state_to_conditioning`, :meth:`on_output`. A media-in model (v2v, a VLA
36
+ policy) reads its input tracks (``self._input_buffers``) inside
37
+ ``state_to_conditioning`` and returns the frames as ordinary conditioning keys —
38
+ no special flag or reserved field; the loop above carries them like any other
39
+ conditioning.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import asyncio
45
+ import copy
46
+ import itertools
47
+ from collections.abc import Mapping
48
+ from typing import Any, Optional
49
+
50
+ import numpy as np
51
+
52
+ from reactor_runtime.interface import BufferClosed
53
+ from reactor_runtime.interface.pipeline.reactor_pipeline import ReactorPipeline
54
+ from reactor_runtime.utils.log import get_logger
55
+
56
+ # Optional dependency: the runtime does NOT depend on the engine/contract. This
57
+ # guarded import lets a plain ``reactor_runtime`` install (and non-engine models)
58
+ # work without it — importing this module never fails. Only engine-backed models
59
+ # need the contract, and ``RealtimePipeline.__init__`` fails clearly (below) if
60
+ # it's absent, instead of a bare ``ModuleNotFoundError``.
61
+ try:
62
+ from realtime_engine import (
63
+ Action,
64
+ Attach,
65
+ Detach,
66
+ Inbox,
67
+ Outbox,
68
+ RealtimeInterface,
69
+ Reset,
70
+ run_engine,
71
+ )
72
+
73
+ HAS_REALTIME_ENGINE = True
74
+ except ImportError:
75
+ HAS_REALTIME_ENGINE = False
76
+
77
+ logger = get_logger(__name__)
78
+
79
+
80
+ class RealtimePipeline(ReactorPipeline):
81
+ """Drive a queue/``serve`` :class:`RealtimeInterface` engine from the runtime.
82
+
83
+ Override :meth:`run` is **not** needed — this class provides the queue
84
+ loop. Subclasses provide the model surface (state + output track) and the
85
+ hooks. ``inference()`` is intentionally unused: the engine, not a
86
+ generator, produces frames.
87
+ """
88
+
89
+ # Session-liveness poll interval. This only governs how quickly the bridge
90
+ # notices connect/disconnect transitions to issue Attach/Detach — it is NOT
91
+ # on the frame path (chunks flow through an independent drain task + the
92
+ # engine's own loop), so a coarse interval is fine.
93
+ _lifecycle_poll_s: float = 0.02
94
+
95
+ def __init__(self) -> None:
96
+ if not HAS_REALTIME_ENGINE:
97
+ raise ImportError(
98
+ "RealtimePipeline (engine-backed models) requires the realtime "
99
+ "contract package. Install it with "
100
+ "`pip install reactor-realtime-engine`."
101
+ )
102
+ super().__init__()
103
+ self._engine: Optional[RealtimeInterface] = None
104
+ self._inbox: Optional[Inbox] = None
105
+ self._sid: Optional[str] = None
106
+ self._last_cond: Optional[dict] = None
107
+ self._sid_counter = itertools.count(1)
108
+ # Conditioning-loop cadence; set from the manifest in run(). Initialized
109
+ # here so the attr always has a value (and tests can set it directly).
110
+ self._rate_hz: float = 1.0
111
+
112
+ # ------------------------------------------------------------------
113
+ # load() — build the engine (no event-loop primitives here)
114
+ # ------------------------------------------------------------------
115
+
116
+ def load(self, config: dict[str, Any]) -> None:
117
+ """Build the engine once, before any client connects.
118
+
119
+ Queues / events are created in :meth:`run` instead — ``load()`` runs
120
+ before the event loop exists.
121
+ """
122
+ self._engine = self.build_engine(config)
123
+
124
+ # ------------------------------------------------------------------
125
+ # run() — the queue-plumbing loop (replaces the generator loop)
126
+ # ------------------------------------------------------------------
127
+
128
+ async def run(self) -> None:
129
+ engine = self._engine
130
+ if engine is None:
131
+ raise RuntimeError(
132
+ f"{type(self).__name__}.load() must build an engine before run()"
133
+ )
134
+
135
+ inbox, outbox, stop = Inbox(), Outbox(), asyncio.Event()
136
+ self._inbox = inbox
137
+
138
+ # Pace emission to the engine's declared chunk cadence — NOT to its
139
+ # per-step compute time. The engine's serve loop already ticks at
140
+ # rate_hz; deriving fps from step_ms (tiny on a fast engine) would tell
141
+ # the output buffer to drain far faster than chunks actually arrive.
142
+ # frames_per_chunk is declared by the manifest (not inferred from
143
+ # chunk_shape, which is an opaque output shape).
144
+ manifest = engine.manifest()
145
+ self.output_buffer.set_fps(
146
+ max(1.0, manifest.rate_hz * manifest.frames_per_chunk)
147
+ )
148
+ # The conditioning loop samples state at the engine's tick cadence.
149
+ self._rate_hz = max(1.0, manifest.rate_hz)
150
+
151
+ lock = self._gen_lock
152
+ if lock is None:
153
+ raise RuntimeError(
154
+ f"{type(self).__name__}.run() called before _init_async()"
155
+ )
156
+
157
+ engine_task = asyncio.create_task(run_engine(engine, inbox, outbox, stop=stop))
158
+ try:
159
+ while True:
160
+ # Fresh state per connection (mirrors ReactorPipeline.run()).
161
+ self.state = self._state_cls()
162
+ self._sid = None
163
+ self._last_cond = None
164
+ await self.connected.wait()
165
+
166
+ # Attach only after the @connected handler has initialized state.
167
+ # That handler runs under _gen_lock (ReactorPipeline._invoke), and
168
+ # the dispatcher acquires the lock before it yields, so taking the
169
+ # lock here orders us strictly after it — otherwise a model that
170
+ # sets seed/prompt/image in @connected would attach with defaults.
171
+ async with lock:
172
+ sid = f"s{next(self._sid_counter)}"
173
+ self._sid = sid
174
+ # deepcopy: the inbox payload must be a self-contained
175
+ # snapshot — a nested ndarray/list in the context stays a
176
+ # live alias of self.state under a shallow dict() copy, so a
177
+ # later in-place mutation could change an already-enqueued msg.
178
+ inbox.put_nowait(
179
+ Attach(sid, copy.deepcopy(dict(self._attach_context())))
180
+ )
181
+ self._push_action_if_changed() # seed initial conditioning
182
+
183
+ # The single conditioning path: sample state_to_conditioning() at
184
+ # the engine's cadence and push on change. Started after Attach so
185
+ # the engine has a session to condition.
186
+ #
187
+ # Why this is a separate task from the drain loop below (they are
188
+ # NOT two competing rates): the two directions have different
189
+ # natural triggers. INBOUND (client->engine) is time-driven — we
190
+ # sample conditioning at the engine's step cadence (rate_hz), so
191
+ # it's a fixed-rate loop. OUTBOUND (engine->client) is
192
+ # event-driven — we emit each chunk the instant it lands on the
193
+ # outbox, so that loop blocks on outbox.get(). Keeping them
194
+ # separate lets output emit with no added latency while
195
+ # conditioning samples on its own clock; folding both into one
196
+ # loop would gate one direction on the other's timing.
197
+ cond_task = asyncio.create_task(self._conditioning_loop(sid))
198
+
199
+ # Drain chunks -> emit, and notice disconnect. This loop is paced
200
+ # by chunk ARRIVAL (i.e. rate_hz), not by _lifecycle_poll_s:
201
+ # wait_for only bounds a single get() so a paused engine (no
202
+ # chunks) still re-checks liveness and detaches promptly instead
203
+ # of blocking forever — it's a liveness timeout, not the emit
204
+ # rate. A timed-out get() leaves any in-flight chunk on the queue
205
+ # (get is cancellation-safe), so the next iteration picks it up.
206
+ try:
207
+ while self.connected.is_set():
208
+ try:
209
+ chunk = await asyncio.wait_for(
210
+ outbox.get(), self._lifecycle_poll_s
211
+ )
212
+ except asyncio.TimeoutError:
213
+ continue # no chunk this interval — re-check liveness
214
+ if chunk.session_id != sid:
215
+ continue # a chunk for a previous/other session — drop it
216
+ try:
217
+ await self.on_output(chunk.output)
218
+ except Exception:
219
+ # A model bug in on_output (e.g. a translation raising
220
+ # on one malformed chunk) must not kill the session —
221
+ # log, drop this chunk, keep draining. Parity with
222
+ # _conditioning_loop; a real disconnect is still caught
223
+ # by the connected/wait_for liveness check above.
224
+ logger.exception(
225
+ "on_output failed for a chunk; dropping it "
226
+ "(session survives)"
227
+ )
228
+ finally:
229
+ # Stop the conditioning loop before clearing identity so it
230
+ # can't enqueue an Action after the Detach below.
231
+ cond_task.cancel()
232
+ # Clear identity BEFORE enqueuing Detach: a racing tick then
233
+ # sees no session and won't enqueue an Action after the Detach
234
+ # (the local sid stays valid here).
235
+ self._sid = None
236
+ self.state = None
237
+ inbox.put_nowait(Detach(sid))
238
+ for buf in self._input_buffers.values():
239
+ buf.reset()
240
+ self.output_buffer.flush()
241
+ await _await_quietly(cond_task)
242
+ finally:
243
+ stop.set()
244
+ self._inbox = None
245
+ await _await_quietly(engine_task)
246
+
247
+ # ------------------------------------------------------------------
248
+ # Conditioning — one tick-cadence loop, push on change
249
+ # ------------------------------------------------------------------
250
+
251
+ async def _conditioning_loop(self, sid: str) -> None:
252
+ """Snapshot conditioning at the engine's cadence; enqueue on change.
253
+
254
+ The sole runtime→engine conditioning path. Each tick, under ``_gen_lock``
255
+ (so we never snapshot state torn mid-mutation by an async ``@event`` /
256
+ ``@connected`` handler), recompute :meth:`state_to_conditioning` and push
257
+ one ``Action`` only if it changed vs the last. Control fields and any
258
+ input media a model reads inside ``state_to_conditioning`` flow through
259
+ here identically. ``default_serve`` reuses the last conditioning when no
260
+ new ``Action`` arrives, so unchanged state needs no message. A
261
+ :class:`BufferClosed` (a model reading a closed input track after the
262
+ client left) or cancellation ends the loop; teardown issues the Detach.
263
+ """
264
+ interval = 1.0 / self._rate_hz
265
+ while self._inbox is not None and self._sid == sid and self.state is not None:
266
+ try:
267
+ async with self._gen_lock:
268
+ self._push_action_if_changed()
269
+ except BufferClosed:
270
+ return # a model read a closed input track; teardown does Detach
271
+ except Exception:
272
+ # A model bug in state_to_conditioning must not silently freeze
273
+ # conditioning for the rest of the session — log and keep ticking
274
+ # (mirrors default_serve's "skip the tick, the loop survives").
275
+ logger.exception(
276
+ "conditioning snapshot failed; skipping tick (loop survives)"
277
+ )
278
+ await asyncio.sleep(interval)
279
+
280
+ def _push_action_if_changed(self) -> None:
281
+ """Recompute conditioning; enqueue an ``Action`` only when it changed.
282
+
283
+ Called under ``_gen_lock`` (from :meth:`_conditioning_loop`, and once at
284
+ Attach to seed). Kept side-effect-cheap so holding the lock is brief.
285
+ """
286
+ if self._inbox is None or self._sid is None or self.state is None:
287
+ return
288
+ # deepcopy so the enqueued Action is a self-contained snapshot: a nested
289
+ # ndarray/list (e.g. an input frame a model pulled into conditioning)
290
+ # would otherwise alias live storage and could be mutated in place after
291
+ # enqueue (also keeps _last_cond a stable comparison base).
292
+ cond = copy.deepcopy(dict(self.state_to_conditioning()))
293
+ if _conds_equal(cond, self._last_cond):
294
+ return
295
+ self._last_cond = cond
296
+ self._inbox.put_nowait(Action(self._sid, cond))
297
+
298
+ def reset_session(self) -> None:
299
+ """Reset the live session's episode (edge-triggered — deliver once).
300
+
301
+ Call from an ``@event`` handler for a discrete command (e.g. episode
302
+ reset), NOT via conditioning: conditioning is sampled every tick, so a
303
+ reset carried there would drop (two within a tick) or repeat (a lingering
304
+ flag fires every step). ``default_serve`` turns this ``Reset`` into
305
+ detach + re-attach, so the engine rebuilds the session fresh (clears any
306
+ per-session state — frame buffers, step count) and re-seeds from a fresh
307
+ attach context. No-op if there's no live session.
308
+
309
+ Also clears the runtime-side carry-over so the new episode starts clean:
310
+ the input-track buffers (else a media-in model's next
311
+ ``state_to_conditioning`` replays stale pre-reset frames) and
312
+ ``_last_cond`` — so the next conditioning tick re-seeds the rebuilt
313
+ session (which ``default_serve`` re-attaches with empty conditioning).
314
+ """
315
+ if self._inbox is None or self._sid is None or self.state is None:
316
+ return
317
+ for buf in self._input_buffers.values():
318
+ buf.reset()
319
+ self._last_cond = None # force the next tick to re-seed the rebuilt session
320
+ self._inbox.put_nowait(
321
+ Reset(self._sid, copy.deepcopy(dict(self._attach_context())))
322
+ )
323
+
324
+ # ------------------------------------------------------------------
325
+ # Hooks — subclasses override these
326
+ # ------------------------------------------------------------------
327
+
328
+ def build_engine(self, config: dict[str, Any]) -> RealtimeInterface:
329
+ """Construct the engine. Required override."""
330
+ raise NotImplementedError(
331
+ f"{type(self).__name__} must implement build_engine()"
332
+ )
333
+
334
+ def _state_snapshot(self) -> dict[str, Any]:
335
+ """Public state fields as a plain dict (the default hook payload)."""
336
+ if self.state is None:
337
+ return {}
338
+ return {
339
+ name: getattr(self.state, name) for name in type(self.state)._public_fields
340
+ }
341
+
342
+ def _attach_context(self) -> Mapping[str, Any]:
343
+ """Seed context for :class:`Attach` (image / scene / prompt / seed).
344
+
345
+ Defaults to a snapshot of public state at connect time; override to
346
+ narrow it (e.g. only the seed).
347
+ """
348
+ return self._state_snapshot()
349
+
350
+ def state_to_conditioning(self) -> Mapping[str, Any]:
351
+ """Adapter from ``self.state`` (+ input tracks) to engine conditioning.
352
+
353
+ The single State/input -> ``dict[str, Any]`` seam for :class:`Action`,
354
+ sampled once per tick by :meth:`_conditioning_loop`. Override it to
355
+ decouple your state shape from the engine's conditioning vocabulary —
356
+ rename fields, drop internal-only fields, or derive values the engine
357
+ wants but you don't expose in state. Defaults to a snapshot of public
358
+ state.
359
+
360
+ **Media-in models** (v2v, VLA policies) read their input tracks here and
361
+ return the frames as ordinary conditioning keys. **Retain the last frame
362
+ across gap ticks** — a ``LATEST`` read returns ``None`` when no fresh
363
+ frame arrived, and dropping the key would push a key-less ``Action`` that
364
+ flaps the track out of the engine's conditioning until frames resume
365
+ (``default_serve`` replaces conditioning wholesale). Keep the key on the
366
+ last value instead::
367
+
368
+ def state_to_conditioning(self):
369
+ frames = self.input.camera.try_read(1, mode=ReadMode.LATEST)
370
+ if frames:
371
+ self._last_camera = frames[0].data
372
+ cond = {"prompt": self.state.prompt}
373
+ if self._last_camera is not None:
374
+ cond["camera"] = self._last_camera
375
+ return cond
376
+
377
+ (If the engine keeps its own per-track history and tolerates a missing
378
+ key on a gap tick — e.g. GR00T's step appends only when the key is
379
+ present — you can drop the key instead; retain-last is the safe default.)
380
+
381
+ The loop deep-copies the result into the ``Action`` (snapshot safety), so
382
+ returned frame arrays are safe. Change-detection then suppresses
383
+ byte-identical snapshots — fine for content-conditioning (``default_serve``
384
+ reuses the last, identical value); a model that must forward *every* frame
385
+ object regardless of content can include a monotonic counter/timestamp key
386
+ to defeat suppression.
387
+
388
+ Note: change-detection between ticks compares the returned dict; array
389
+ values are handled with :func:`numpy.array_equal`.
390
+ """
391
+ return self._state_snapshot()
392
+
393
+ async def on_output(self, output: Any) -> None:
394
+ """Put one engine chunk on the wire — the single emission seam.
395
+
396
+ Default: the opaque engine ndarray is wrapped into the model's
397
+ single-track :class:`Output` (:meth:`_resolve_output`) and emitted
398
+ (FPS-paced). Override to send a data-channel message, emit multiple
399
+ tracks, or translate however the model needs — e.g. a VLA policy's action
400
+ chunk goes to ``send`` instead of ``emit``.
401
+ """
402
+ await self.emit(self._resolve_output(output))
403
+
404
+
405
+ def _conds_equal(a: Optional[dict], b: Optional[dict]) -> bool:
406
+ """Dict equality that tolerates ndarray values (plain ``==`` on arrays
407
+ yields an array, and ``bool()`` of that raises ``ValueError``)."""
408
+ if a is None or b is None:
409
+ return a is b
410
+ if a.keys() != b.keys():
411
+ return False
412
+ for key in a:
413
+ av, bv = a[key], b[key]
414
+ if isinstance(av, np.ndarray) or isinstance(bv, np.ndarray):
415
+ if not np.array_equal(av, bv):
416
+ return False
417
+ elif av != bv:
418
+ return False
419
+ return True
420
+
421
+
422
+ async def _await_quietly(task: asyncio.Task) -> None:
423
+ try:
424
+ await task
425
+ except asyncio.CancelledError:
426
+ pass
@@ -59,7 +59,13 @@ from reactor_runtime.transports.types import (
59
59
  )
60
60
 
61
61
  from reactor_runtime.utils.log import get_logger
62
- from reactor_runtime.transports.gstreamer.gst import Gst, GstSdp, GLib, GstWebRTC
62
+ from reactor_runtime.transports.gstreamer.gst import (
63
+ Gst,
64
+ GstSdp,
65
+ GLib,
66
+ GstVideo,
67
+ GstWebRTC,
68
+ )
63
69
  from reactor_runtime.transports.gstreamer.gst_helpers import (
64
70
  add_many,
65
71
  link_pads,
@@ -143,6 +149,32 @@ def _gst_buffer_pts_seconds(buf: object) -> Optional[float]:
143
149
  return None
144
150
 
145
151
 
152
+ def _rgb_frame_from_mapped_buffer(
153
+ data: bytes | bytearray | memoryview, width: int, height: int, stride: int
154
+ ) -> np.ndarray:
155
+ """Copy a mapped RGB buffer into a tightly packed ``(H, W, 3)`` array.
156
+
157
+ GStreamer pads each row up to a 4-byte boundary, so ``stride`` (the bytes
158
+ the negotiated caps reserve per row) can exceed ``width * 3``. Reading at
159
+ the true stride and dropping the trailing padding keeps the rows from
160
+ shearing and the channels from rotating into grayscale.
161
+
162
+ Args:
163
+ data: The mapped buffer memory (anything exposing the buffer protocol).
164
+ width: Frame width in pixels.
165
+ height: Frame height in pixels.
166
+ stride: Bytes per row including padding, from the negotiated caps.
167
+
168
+ Returns:
169
+ An owned ``(height, width, 3)`` uint8 array with padding removed.
170
+ """
171
+ row_bytes = width * 3
172
+ if stride == row_bytes:
173
+ return np.ndarray((height, width, 3), dtype=np.uint8, buffer=data).copy()
174
+ padded = np.ndarray((height, stride), dtype=np.uint8, buffer=data)
175
+ return np.ascontiguousarray(padded[:, :row_bytes]).reshape(height, width, 3)
176
+
177
+
146
178
  # =============================================================================
147
179
  # GStreamerTransport
148
180
  # =============================================================================
@@ -1201,10 +1233,11 @@ class GStreamerTransport(WebRTCTransport):
1201
1233
  return Gst.FlowReturn.OK
1202
1234
 
1203
1235
  try:
1204
- # Create numpy array from buffer (RGB format)
1205
- frame: np.ndarray = np.ndarray(
1206
- shape=(height, width, 3), dtype=np.uint8, buffer=map_info.data
1207
- ).copy() # Copy to own the data
1236
+ # Read at the true row stride from the negotiated caps so padded
1237
+ # rows (width * 3 not 4-aligned) don't shear into grayscale.
1238
+ video_info = GstVideo.VideoInfo.new_from_caps(caps)
1239
+ stride = video_info.stride[0] if video_info is not None else width * 3
1240
+ frame = _rgb_frame_from_mapped_buffer(map_info.data, width, height, stride)
1208
1241
 
1209
1242
  self._gst_emit_event(
1210
1243
  VideoFrameEvent(
@@ -4,6 +4,7 @@ from reactor_runtime.transports.gstreamer.gst_helpers import (
4
4
  make_element,
5
5
  try_set_property,
6
6
  )
7
+ from reactor_runtime.transports.gstreamer.settings import RTP_PAYLOAD_MTU
7
8
  from .base import BaseEncoderBin
8
9
 
9
10
 
@@ -71,6 +72,7 @@ class AV1EncoderBin(BaseEncoderBin):
71
72
 
72
73
  # Set RTP payload type to match negotiated PT
73
74
  self._pay.set_property("pt", self._pt)
75
+ try_set_property(self._pay, "mtu", RTP_PAYLOAD_MTU)
74
76
  if ssrc is not None:
75
77
  try_set_property(self._pay, "ssrc", ssrc)
76
78
 
@@ -1,7 +1,10 @@
1
1
  from typing import Optional
2
2
 
3
3
  from reactor_runtime.transports.gstreamer.gst import Gst
4
- from reactor_runtime.transports.gstreamer.settings import HW_CODECS_ENABLED
4
+ from reactor_runtime.transports.gstreamer.settings import (
5
+ HW_CODECS_ENABLED,
6
+ RTP_PAYLOAD_MTU,
7
+ )
5
8
  from reactor_runtime.transports.gstreamer.gst_helpers import (
6
9
  link_many,
7
10
  make_element,
@@ -156,6 +159,7 @@ class H264EncoderBin(BaseEncoderBin):
156
159
 
157
160
  # Set RTP payload type
158
161
  self._pay.set_property("pt", self._pt)
162
+ try_set_property(self._pay, "mtu", RTP_PAYLOAD_MTU)
159
163
 
160
164
  # config-interval=1 forces SPS/PPS to be sent periodically.
161
165
  # Important for WebRTC because:
@@ -166,13 +170,21 @@ class H264EncoderBin(BaseEncoderBin):
166
170
  try_set_property(self._pay, "ssrc", ssrc)
167
171
 
168
172
  # ---------------------------------------------------------
169
- # Enforce negotiated profile/level
173
+ # Enforce negotiated profile only — NOT level.
170
174
  # ---------------------------------------------------------
171
- profile, level = h264_plid_to_gst_profile_level(profile_level_id)
172
-
173
- caps = Gst.Caps.from_string(
174
- f"video/x-h264,profile=(string){profile},level=(string){level}"
175
- )
175
+ # The SDP profile-level-id encodes a level (e.g. 42e01f -> 3.1) that is
176
+ # only valid up to ~720p. Pinning that level in the capsfilter caps the
177
+ # encoder below the actual resolution: at 2K the encoded stream is
178
+ # ~level 5.x, the level-3.1 capsfilter fails to negotiate, and the whole
179
+ # send branch stalls with `not-negotiated` — zero RTP ever leaves. VP8
180
+ # (no level) is unaffected, which is why it works over the same path.
181
+ #
182
+ # Pin the profile only. x264/NVENC pick a level valid for the resolution,
183
+ # and receivers (incl. Safari) match on the profile, not the exact level.
184
+ # (`h264_plid_to_gst_profile_level` still validates the negotiated id.)
185
+ profile, _level = h264_plid_to_gst_profile_level(profile_level_id)
186
+
187
+ caps = Gst.Caps.from_string(f"video/x-h264,profile=(string){profile}")
176
188
 
177
189
  try_set_property(self._capsfilter, "caps", caps)
178
190
 
@@ -1,7 +1,10 @@
1
1
  from typing import Dict, Optional
2
2
 
3
3
  from reactor_runtime.transports.gstreamer.gst import Gst
4
- from reactor_runtime.transports.gstreamer.settings import HW_CODECS_ENABLED
4
+ from reactor_runtime.transports.gstreamer.settings import (
5
+ HW_CODECS_ENABLED,
6
+ RTP_PAYLOAD_MTU,
7
+ )
5
8
  from reactor_runtime.transports.gstreamer.gst_helpers import (
6
9
  make_element,
7
10
  try_set_property,
@@ -13,11 +16,19 @@ def h265_fmtp_to_x265_option_string(fmtp: Dict[str, Optional[str]]) -> str:
13
16
  """
14
17
  Map HEVC SDP fmtp (RFC 7798) keys to ``x265enc`` ``option-string`` tokens.
15
18
 
16
- SDP ``tier-flag`` and ``level-id`` are mapped to ``high-tier`` and
17
- ``level-idc`` respectively. ``profile-id`` is intentionally omitted:
18
- GStreamer's x265enc enforces profile through caps negotiation (calling
19
- x265_param_apply_profile internally), not via x265_param_parse — passing
20
- ``profile=...`` in option-string causes an encoder init failure.
19
+ SDP ``tier-flag`` is mapped to ``high-tier``. ``profile-id`` is
20
+ intentionally omitted: GStreamer's x265enc enforces profile through caps
21
+ negotiation (calling x265_param_apply_profile internally), not via
22
+ x265_param_parse — passing ``profile=...`` in option-string causes an
23
+ encoder init failure.
24
+
25
+ ``level-id`` is intentionally NOT mapped to ``level-idc``. The offered
26
+ level-id advertises the receiver's decode ceiling, and forcing it onto
27
+ the encoder makes x265 refuse to initialize for any frame above that
28
+ level's budget (Safari offers level-id=93 = level 3.1, ~720p), silently
29
+ stalling the sender branch. x265 derives a resolution-valid level and
30
+ writes it into the bitstream; receivers decode based on that, and in
31
+ practice tolerate streams above their advertised ceiling.
21
32
 
22
33
  Unrecognized or missing values are omitted so the encoder can use defaults.
23
34
  """
@@ -36,19 +47,6 @@ def h265_fmtp_to_x265_option_string(fmtp: Dict[str, Optional[str]]) -> str:
36
47
  elif tier == "0":
37
48
  parts.append("high-tier=0")
38
49
 
39
- level = _get("level-id")
40
- if level is not None:
41
- try:
42
- lid = int(level)
43
- except ValueError:
44
- lid = -1
45
- if 0 <= lid <= 255:
46
- level_idc = lid / 30
47
- if level_idc == int(level_idc):
48
- parts.append(f"level-idc={int(level_idc)}")
49
- else:
50
- parts.append(f"level-idc={level_idc:.1f}")
51
-
52
50
  return ":".join(parts)
53
51
 
54
52
 
@@ -122,6 +120,7 @@ class H265EncoderBin(BaseEncoderBin):
122
120
 
123
121
  # Set RTP payload type to match SDP negotiation
124
122
  self._pay.set_property("pt", self._pt)
123
+ try_set_property(self._pay, "mtu", RTP_PAYLOAD_MTU)
125
124
 
126
125
  # config-interval=1 ensures VPS/SPS/PPS are periodically sent.
127
126
  # Important for:
@@ -17,6 +17,7 @@ from reactor_runtime.transports.gstreamer.gst_helpers import (
17
17
  make_element,
18
18
  try_set_property,
19
19
  )
20
+ from reactor_runtime.transports.gstreamer.settings import RTP_PAYLOAD_MTU
20
21
  from .base import BaseEncoderBin
21
22
 
22
23
  # opusenc frame-size enum values (ms); must be >= SDP minptime
@@ -131,6 +132,7 @@ class OpusEncoderBin(BaseEncoderBin):
131
132
  try_set_property(self._enc, "dtx", dtx)
132
133
 
133
134
  self._pay.set_property("pt", self._pt)
135
+ try_set_property(self._pay, "mtu", RTP_PAYLOAD_MTU)
134
136
  if ssrc is not None:
135
137
  try_set_property(self._pay, "ssrc", ssrc)
136
138
 
@@ -4,6 +4,7 @@ from reactor_runtime.transports.gstreamer.gst_helpers import (
4
4
  make_element,
5
5
  try_set_property,
6
6
  )
7
+ from reactor_runtime.transports.gstreamer.settings import RTP_PAYLOAD_MTU
7
8
  from .base import BaseEncoderBin
8
9
 
9
10
 
@@ -86,6 +87,7 @@ class VP8EncoderBin(BaseEncoderBin):
86
87
 
87
88
  # RTP payload type must match SDP negotiation
88
89
  self._pay.set_property("pt", self._pt)
90
+ try_set_property(self._pay, "mtu", RTP_PAYLOAD_MTU)
89
91
  if ssrc is not None:
90
92
  try_set_property(self._pay, "ssrc", ssrc)
91
93