toposync-ext-streaming 0.7.10__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/.gitignore +1 -0
  2. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/PKG-INFO +1 -1
  3. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/pyproject.toml +1 -1
  4. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/api/routes.py +101 -5
  5. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/pipelines/operators.py +35 -7
  6. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/wizard/pipeline_builder.py +10 -0
  7. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/LICENSE +0 -0
  8. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/LICENSE.ffmpeg +0 -0
  9. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/LICENSE.mediamtx +0 -0
  10. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/README.md +0 -0
  11. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/__init__.py +0 -0
  12. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/api/__init__.py +0 -0
  13. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/api/models.py +0 -0
  14. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
  15. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
  16. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/extension.json +0 -0
  17. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
  18. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/plugin.py +0 -0
  19. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/326.js +0 -0
  20. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
  21. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/387.js +0 -0
  22. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/4.js +0 -0
  23. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
  24. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/623.js +0 -0
  25. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
  26. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/703.js +0 -0
  27. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
  28. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/main.js +0 -0
  29. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
  30. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/static/remoteEntry.js +0 -0
  31. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
  32. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
  33. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
  34. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
  35. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
  36. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/engine_manager.py +0 -0
  37. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
  38. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
  39. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
  40. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
  41. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
  42. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
  43. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
  44. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
  45. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
  46. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
  47. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
  48. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
  49. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/platform.py +0 -0
  50. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
  51. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
  52. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/resize.py +0 -0
  53. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
  54. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
  55. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
  56. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/package.json +0 -0
  57. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/activate.tsx +0 -0
  58. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/api/streamingApi.ts +0 -0
  59. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/constants.ts +0 -0
  60. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/entry.ts +0 -0
  61. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/settings/StreamingSettingsPanel.tsx +0 -0
  62. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/settings/SubModal.tsx +0 -0
  63. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
  64. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/translations.ts +0 -0
  65. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/src/types.ts +0 -0
  66. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/tsconfig.json +0 -0
  67. {toposync_ext_streaming-0.7.10 → toposync_ext_streaming-0.8.0}/ui/webpack.config.js +0 -0
@@ -38,3 +38,4 @@ extensions/streaming/src/toposync_ext_streaming/bin/ffmpeg/**/ffmpeg*
38
38
  extensions/vision/models/
39
39
 
40
40
  *.temporary.*
41
+ .playwright-mcp/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toposync-ext-streaming
3
- Version: 0.7.10
3
+ Version: 0.8.0
4
4
  Summary: Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toposync-ext-streaming"
3
- version = "0.7.10"
3
+ version = "0.8.0"
4
4
  description = "Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -5903,6 +5903,7 @@ async def _sync_pipeline_output_publication_nodes(
5903
5903
  graph = pipeline.graph if isinstance(pipeline.graph, dict) else {}
5904
5904
  nodes = graph.get("nodes") if isinstance(graph.get("nodes"), list) else []
5905
5905
  changed = False
5906
+ publication_targets: dict[str, tuple[str, str, str]] = {}
5906
5907
  for node in nodes:
5907
5908
  if not isinstance(node, dict):
5908
5909
  continue
