reactor-runtime 3.8.0__tar.gz → 3.8.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.
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/PKG-INFO +4 -4
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/README.md +2 -2
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/pyproject.toml +3 -3
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/pyproject.toml.orig +3 -3
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/__init__.py +9 -1
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/model.py +7 -1
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/stats.py +4 -1
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/transport.py +23 -3
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/values.py +26 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/http/routes.py +4 -0
- reactor_runtime-3.8.2/src/reactor_runtime/http/step_usage.py +30 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/metrics/session.py +1 -1
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/client_stats.py +21 -2
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/connection_manager.py +32 -2
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/runner.py +175 -42
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/runtime_stats.py +2 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/session_start.py +12 -1
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/step_results/__init__.py +3 -2
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/step_results/mp4.py +26 -5
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/step_results/store.py +140 -17
- reactor_runtime-3.8.2/src/reactor_runtime/step_results/usage.py +46 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/client_stats.py +15 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/connection.py +10 -2
- reactor_runtime-3.8.2/src/reactor_runtime/transport/webrtc/frame_stages.py +151 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/pacer.py +14 -3
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/peer.py +54 -7
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/stats.py +39 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/platform_pb2.py +20 -18
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/platform_pb2.pyi +14 -2
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/LICENSE +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/NOTICE +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/fields.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/service.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/session.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/errors.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/ipc.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/protocol.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/rank.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/runner.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/distributed/worker.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/http/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/http/server.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/app/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/app/input_state.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/app/outcome.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/app/reactor_app.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/app/step_completed.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/client.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/events/decorators.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/internal/aliases.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/internal/bridge.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/model/contract.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/log.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/manifest.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/message_gateway.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/metrics/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/metrics/command.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/metrics/model.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/metrics/registry.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/recording/recorder.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/state_machine.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/system_client.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/upload_resolution.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/serve.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/service.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/acceptor.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/config.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/reading.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/control_pb2.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/control_pb2.pyi +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_wire/v1/track_pb2.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor-runtime
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.2
|
|
4
4
|
Summary: A Python framework for building real-time, interactive video models
|
|
5
5
|
Author: Reactor
|
|
6
6
|
Author-email: Reactor <team@reactor.inc>
|
|
@@ -19,7 +19,7 @@ Requires-Dist: numpy>=2.1
|
|
|
19
19
|
Requires-Dist: prometheus-client>=0.26.0
|
|
20
20
|
Requires-Dist: protobuf>=7.35.1
|
|
21
21
|
Requires-Dist: pyyaml>=6.0.3
|
|
22
|
-
Requires-Dist: reactor-webrtc==0.
|
|
22
|
+
Requires-Dist: reactor-webrtc==0.22.1
|
|
23
23
|
Requires-Dist: uvicorn>=0.49.0
|
|
24
24
|
Requires-Python: >=3.12
|
|
25
25
|
Project-URL: Source, https://github.com/reactor-team/reactor-runtime
|
|
@@ -124,7 +124,7 @@ class MyModel(ReactorApp):
|
|
|
124
124
|
|
|
125
125
|
Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. The default `run()` is the loop that drives the three hooks, and it reports each step that runs with `complete_step()`, which is what the runtime counts a session's steps by. To write that report yourself, for example to keep extra files with the step, return a `StepCompleted(output=..., files=...)` from `process_output()` instead of the `Output`. Override `run()` to write your own loop against `emit()`, `send()`, `complete_step()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called, and your loop calls `complete_step(StepCompleted(output=...))` each time it finishes a unit of work. Do that for a loop that is not one step per emit, such as a renderer that emits several times per step or a model that must block on an input.
|
|
126
126
|
|
|
127
|
-
A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete."
|
|
127
|
+
A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. `steps` is a whole number from 1 to 10,000. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete." A session with `steps` has no client to correct its starting input, so a starting command the runtime cannot apply (an upload whose bytes never arrive, or arguments the contract refuses) ends it at once with the reason `start_refused` and the refusal as its close reason. If the handler of a starting command fails instead, by raising `CommandError` or any other exception, every step the model has not reported yet is recorded with the handler's message, so a session at step 3 of 4 saves steps 3 and 4 with that error, and the session ends with the reason `error`. The model keeps running and takes the next session.
|
|
128
128
|
|
|
129
129
|
A model can keep each step it reports as a folder of files, turned on in `reactor.yaml`:
|
|
130
130
|
|
|
@@ -137,7 +137,7 @@ runtime:
|
|
|
137
137
|
queue: 8
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them
|
|
140
|
+
Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them, with the measured length, frame count, frame rate, and size of each video track in its `usage`. `queue` bounds how many steps the model reports may wait to be saved; a step that finds it full is not saved, so saving never holds the model up. The steps a failed starting command records are saved past that bound, so each of them gets its folder. Each step is its own encode, so this suits a model that finishes a step every few seconds rather than every frame.
|
|
141
141
|
|
|
142
142
|
When step results are on, `GET /sessions/{id}/steps` lists a session's saved folders by the session's own id; each stays readable for five minutes after it is written, after the session ends too.
|
|
143
143
|
|
|
@@ -97,7 +97,7 @@ class MyModel(ReactorApp):
|
|
|
97
97
|
|
|
98
98
|
Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. The default `run()` is the loop that drives the three hooks, and it reports each step that runs with `complete_step()`, which is what the runtime counts a session's steps by. To write that report yourself, for example to keep extra files with the step, return a `StepCompleted(output=..., files=...)` from `process_output()` instead of the `Output`. Override `run()` to write your own loop against `emit()`, `send()`, `complete_step()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called, and your loop calls `complete_step(StepCompleted(output=...))` each time it finishes a unit of work. Do that for a loop that is not one step per emit, such as a renderer that emits several times per step or a model that must block on an input.
|
|
99
99
|
|
|
100
|
-
A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete."
|
|
100
|
+
A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. `steps` is a whole number from 1 to 10,000. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete." A session with `steps` has no client to correct its starting input, so a starting command the runtime cannot apply (an upload whose bytes never arrive, or arguments the contract refuses) ends it at once with the reason `start_refused` and the refusal as its close reason. If the handler of a starting command fails instead, by raising `CommandError` or any other exception, every step the model has not reported yet is recorded with the handler's message, so a session at step 3 of 4 saves steps 3 and 4 with that error, and the session ends with the reason `error`. The model keeps running and takes the next session.
|
|
101
101
|
|
|
102
102
|
A model can keep each step it reports as a folder of files, turned on in `reactor.yaml`:
|
|
103
103
|
|
|
@@ -110,7 +110,7 @@ runtime:
|
|
|
110
110
|
queue: 8
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them
|
|
113
|
+
Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them, with the measured length, frame count, frame rate, and size of each video track in its `usage`. `queue` bounds how many steps the model reports may wait to be saved; a step that finds it full is not saved, so saving never holds the model up. The steps a failed starting command records are saved past that bound, so each of them gets its folder. Each step is its own encode, so this suits a model that finishes a step every few seconds rather than every frame.
|
|
114
114
|
|
|
115
115
|
When step results are on, `GET /sessions/{id}/steps` lists a session's saved folders by the session's own id; each stays readable for five minutes after it is written, after the session ends too.
|
|
116
116
|
|
|
@@ -10,7 +10,7 @@ module-name = [
|
|
|
10
10
|
namespace = true
|
|
11
11
|
|
|
12
12
|
[tool.reactor-wire]
|
|
13
|
-
version = "1.
|
|
13
|
+
version = "1.20261013.10"
|
|
14
14
|
|
|
15
15
|
[tool.ruff]
|
|
16
16
|
line-length = 100
|
|
@@ -95,7 +95,7 @@ pythonpath = ["."]
|
|
|
95
95
|
|
|
96
96
|
[project]
|
|
97
97
|
name = "reactor-runtime"
|
|
98
|
-
version = "3.8.
|
|
98
|
+
version = "3.8.2"
|
|
99
99
|
description = "A Python framework for building real-time, interactive video models"
|
|
100
100
|
readme = "README.md"
|
|
101
101
|
license = "Apache-2.0"
|
|
@@ -119,7 +119,7 @@ dependencies = [
|
|
|
119
119
|
"prometheus-client>=0.26.0",
|
|
120
120
|
"protobuf>=7.35.1",
|
|
121
121
|
"pyyaml>=6.0.3",
|
|
122
|
-
"reactor-webrtc==0.
|
|
122
|
+
"reactor-webrtc==0.22.1",
|
|
123
123
|
"uvicorn>=0.49.0",
|
|
124
124
|
]
|
|
125
125
|
|
|
@@ -16,11 +16,11 @@ namespace = true
|
|
|
16
16
|
# gates the pin against proto/, so a release cannot ship a schema older than the
|
|
17
17
|
# sources it was built from.
|
|
18
18
|
[tool.reactor-wire]
|
|
19
|
-
version = "1.
|
|
19
|
+
version = "1.20261013.10"
|
|
20
20
|
|
|
21
21
|
[project]
|
|
22
22
|
name = "reactor-runtime"
|
|
23
|
-
version = "3.8.
|
|
23
|
+
version = "3.8.2"
|
|
24
24
|
description = "A Python framework for building real-time, interactive video models"
|
|
25
25
|
readme = "README.md"
|
|
26
26
|
license = "Apache-2.0"
|
|
@@ -42,7 +42,7 @@ dependencies = [
|
|
|
42
42
|
"prometheus-client>=0.26.0",
|
|
43
43
|
"protobuf>=7.35.1",
|
|
44
44
|
"pyyaml>=6.0.3",
|
|
45
|
-
"reactor-webrtc==0.
|
|
45
|
+
"reactor-webrtc==0.22.1",
|
|
46
46
|
"uvicorn>=0.49.0",
|
|
47
47
|
]
|
|
48
48
|
|
|
@@ -34,7 +34,12 @@ from reactor_runtime.core.session import (
|
|
|
34
34
|
Transition,
|
|
35
35
|
)
|
|
36
36
|
from reactor_runtime.core.stats import TransportReading, TransportTrackReading
|
|
37
|
-
from reactor_runtime.core.transport import
|
|
37
|
+
from reactor_runtime.core.transport import (
|
|
38
|
+
Connection,
|
|
39
|
+
ConnectionSink,
|
|
40
|
+
FrameStageSource,
|
|
41
|
+
TransportStatsSource,
|
|
42
|
+
)
|
|
38
43
|
from reactor_runtime.core.typespec import TypeSpec
|
|
39
44
|
from reactor_runtime.core.values import (
|
|
40
45
|
ClientConnectionStat,
|
|
@@ -45,6 +50,7 @@ from reactor_runtime.core.values import (
|
|
|
45
50
|
CompletedStep,
|
|
46
51
|
ConnectionCapabilities,
|
|
47
52
|
ConnId,
|
|
53
|
+
FrameStage,
|
|
48
54
|
Health,
|
|
49
55
|
HealthStatus,
|
|
50
56
|
InputFrame,
|
|
@@ -76,6 +82,8 @@ __all__ = [
|
|
|
76
82
|
"EndReason",
|
|
77
83
|
"FieldInfo",
|
|
78
84
|
"FileUploaded",
|
|
85
|
+
"FrameStage",
|
|
86
|
+
"FrameStageSource",
|
|
79
87
|
"Health",
|
|
80
88
|
"HealthStatus",
|
|
81
89
|
"InputField",
|
|
@@ -189,13 +189,19 @@ def _build_command(cls: type[Command]) -> None:
|
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
class EndReason(StrEnum):
|
|
192
|
-
"""Why a session ended.
|
|
192
|
+
"""Why a session ended.
|
|
193
|
+
|
|
194
|
+
``START_REFUSED`` ends a session with a step count whose starting input
|
|
195
|
+
cannot be applied: a starting command names an upload whose bytes never
|
|
196
|
+
arrive, or the contract refuses its arguments.
|
|
197
|
+
"""
|
|
193
198
|
|
|
194
199
|
STOPPED = "stopped"
|
|
195
200
|
TIMED_OUT = "timed_out"
|
|
196
201
|
EVICTED = "evicted"
|
|
197
202
|
MODERATED = "moderated"
|
|
198
203
|
ERROR = "error"
|
|
204
|
+
START_REFUSED = "start_refused"
|
|
199
205
|
|
|
200
206
|
|
|
201
207
|
@dataclass(frozen=True)
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
from collections.abc import Mapping
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
15
|
|
|
16
|
-
from reactor_runtime.core.values import ClientTrackDirection, TrackKind
|
|
16
|
+
from reactor_runtime.core.values import ClientTrackDirection, FrameStage, TrackKind
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@dataclass(frozen=True)
|
|
@@ -32,6 +32,8 @@ class TransportTrackReading:
|
|
|
32
32
|
``"packets_lost"``, ``"frames_per_second"``), and a measurement the
|
|
33
33
|
transport has no value for is left out rather than reported as
|
|
34
34
|
zero.
|
|
35
|
+
frame_stages: Where the track's frames spent their time on this side
|
|
36
|
+
of the wire since the previous reading, stage by stage.
|
|
35
37
|
"""
|
|
36
38
|
|
|
37
39
|
track_name: str
|
|
@@ -39,6 +41,7 @@ class TransportTrackReading:
|
|
|
39
41
|
direction: ClientTrackDirection
|
|
40
42
|
codec: str = ""
|
|
41
43
|
metrics: Mapping[str, float] = field(default_factory=dict)
|
|
44
|
+
frame_stages: tuple[FrameStage, ...] = ()
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
@dataclass(frozen=True)
|
|
@@ -4,13 +4,14 @@ The two structural protocols that form the seam between a transport and the
|
|
|
4
4
|
runner, pointing opposite ways. ``Connection`` carries commands down to one
|
|
5
5
|
client; ``ConnectionSink`` carries facts up from the transport. Both are
|
|
6
6
|
``Protocol``s, so a transport author or a test fake conforms by shape and
|
|
7
|
-
signaling never reaches the runner. ``TransportStatsSource``
|
|
8
|
-
|
|
7
|
+
signaling never reaches the runner. ``TransportStatsSource`` and
|
|
8
|
+
``FrameStageSource`` are optional capabilities a connection adds when its
|
|
9
|
+
transport measures its wire.
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from __future__ import annotations
|
|
12
13
|
|
|
13
|
-
from collections.abc import Mapping
|
|
14
|
+
from collections.abc import Collection, Mapping
|
|
14
15
|
from typing import Protocol, runtime_checkable
|
|
15
16
|
|
|
16
17
|
from reactor_runtime.core.stats import TransportReading
|
|
@@ -18,6 +19,7 @@ from reactor_runtime.core.values import (
|
|
|
18
19
|
ClientStatsBatch,
|
|
19
20
|
ConnectionCapabilities,
|
|
20
21
|
ConnId,
|
|
22
|
+
FrameStage,
|
|
21
23
|
InputFrame,
|
|
22
24
|
MediaChunk,
|
|
23
25
|
)
|
|
@@ -118,6 +120,24 @@ class TransportStatsSource(Protocol):
|
|
|
118
120
|
"""
|
|
119
121
|
|
|
120
122
|
|
|
123
|
+
@runtime_checkable
|
|
124
|
+
class FrameStageSource(Protocol):
|
|
125
|
+
"""A connection whose transport times each frame's stages on its side of the wire.
|
|
126
|
+
|
|
127
|
+
Optional, beside :class:`TransportStatsSource`, and checked for the same
|
|
128
|
+
way. A track's times add up until they are taken, so each take covers the
|
|
129
|
+
window since that track's previous one.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def take_frame_stages(self, tracks: Collection[str]) -> Mapping[str, tuple[FrameStage, ...]]:
|
|
133
|
+
"""Return the stage times of *tracks* since their previous take, and start them again.
|
|
134
|
+
|
|
135
|
+
Keyed by track name. A track with no frames in its window is left out,
|
|
136
|
+
and so is a stage with no frames in it. Tracks not named keep their
|
|
137
|
+
times for a later take.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
|
|
121
141
|
@runtime_checkable
|
|
122
142
|
class ConnectionSink(Protocol):
|
|
123
143
|
"""The transport-to-runner upward channel — the facts-up half.
|
|
@@ -44,6 +44,29 @@ class CommandFailure:
|
|
|
44
44
|
message: str
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class FrameStage:
|
|
49
|
+
"""The time a track's frames spent in one stage of their trip, over one window.
|
|
50
|
+
|
|
51
|
+
The stages cover the transport between the client's app and the model:
|
|
52
|
+
``submit``, ``encode_wait``, ``encode``, ``packetize``, ``pacer``,
|
|
53
|
+
``jitter_buffer``, ``decode``, ``delivery``, ``output_pacing`` and
|
|
54
|
+
``output_queue``, in the order a frame goes through them. Each side
|
|
55
|
+
reports the stages it can see. The set grows, so a reader ignores a name
|
|
56
|
+
it does not know.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
name: The stage.
|
|
60
|
+
total_ms: The time all *frames* frames spent in the stage together,
|
|
61
|
+
so the average per frame is ``total_ms / frames``.
|
|
62
|
+
frames: How many frames the window counted in the stage.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
total_ms: float
|
|
67
|
+
frames: int
|
|
68
|
+
|
|
69
|
+
|
|
47
70
|
@dataclass(frozen=True)
|
|
48
71
|
class ClientTrackStat:
|
|
49
72
|
"""One WebRTC quality reading a client took for one of its tracks.
|
|
@@ -80,6 +103,8 @@ class ClientTrackStat:
|
|
|
80
103
|
readings with different totals cannot be re-aggregated correctly
|
|
81
104
|
afterward; a raw count can always be summed and divided once, at
|
|
82
105
|
query time.
|
|
106
|
+
frame_stages: Where the track's frames spent their time since the
|
|
107
|
+
client's previous batch, stage by stage.
|
|
83
108
|
"""
|
|
84
109
|
|
|
85
110
|
timestamp: int
|
|
@@ -89,6 +114,7 @@ class ClientTrackStat:
|
|
|
89
114
|
codec: str
|
|
90
115
|
paused: bool
|
|
91
116
|
metrics: Mapping[str, float]
|
|
117
|
+
frame_stages: tuple[FrameStage, ...] = ()
|
|
92
118
|
|
|
93
119
|
|
|
94
120
|
@dataclass(frozen=True)
|
|
@@ -18,6 +18,7 @@ from pydantic import BaseModel, Field
|
|
|
18
18
|
|
|
19
19
|
from reactor_runtime.core import Health, HealthStatus, RuntimeState, SessionState
|
|
20
20
|
from reactor_runtime.http.events import format_sse
|
|
21
|
+
from reactor_runtime.http.step_usage import StepUsage
|
|
21
22
|
from reactor_runtime.metrics import CONTENT_TYPE as METRICS_CONTENT_TYPE
|
|
22
23
|
from reactor_runtime.metrics import RuntimeMetrics
|
|
23
24
|
from reactor_runtime.recording import ClipManifest, ClipSessionGoneError, Pending
|
|
@@ -380,6 +381,9 @@ class StepResult(BaseModel):
|
|
|
380
381
|
step: int
|
|
381
382
|
session_id: str
|
|
382
383
|
files: list[StepFile] = Field(description="The files beside result.json.")
|
|
384
|
+
usage: list[StepUsage] = Field(
|
|
385
|
+
description="One entry per video track the step wrote; empty when it wrote none."
|
|
386
|
+
)
|
|
383
387
|
messages: list[StepMessage] = Field(
|
|
384
388
|
description="The messages the model broadcast since the step before."
|
|
385
389
|
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The HTTP shape of what a saved step delivered: the ``usage`` entries of ``result.json``.
|
|
2
|
+
|
|
3
|
+
Each kind of output a step can deliver has its own model here, tagged by a
|
|
4
|
+
``kind`` literal, and :data:`StepUsage` is the type of one entry. A new kind
|
|
5
|
+
adds its model to this module and joins :data:`StepUsage`, which then becomes
|
|
6
|
+
a union discriminated on ``kind``, so the route models never change.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StepVideoUsage(BaseModel):
|
|
17
|
+
"""The measured facts of one video track the step wrote, as the file holds it."""
|
|
18
|
+
|
|
19
|
+
kind: Literal["video"]
|
|
20
|
+
track: str = Field(description="The output track the stream holds.")
|
|
21
|
+
file: str = Field(description="The file the stream is in.")
|
|
22
|
+
duration_s: float = Field(description="How long the stream plays, in seconds.")
|
|
23
|
+
frames: int = Field(description="How many frames the stream holds.")
|
|
24
|
+
fps: float = Field(description="The rate the stream plays at, in frames per second.")
|
|
25
|
+
width: int = Field(description="The stream's width in pixels.")
|
|
26
|
+
height: int = Field(description="The stream's height in pixels.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
StepUsage = StepVideoUsage
|
|
30
|
+
"""One ``usage`` entry of a step's ``result.json``."""
|
|
@@ -29,7 +29,7 @@ def _reason_label(detail: Mapping[str, Any]) -> str:
|
|
|
29
29
|
|
|
30
30
|
The runtime authors every reason, and a move that names none is a plain stop —
|
|
31
31
|
the same reading the runner's own dispatch takes. The type guard holds the
|
|
32
|
-
label to the
|
|
32
|
+
label to the :class:`EndReason` values whatever a caller puts in the
|
|
33
33
|
detail, because one unbounded label value costs a series forever.
|
|
34
34
|
"""
|
|
35
35
|
reason = detail.get("reason", EndReason.STOPPED)
|
|
@@ -12,16 +12,17 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import math
|
|
14
14
|
import time
|
|
15
|
-
from collections.abc import Callable, Mapping
|
|
15
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
-
from reactor_runtime.core import ClientStatsBatch, ConnId
|
|
18
|
+
from reactor_runtime.core import ClientStatsBatch, ConnId, FrameStage
|
|
19
19
|
|
|
20
20
|
# The SDK sends a batch every few seconds, with a few tracks and a few dozen
|
|
21
21
|
# metrics each; these limits are far above that.
|
|
22
22
|
MIN_INTERVAL_SECONDS = 1.0
|
|
23
23
|
MAX_TRACKS = 32
|
|
24
24
|
MAX_METRICS = 64
|
|
25
|
+
MAX_FRAME_STAGES = 32
|
|
25
26
|
MAX_NAME_LENGTH = 128
|
|
26
27
|
|
|
27
28
|
|
|
@@ -70,7 +71,10 @@ def fits(batch: ClientStatsBatch) -> bool:
|
|
|
70
71
|
readings.append(batch.connection_stat.metrics)
|
|
71
72
|
if any(len(metrics) > MAX_METRICS for metrics in readings):
|
|
72
73
|
return False
|
|
74
|
+
if any(len(stat.frame_stages) > MAX_FRAME_STAGES for stat in batch.track_stats):
|
|
75
|
+
return False
|
|
73
76
|
names = [name for metrics in readings for name in metrics]
|
|
77
|
+
names += [stage.name for stat in batch.track_stats for stage in stat.frame_stages]
|
|
74
78
|
names += [field for stat in batch.track_stats for field in (stat.track_name, stat.codec)]
|
|
75
79
|
return all(len(name) <= MAX_NAME_LENGTH for name in names)
|
|
76
80
|
|
|
@@ -91,6 +95,7 @@ def to_detail(batch: ClientStatsBatch) -> dict[str, Any]:
|
|
|
91
95
|
"codec": stat.codec,
|
|
92
96
|
"paused": stat.paused,
|
|
93
97
|
"metrics": _finite(stat.metrics),
|
|
98
|
+
"frame_stages": frame_stages_detail(stat.frame_stages),
|
|
94
99
|
}
|
|
95
100
|
for stat in batch.track_stats
|
|
96
101
|
],
|
|
@@ -105,6 +110,20 @@ def to_detail(batch: ClientStatsBatch) -> dict[str, Any]:
|
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
|
|
113
|
+
def frame_stages_detail(stages: Iterable[FrameStage]) -> list[dict[str, Any]]:
|
|
114
|
+
"""Shape a track's stage times for a fact's detail.
|
|
115
|
+
|
|
116
|
+
A stage with no name or no frames has no average, and a negative time or
|
|
117
|
+
one JSON cannot carry is no measurement, so all of them are left out. The
|
|
118
|
+
runtime's own stages follow the same rule when they are timed.
|
|
119
|
+
"""
|
|
120
|
+
return [
|
|
121
|
+
{"name": stage.name, "total_ms": stage.total_ms, "frames": stage.frames}
|
|
122
|
+
for stage in stages
|
|
123
|
+
if stage.name and stage.frames > 0 and math.isfinite(stage.total_ms) and stage.total_ms >= 0
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
108
127
|
def _finite(metrics: Mapping[str, float]) -> dict[str, float]:
|
|
109
128
|
"""Keep the metric values JSON can carry: finite numbers, not NaN or infinity."""
|
|
110
129
|
return {key: value for key, value in metrics.items() if math.isfinite(value)}
|
{reactor_runtime-3.8.0 → reactor_runtime-3.8.2}/src/reactor_runtime/runner/connection_manager.py
RENAMED
|
@@ -41,11 +41,13 @@ replaced rather than edited.
|
|
|
41
41
|
from __future__ import annotations
|
|
42
42
|
|
|
43
43
|
import random
|
|
44
|
-
from collections.abc import Callable, Mapping
|
|
44
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
45
|
+
from dataclasses import replace
|
|
45
46
|
|
|
46
47
|
from reactor_runtime.core import (
|
|
47
48
|
Connection,
|
|
48
49
|
ConnId,
|
|
50
|
+
FrameStageSource,
|
|
49
51
|
MediaChunk,
|
|
50
52
|
SessionEvent,
|
|
51
53
|
TransportReading,
|
|
@@ -146,7 +148,7 @@ class ConnectionManager:
|
|
|
146
148
|
|
|
147
149
|
A connection whose transport has not taken a reading yet, or does not
|
|
148
150
|
measure its wire (it is not a :class:`TransportStatsSource`), is left
|
|
149
|
-
out.
|
|
151
|
+
out. Reading them changes nothing, so any number of callers can.
|
|
150
152
|
"""
|
|
151
153
|
return [
|
|
152
154
|
(cid, reading)
|
|
@@ -155,6 +157,34 @@ class ConnectionManager:
|
|
|
155
157
|
and (reading := conn.latest_reading) is not None
|
|
156
158
|
]
|
|
157
159
|
|
|
160
|
+
def take_frame_stages(
|
|
161
|
+
self, readings: Iterable[tuple[ConnId, TransportReading]]
|
|
162
|
+
) -> list[tuple[ConnId, TransportReading]]:
|
|
163
|
+
"""Add each connection's stage times since the previous take to its *readings*.
|
|
164
|
+
|
|
165
|
+
For a connection that times its frames (a :class:`FrameStageSource`),
|
|
166
|
+
each track in its reading gets the stage times the connection held for
|
|
167
|
+
it, and those are taken: the next call gets only what was timed after
|
|
168
|
+
this one, so the runner calls this once per report. Stages of a track
|
|
169
|
+
the reading does not carry yet stay with the connection until a
|
|
170
|
+
reading does. A connection with no reading here keeps all its stages.
|
|
171
|
+
"""
|
|
172
|
+
out: list[tuple[ConnId, TransportReading]] = []
|
|
173
|
+
for cid, reading in readings:
|
|
174
|
+
conn = self._by_id.get(cid)
|
|
175
|
+
if isinstance(conn, FrameStageSource) and reading.tracks:
|
|
176
|
+
stages = conn.take_frame_stages([t.track_name for t in reading.tracks])
|
|
177
|
+
if stages:
|
|
178
|
+
reading = replace(
|
|
179
|
+
reading,
|
|
180
|
+
tracks=tuple(
|
|
181
|
+
replace(track, frame_stages=stages.get(track.track_name, ()))
|
|
182
|
+
for track in reading.tracks
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
out.append((cid, reading))
|
|
186
|
+
return out
|
|
187
|
+
|
|
158
188
|
def new_conn_id(self) -> ConnId:
|
|
159
189
|
"""Mint a fresh random connection id, unique within the session.
|
|
160
190
|
|