janus-api-streaming 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any
6
+
7
+ from . import _compat
8
+ from .errors import StreamingNotStarted, StreamingProtocolError
9
+ from .models import (
10
+ CreatedMountpoint,
11
+ FileMountpointSpec,
12
+ MountpointInfo,
13
+ MountpointSummary,
14
+ RtpMountpointSpec,
15
+ RtspMountpointSpec,
16
+ )
17
+
18
+ HandleFactory = Callable[..., Awaitable[_compat.JanusStreamingHandle]]
19
+
20
+
21
+ class StreamingAdminClient:
22
+ def __init__(
23
+ self,
24
+ session: Any,
25
+ *,
26
+ admin_key: str | None = None,
27
+ request_timeout: float = 10.0,
28
+ attach_timeout: float = 5.0,
29
+ max_concurrency: int = 32,
30
+ handle_factory: HandleFactory | None = None,
31
+ ) -> None:
32
+ if request_timeout <= 0:
33
+ raise ValueError("request_timeout must be greater than zero")
34
+ if attach_timeout <= 0:
35
+ raise ValueError("attach_timeout must be greater than zero")
36
+ if max_concurrency < 1:
37
+ raise ValueError("max_concurrency must be at least one")
38
+ self._session = session
39
+ self._admin_key = admin_key
40
+ self._request_timeout = request_timeout
41
+ self._attach_timeout = attach_timeout
42
+ self._semaphore = asyncio.Semaphore(max_concurrency)
43
+ self._start_lock = asyncio.Lock()
44
+ self._handle: _compat.JanusStreamingHandle | None = None
45
+ self._handle_factory = handle_factory or _compat.JanusStreamingHandle.attach
46
+
47
+ async def start(self) -> None:
48
+ async with self._start_lock:
49
+ if self._handle is not None:
50
+ return
51
+ self._handle = await self._handle_factory(
52
+ self._session,
53
+ admin_key=self._admin_key,
54
+ attach_timeout=self._attach_timeout,
55
+ default_timeout=self._request_timeout,
56
+ )
57
+
58
+ async def close(self) -> None:
59
+ handle, self._handle = self._handle, None
60
+ if handle is not None:
61
+ await handle.close()
62
+
63
+ async def list_mountpoints(self) -> tuple[MountpointSummary, ...]:
64
+ payload = await self._command(_compat.list_request(), "list")
65
+ values = payload.get("list", payload.get("mountpoints", []))
66
+ if not isinstance(values, list):
67
+ raise StreamingProtocolError("list response did not contain a mountpoint list")
68
+ return tuple(self._parse_summary(item) for item in values)
69
+
70
+ async def info(self, mountpoint_id: int, *, secret: str | None = None) -> MountpointInfo:
71
+ payload = await self._command(
72
+ _compat.info_request(mountpoint_id, secret),
73
+ "info",
74
+ )
75
+ value = payload.get("info", payload.get("mountpoint"))
76
+ if not isinstance(value, dict):
77
+ raise StreamingProtocolError("info response did not contain mountpoint information")
78
+ return self._parse_info(value)
79
+
80
+ async def create_rtp(self, spec: RtpMountpointSpec) -> CreatedMountpoint:
81
+ return await self._create(_compat.rtp_create_request(spec, self._admin_key))
82
+
83
+ async def create_rtsp(self, spec: RtspMountpointSpec) -> CreatedMountpoint:
84
+ return await self._create(_compat.rtsp_create_request(spec, self._admin_key))
85
+
86
+ async def create_file(self, spec: FileMountpointSpec) -> CreatedMountpoint:
87
+ return await self._create(_compat.file_create_request(spec, self._admin_key))
88
+
89
+ async def edit(
90
+ self,
91
+ mountpoint_id: int,
92
+ *,
93
+ secret: str | None = None,
94
+ new_description: str | None = None,
95
+ new_metadata: str | None = None,
96
+ new_secret: str | None = None,
97
+ new_pin: str | None = None,
98
+ new_private: bool | None = None,
99
+ permanent: bool = False,
100
+ edited_event: bool = False,
101
+ ) -> dict[str, Any]:
102
+ return await self._command(
103
+ _compat.edit_request(
104
+ mountpoint_id,
105
+ secret=secret,
106
+ new_description=new_description,
107
+ new_metadata=new_metadata,
108
+ new_secret=new_secret,
109
+ new_pin=new_pin,
110
+ new_is_private=new_private,
111
+ permanent=permanent,
112
+ edited_event=edited_event,
113
+ ),
114
+ "edit",
115
+ )
116
+
117
+ async def enable(self, mountpoint_id: int, *, secret: str | None = None) -> None:
118
+ await self._command(_compat.enable_request(mountpoint_id, secret), "enable")
119
+
120
+ async def disable(
121
+ self,
122
+ mountpoint_id: int,
123
+ *,
124
+ secret: str | None = None,
125
+ stop_recording: bool = True,
126
+ ) -> None:
127
+ await self._command(
128
+ _compat.disable_request(mountpoint_id, secret, stop_recording),
129
+ "disable",
130
+ )
131
+
132
+ async def kick_all(self, mountpoint_id: int, *, secret: str | None = None) -> None:
133
+ await self._command(_compat.kick_all_request(mountpoint_id, secret), "kick_all")
134
+
135
+ async def destroy(
136
+ self,
137
+ mountpoint_id: int,
138
+ *,
139
+ secret: str | None = None,
140
+ permanent: bool = False,
141
+ ) -> None:
142
+ await self._command(
143
+ _compat.destroy_request(mountpoint_id, secret, permanent),
144
+ "destroy",
145
+ )
146
+
147
+ async def _create(self, request: Any) -> CreatedMountpoint:
148
+ payload = await self._command(request, "create")
149
+ value = payload.get("stream")
150
+ if not isinstance(value, dict):
151
+ raise StreamingProtocolError("create response did not contain a stream")
152
+ normalized = dict(value)
153
+ normalized["private"] = normalized.pop("is_private", normalized.get("private"))
154
+ normalized["permanent"] = bool(payload.get("permanent", False))
155
+ return CreatedMountpoint.model_validate(normalized)
156
+
157
+ async def _command(self, request: Any, operation: str) -> dict[str, Any]:
158
+ handle = self._require_handle()
159
+ async with self._semaphore:
160
+ response = await handle.request(request, operation=operation)
161
+ return _compat.plugin_payload(response)
162
+
163
+ def _require_handle(self) -> _compat.JanusStreamingHandle:
164
+ if self._handle is None:
165
+ raise StreamingNotStarted("StreamingAdminClient.start() has not been called")
166
+ return self._handle
167
+
168
+ @staticmethod
169
+ def _parse_summary(value: Any) -> MountpointSummary:
170
+ if not isinstance(value, dict):
171
+ value = _compat.to_jsonable(value)
172
+ normalized = dict(value)
173
+ normalized["media"] = [
174
+ StreamingAdminClient._normalize_track(item) for item in value.get("media", [])
175
+ ]
176
+ return MountpointSummary.model_validate(normalized)
177
+
178
+ @staticmethod
179
+ def _parse_info(value: Any) -> MountpointInfo:
180
+ if not isinstance(value, dict):
181
+ value = _compat.to_jsonable(value)
182
+ normalized = dict(value)
183
+ normalized["private"] = normalized.pop("is_private", normalized.get("private"))
184
+ normalized.pop("secret", None)
185
+ normalized.pop("pin", None)
186
+ normalized["media"] = [
187
+ StreamingAdminClient._normalize_track(item) for item in value.get("media", [])
188
+ ]
189
+ return MountpointInfo.model_validate(normalized)
190
+
191
+ @staticmethod
192
+ def _normalize_track(value: Any) -> dict[str, Any]:
193
+ if not isinstance(value, dict):
194
+ value = _compat.to_jsonable(value)
195
+ normalized = dict(value)
196
+ normalized["payload_type"] = normalized.pop("pt", normalized.get("payload_type"))
197
+ normalized["port"] = normalized.get("port", normalized.get("rtp_port"))
198
+ allowed = {
199
+ "mid",
200
+ "type",
201
+ "label",
202
+ "msid",
203
+ "mindex",
204
+ "age_ms",
205
+ "payload_type",
206
+ "codec",
207
+ "rtpmap",
208
+ "fmtp",
209
+ "port",
210
+ }
211
+ return {key: item for key, item in normalized.items() if key in allowed}
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class StreamingError(Exception):
5
+ """Base exception exposed by janus-api-streaming."""
6
+
7
+
8
+ class StreamingNotStarted(StreamingError):
9
+ pass
10
+
11
+
12
+ class StreamingTimeout(StreamingError):
13
+ def __init__(self, operation: str, timeout: float) -> None:
14
+ super().__init__(f"{operation!r} timed out after {timeout:.1f}s")
15
+ self.operation = operation
16
+ self.timeout = timeout
17
+
18
+
19
+ class StreamingBackendError(StreamingError):
20
+ pass
21
+
22
+
23
+ class StreamingProtocolError(StreamingError):
24
+ pass
25
+
26
+
27
+ class JanusCommandError(StreamingError):
28
+ def __init__(self, code: int | None, reason: str) -> None:
29
+ super().__init__(reason)
30
+ self.code = code
31
+ self.reason = reason
32
+
33
+
34
+ class InvalidViewerState(StreamingError):
35
+ def __init__(self, current: object, expected: tuple[object, ...]) -> None:
36
+ expected_text = ", ".join(str(value) for value in expected)
37
+ super().__init__(f"viewer is in state {current!s}; expected one of: {expected_text}")
38
+ self.current = current
39
+ self.expected = expected
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from typing import Annotated, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
8
+
9
+ class StrictModel(BaseModel):
10
+ model_config = ConfigDict(
11
+ extra="forbid",
12
+ frozen=True,
13
+ populate_by_name=True,
14
+ str_strip_whitespace=True,
15
+ )
16
+
17
+
18
+ class SessionDescription(StrictModel):
19
+ type: Literal["offer", "answer"]
20
+ sdp: str = Field(min_length=1, repr=False)
21
+
22
+
23
+ class IceCandidate(StrictModel):
24
+ candidate: str = Field(min_length=1, repr=False)
25
+ sdp_mid: str | None = Field(default=None, alias="sdpMid")
26
+ sdp_mline_index: int | None = Field(default=None, alias="sdpMLineIndex", ge=0)
27
+
28
+
29
+ class ViewerState(StrEnum):
30
+ NEW = "new"
31
+ OFFERED = "offered"
32
+ ACTIVE = "active"
33
+ PAUSED = "paused"
34
+ FAILED = "failed"
35
+ CLOSED = "closed"
36
+
37
+
38
+ class AudioTrackSpec(StrictModel):
39
+ type: Literal["audio"] = "audio"
40
+ mid: str = Field(min_length=1, max_length=64)
41
+ port: int | None = Field(default=None, ge=1, le=65535)
42
+ rtcp_port: int | None = Field(default=None, ge=1, le=65535)
43
+ payload_type: int = Field(default=111, ge=0, le=127)
44
+ codec: str = Field(default="opus", min_length=1)
45
+ fmtp: str | None = None
46
+ multicast: str | None = None
47
+ bind_interface: str | None = None
48
+ skew: bool = False
49
+ label: str | None = None
50
+ msid: str | None = None
51
+
52
+
53
+ class VideoTrackSpec(StrictModel):
54
+ type: Literal["video"] = "video"
55
+ mid: str = Field(min_length=1, max_length=64)
56
+ port: int | None = Field(default=None, ge=1, le=65535)
57
+ rtcp_port: int | None = Field(default=None, ge=1, le=65535)
58
+ payload_type: int = Field(default=96, ge=0, le=127)
59
+ codec: str = Field(default="h264", min_length=1)
60
+ fmtp: str | None = None
61
+ multicast: str | None = None
62
+ bind_interface: str | None = None
63
+ skew: bool = False
64
+ simulcast: bool = False
65
+ port2: int | None = Field(default=None, ge=1, le=65535)
66
+ port3: int | None = Field(default=None, ge=1, le=65535)
67
+ svc: bool = False
68
+ h264_sps: str | None = None
69
+ collision_ms: int | None = Field(default=None, ge=0)
70
+ label: str | None = None
71
+ msid: str | None = None
72
+
73
+
74
+ class DataTrackSpec(StrictModel):
75
+ type: Literal["data"] = "data"
76
+ mid: str = Field(min_length=1, max_length=64)
77
+ port: int | None = Field(default=None, ge=1, le=65535)
78
+ data_type: Literal["text", "binary"] = "text"
79
+ buffer_latest_message: bool = False
80
+ multicast: str | None = None
81
+ bind_interface: str | None = None
82
+ label: str | None = None
83
+ msid: str | None = None
84
+
85
+
86
+ TrackSpec = Annotated[AudioTrackSpec | VideoTrackSpec | DataTrackSpec, Field(discriminator="type")]
87
+
88
+
89
+ class RtpMountpointSpec(StrictModel):
90
+ id: int | None = Field(default=None, ge=0)
91
+ name: str | None = None
92
+ description: str | None = None
93
+ metadata: str | None = None
94
+ private: bool = True
95
+ secret: str | None = Field(default=None, repr=False)
96
+ pin: str | None = Field(default=None, repr=False)
97
+ enabled: bool = False
98
+ permanent: bool = False
99
+ media: tuple[TrackSpec, ...] = Field(min_length=1)
100
+ threads: int | None = Field(default=None, ge=0)
101
+ buffer_keyframes_ms: int | None = Field(default=None, ge=0)
102
+ buffer_keyframes_bytes: int | None = Field(default=None, ge=0)
103
+ srtp_suite: Literal[32, 80] | None = None
104
+ srtp_crypto: str | None = Field(default=None, repr=False)
105
+ e2ee: bool | None = None
106
+
107
+ @model_validator(mode="after")
108
+ def unique_mids(self) -> RtpMountpointSpec:
109
+ mids = [item.mid for item in self.media]
110
+ if len(mids) != len(set(mids)):
111
+ raise ValueError("media mids must be unique")
112
+ return self
113
+
114
+
115
+ class RtspMountpointSpec(StrictModel):
116
+ id: int | None = Field(default=None, ge=0)
117
+ url: str = Field(min_length=1)
118
+ name: str | None = None
119
+ description: str | None = None
120
+ metadata: str | None = None
121
+ private: bool = True
122
+ secret: str | None = Field(default=None, repr=False)
123
+ pin: str | None = Field(default=None, repr=False)
124
+ enabled: bool = False
125
+ permanent: bool = False
126
+ username: str | None = None
127
+ password: str | None = Field(default=None, repr=False)
128
+ reconnect_delay_seconds: int | None = Field(default=5, ge=0)
129
+ session_timeout_seconds: int | None = Field(default=None, ge=0)
130
+ media_timeout_seconds: int | None = Field(default=None, ge=0)
131
+ connection_timeout_seconds: int | None = Field(default=None, ge=0)
132
+ bind_interface: str | None = None
133
+ notify_changes: bool = True
134
+ fail_check: bool | None = None
135
+ quirk: bool | None = None
136
+
137
+
138
+ class FileMountpointSpec(StrictModel):
139
+ type: Literal["live", "ondemand"]
140
+ id: int | None = Field(default=None, ge=0)
141
+ filename: str = Field(min_length=1)
142
+ name: str | None = None
143
+ description: str | None = None
144
+ metadata: str | None = None
145
+ private: bool = True
146
+ secret: str | None = Field(default=None, repr=False)
147
+ pin: str | None = Field(default=None, repr=False)
148
+ enabled: bool = False
149
+ permanent: bool = False
150
+ audio: bool = True
151
+ video: bool = False
152
+
153
+
154
+ class MountpointTrack(StrictModel):
155
+ mid: str
156
+ type: Literal["audio", "video", "data"]
157
+ label: str | None = None
158
+ msid: str | None = None
159
+ mindex: int | None = None
160
+ age_ms: int | None = Field(default=None, ge=0)
161
+ payload_type: int | None = Field(default=None, ge=0, le=127)
162
+ codec: str | None = None
163
+ rtpmap: str | None = None
164
+ fmtp: str | None = None
165
+ port: int | None = Field(default=None, ge=1, le=65535)
166
+
167
+
168
+ class MountpointSummary(StrictModel):
169
+ id: int
170
+ type: Literal["rtp", "live", "ondemand", "rtsp"]
171
+ description: str | None = None
172
+ metadata: str | None = None
173
+ enabled: bool | None = None
174
+ media: tuple[MountpointTrack, ...] = ()
175
+
176
+
177
+ class MountpointInfo(StrictModel):
178
+ id: int
179
+ type: Literal["rtp", "live", "ondemand", "rtsp"]
180
+ name: str | None = None
181
+ description: str | None = None
182
+ metadata: str | None = None
183
+ private: bool | None = None
184
+ viewers: int = Field(default=0, ge=0)
185
+ enabled: bool | None = None
186
+ media: tuple[MountpointTrack, ...] = ()
187
+
188
+
189
+ class CreatedPort(StrictModel):
190
+ type: Literal["audio", "video", "data"]
191
+ mid: str
192
+ port: int = Field(ge=1, le=65535)
193
+ msid: str | None = None
194
+
195
+
196
+ class CreatedMountpoint(StrictModel):
197
+ id: int
198
+ type: Literal["rtp", "live", "ondemand", "rtsp"]
199
+ description: str | None = None
200
+ private: bool | None = None
201
+ permanent: bool = False
202
+ ports: tuple[CreatedPort, ...] = ()
203
+
204
+
205
+ class ConfigureStream(StrictModel):
206
+ mid: str
207
+ send: bool | None = None
208
+ substream: int | None = Field(default=None, ge=0, le=2)
209
+ temporal: int | None = Field(default=None, ge=0, le=2)
210
+ fallback_microseconds: int | None = Field(default=250_000, ge=0)
211
+ spatial_layer: int | None = Field(default=None, ge=0, le=1)
212
+ temporal_layer: int | None = Field(default=None, ge=0, le=2)
213
+ min_delay: int | None = Field(default=None, ge=0)
214
+ max_delay: int | None = Field(default=None, ge=0)
File without changes