@@ -5914,11 +5915,25 @@ async def _sync_pipeline_output_publication_nodes(
5914
5915
  continue
5915
5916
  cfg = node.get("config") if isinstance(node.get("config"), dict) else {}
5916
5917
  expected_transmission_id = _publication_transmission_id(publication)
5917
- if str(cfg.get("transmission_id") or "").strip() == expected_transmission_id:
5918
- continue
5919
- next_cfg = {**cfg, "transmission_id": expected_transmission_id}
5920
- node["config"] = next_cfg
5921
- changed = True
5918
+ expected_output_id = f"hls_{_publication_quality_profile(publication)}"
5919
+ expected_quality_profile_id = _publication_quality_profile(publication)
5920
+ publication_targets[node_id] = (
5921
+ expected_transmission_id,
5922
+ expected_output_id,
5923
+ expected_quality_profile_id,
5924
+ )
5925
+ if str(cfg.get("transmission_id") or "").strip() != expected_transmission_id:
5926
+ next_cfg = {**cfg, "transmission_id": expected_transmission_id}
5927
+ node["config"] = next_cfg
5928
+ changed = True
5929
+ if publication_targets:
5930
+ updated_gate_ids = _sync_demand_gates_for_publication_targets(
5931
+ graph=graph,
5932
+ nodes=nodes,
5933
+ publication_targets=publication_targets,
5934
+ )
5935
+ if updated_gate_ids:
5936
+ changed = True
5922
5937
  if not changed:
5923
5938
  continue
5924
5939
  updated = pipeline.model_copy(update={"graph": graph})
@@ -5937,6 +5952,87 @@ async def _sync_pipeline_output_publication_nodes(
5937
5952
  return changed_pipelines
5938
5953
 
5939
5954
 
5955
+ def _sync_demand_gates_for_publication_targets(
5956
+ *,
5957
+ graph: dict[str, Any],
5958
+ nodes: list[Any],
5959
+ publication_targets: dict[str, tuple[str, str, str]],
5960
+ ) -> list[str]:
5961
+ if not publication_targets:
5962
+ return []
5963
+ edges = graph.get("edges") if isinstance(graph.get("edges"), list) else []
5964
+ downstream_by_node: dict[str, set[str]] = {}
5965
+ for edge in edges:
5966
+ if not isinstance(edge, dict):
5967
+ continue
5968
+ source = edge.get("from")
5969
+ target = edge.get("to")
5970
+ if not isinstance(source, dict) or not isinstance(target, dict):
5971
+ continue
5972
+ source_node = str(source.get("node") or "").strip()
5973
+ target_node = str(target.get("node") or "").strip()
5974
+ if not source_node or not target_node:
5975
+ continue
5976
+ downstream_by_node.setdefault(source_node, set()).add(target_node)
5977
+
5978
+ changed_gate_ids: list[str] = []
5979
+ for node in nodes:
5980
+ if not isinstance(node, dict):
5981
+ continue
5982
+ if str(node.get("operator") or "").strip() != "stream.demand_gate":
5983
+ continue
5984
+ gate_id = str(node.get("id") or "").strip()
5985
+ if not gate_id:
5986
+ continue
5987
+ reachable_publications = [
5988
+ publish_node_id
5989
+ for publish_node_id in publication_targets
5990
+ if _graph_node_reaches(gate_id, publish_node_id, downstream_by_node)
5991
+ ]
5992
+ if len(reachable_publications) != 1:
5993
+ continue
5994
+ transmission_id, output_id, quality_profile_id = publication_targets[reachable_publications[0]]
5995
+ cfg = node.get("config") if isinstance(node.get("config"), dict) else {}
5996
+ demand_scope = str(cfg.get("demand_scope") or "").strip().lower()
5997
+ use_specific_output = demand_scope == "output"
5998
+ next_cfg = {**cfg, "transmission_id": transmission_id}
5999
+ if use_specific_output:
6000
+ next_cfg["demand_scope"] = "output"
6001
+ next_cfg["output_id"] = str(cfg.get("output_id") or "").strip() or output_id
6002
+ next_cfg["quality_profile_id"] = str(cfg.get("quality_profile_id") or "").strip() or quality_profile_id
6003
+ else:
6004
+ next_cfg["demand_scope"] = "transmission"
6005
+ next_cfg["output_id"] = ""
6006
+ next_cfg["quality_profile_id"] = ""
6007
+ if next_cfg == cfg:
6008
+ continue
6009
+ node["config"] = next_cfg
6010
+ changed_gate_ids.append(gate_id)
6011
+ return changed_gate_ids
6012
+
6013
+
6014
+ def _graph_node_reaches(
6015
+ source_node_id: str,
6016
+ target_node_id: str,
6017
+ downstream_by_node: dict[str, set[str]],
6018
+ ) -> bool:
6019
+ if source_node_id == target_node_id:
6020
+ return True
6021
+ seen: set[str] = set()
6022
+ stack = [source_node_id]
6023
+ while stack:
6024
+ current = stack.pop()
6025
+ if current in seen:
6026
+ continue
6027
+ seen.add(current)
6028
+ for child in downstream_by_node.get(current, set()):
6029
+ if child == target_node_id:
6030
+ return True
6031
+ if child not in seen:
6032
+ stack.append(child)
6033
+ return False
6034
+
6035
+
5940
6036
  async def _delete_camera_live_pipelines(
5941
6037
  *,
5942
6038
  request: Request,
@@ -8,7 +8,7 @@ import numpy
8
8
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
9
9
 
10
10
  from toposync.runtime.pipelines.execution import PipelineRuntimeDependencies, SinkRuntime, SourceOperatorRuntime
11
- from toposync.runtime.pipelines.images import normalize_artifact_name
11
+ from toposync.runtime.pipelines.images import MAIN_ARTIFACT_NAME, normalize_artifact_name
12
12
  from toposync.runtime.pipelines.operator_registry import OperatorDiagnostic, OperatorRegistry
13
13
  from toposync.runtime.pipelines.packet_contract import resolve_media_ts
14
14
  from toposync.runtime.pipelines.runtime import Lifecycle, Packet
@@ -85,11 +85,30 @@ class DemandGateConfig(BaseModel):
85
85
  model_config = ConfigDict(extra="forbid")
86
86
 
87
87
  transmission_id: str = ""
88
+ demand_scope: Literal["transmission", "output"] = "transmission"
88
89
  output_id: str = ""
89
90
  quality_profile_id: str = ""
90
91
  poll_interval_ms: int = Field(default=500, ge=100, le=10_000)
91
92
  fail_open: bool = True
92
93
 
94
+ @model_validator(mode="before")
95
+ @classmethod
96
+ def _normalize_legacy_scope(cls, value: Any) -> Any:
97
+ if not isinstance(value, dict):
98
+ return value
99
+ data = dict(value)
100
+ scope = str(data.get("demand_scope") or "").strip().lower()
101
+ if scope == "auto":
102
+ scope = "transmission"
103
+ if scope not in {"transmission", "output"}:
104
+ has_specific_output = bool(
105
+ str(data.get("output_id") or "").strip()
106
+ or str(data.get("quality_profile_id") or "").strip()
107
+ )
108
+ scope = "output" if has_specific_output else "transmission"
109
+ data["demand_scope"] = scope
110
+ return data
111
+
93
112
  @field_validator("transmission_id", "output_id", "quality_profile_id", mode="after")
94
113
  @classmethod
95
114
  def _trim(cls, value: str) -> str:
@@ -147,10 +166,11 @@ class DemandGateRuntime(SourceOperatorRuntime):
147
166
  self._last_open = is_open
148
167
  self._last_payload = payload
149
168
  lifecycle = Lifecycle.OPEN if is_open else Lifecycle.CLOSE
169
+ output_id, quality_profile_id = self._selected_demand_target()
150
170
  stream_id = _demand_gate_stream_id(
151
171
  self._config.transmission_id,
152
- output_id=self._config.output_id,
153
- quality_profile_id=self._config.quality_profile_id,
172
+ output_id=output_id,
173
+ quality_profile_id=quality_profile_id,
154
174
  )
155
175
  return Packet.create(
156
176
  stream_id=stream_id,
@@ -158,8 +178,9 @@ class DemandGateRuntime(SourceOperatorRuntime):
158
178
  payload={
159
179
  "gate_open": bool(is_open),
160
180
  "transmission_id": self._config.transmission_id,
161
- "output_id": self._config.output_id,
162
- "quality_profile_id": self._config.quality_profile_id,
181
+ "demand_scope": self._config.demand_scope,
182
+ "output_id": output_id,
183
+ "quality_profile_id": quality_profile_id,
163
184
  **payload,
164
185
  },
165
186
  )
@@ -175,11 +196,12 @@ class DemandGateRuntime(SourceOperatorRuntime):
175
196
  "reason": "services_unavailable",
176
197
  }
177
198
  try:
199
+ output_id, quality_profile_id = self._selected_demand_target()
178
200
  raw = await services.call(
179
201
  "streaming.demand.snapshot",
180
202
  transmission_id=self._config.transmission_id,
181
- output_id=self._config.output_id,
182
- quality_profile_id=self._config.quality_profile_id,
203
+ output_id=output_id,
204
+ quality_profile_id=quality_profile_id,
183
205
  )
184
206
  except Exception as exc: # noqa: BLE001
185
207
  return bool(self._config.fail_open), {
@@ -199,6 +221,11 @@ class DemandGateRuntime(SourceOperatorRuntime):
199
221
  "matched_outputs": int(snapshot.get("matched_outputs") or 0),
200
222
  }
201
223
 
224
+ def _selected_demand_target(self) -> tuple[str, str]:
225
+ if self._config.demand_scope != "output":
226
+ return "", ""
227
+ return self._config.output_id, self._config.quality_profile_id
228
+
202
229
 
203
230
  def register_streaming_pipeline_operators(registry: OperatorRegistry) -> None:
204
231
  if registry.get("stream.demand_gate") is None:
@@ -227,6 +254,7 @@ def register_streaming_pipeline_operators(registry: OperatorRegistry) -> None:
227
254
  capabilities=["streaming", "sink", "realtime"],
228
255
  defaults=PublishVideoConfig(publication_enabled=True).model_dump(mode="json"),
229
256
  share_strategy="never",
257
+ requires_artifacts=[MAIN_ARTIFACT_NAME],
230
258
  requires_media_fields=["ts", "width", "height"],
231
259
  input_modalities=["video"],
232
260
  owner=EXTENSION_ID,
@@ -91,6 +91,15 @@ def build_streaming_wizard_graph(
91
91
  demand_gate_fail_open = _coerce_bool(options.get("demand_gate_fail_open"), default=True)
92
92
  demand_gate_output_id = _safe_text(options.get("demand_gate_output_id"))
93
93
  demand_gate_quality_profile_id = _safe_text(options.get("demand_gate_quality_profile_id"))
94
+ demand_gate_scope_default = "output" if demand_gate_output_id or demand_gate_quality_profile_id else "transmission"
95
+ demand_gate_scope = _pick_choice(
96
+ options.get("demand_gate_scope"),
97
+ allowed={"transmission", "output"},
98
+ default=demand_gate_scope_default,
99
+ )
100
+ if demand_gate_scope != "output":
101
+ demand_gate_output_id = ""
102
+ demand_gate_quality_profile_id = ""
94
103
  demand_gate_poll_interval_ms = _coerce_int(
95
104
  options.get("demand_gate_poll_interval_ms"),
96
105
  default=500,
@@ -144,6 +153,7 @@ def build_streaming_wizard_graph(
144
153
  "operator": "stream.demand_gate",
145
154
  "config": {
146
155
  "transmission_id": transmission_id,
156
+ "demand_scope": demand_gate_scope,
147
157
  "output_id": demand_gate_output_id,
148
158
  "quality_profile_id": demand_gate_quality_profile_id,
149
159
  "poll_interval_ms": int(demand_gate_poll_interval_ms),