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.
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/PKG-INFO +1 -1
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/pyproject.toml +1 -1
- reactor_runtime-2.9.4/src/reactor_runtime/realtime/pipeline.py +426 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/client.py +38 -5
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +2 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +19 -7
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +18 -19
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +2 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +2 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +2 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/video.py +25 -1
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/settings.py +27 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/PKG-INFO +1 -1
- reactor_runtime-2.9.2/src/reactor_runtime/realtime/pipeline.py +0 -304
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/README.md +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/setup.cfg +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/api/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/config.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/experiment/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/experiment/session.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/client.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/defaults.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/driver/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/driver/step_result.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/connected.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/event.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/events/upload.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/decorators.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/handlers.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/interface/upload.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/model_state.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/base.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/file.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/helpers.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/nvml_sampler.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/profiler.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/singleton.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/profiling/torch_chunk_profiler.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/realtime/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/config.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/session_recorder.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/sinks.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/recording/track_resolver.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtime_api.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtimes/http/config.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtimes/http/http_runtime.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/runtimes/http/types.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/schema_validator.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/__main__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/commands/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/commands/run.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/commands/schema.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/main.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/utils/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/utils/config.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/serve/utils/runtime.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/config.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/events.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/quality.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/ice_uris.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/interface.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/media.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/types.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/launch.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/loader.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/log.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/messages.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/paths.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/ports.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/utils/typing.py +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/SOURCES.txt +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/requires.txt +0 -0
- {reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime.egg-info/top_level.txt +0 -0
|
@@ -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
|
{reactor_runtime-2.9.2 → reactor_runtime-2.9.4}/src/reactor_runtime/transports/gstreamer/client.py
RENAMED
|
@@ -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
|
|
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
|
-
#
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
|
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
|
|
173
|
+
# Enforce negotiated profile only — NOT level.
|
|
170
174
|
# ---------------------------------------------------------
|
|
171
|
-
profile
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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``
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|