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.
- janus_api_streaming-0.2.0.dist-info/METADATA +57 -0
- janus_api_streaming-0.2.0.dist-info/RECORD +14 -0
- janus_api_streaming-0.2.0.dist-info/WHEEL +4 -0
- janus_api_streaming-0.2.0.dist-info/entry_points.txt +2 -0
- janus_streaming/__init__.py +67 -0
- janus_streaming/_compat.py +638 -0
- janus_streaming/admin.py +211 -0
- janus_streaming/errors.py +39 -0
- janus_streaming/models.py +214 -0
- janus_streaming/py.typed +0 -0
- janus_streaming/requests.py +489 -0
- janus_streaming/responses.py +189 -0
- janus_streaming/runtime.py +143 -0
- janus_streaming/viewer.py +170 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import builtins
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Any, Literal, TypeVar, Unpack
|
|
7
|
+
|
|
8
|
+
from janus_api.lib import Plugin
|
|
9
|
+
from janus_api.models import JanusResponse
|
|
10
|
+
from janus_api.models.base import Jsep
|
|
11
|
+
from janus_api.models.request import TrickleCandidate
|
|
12
|
+
|
|
13
|
+
from .errors import (
|
|
14
|
+
JanusCommandError,
|
|
15
|
+
StreamingBackendError,
|
|
16
|
+
StreamingProtocolError,
|
|
17
|
+
StreamingTimeout,
|
|
18
|
+
)
|
|
19
|
+
from .models import (
|
|
20
|
+
ConfigureStream,
|
|
21
|
+
DataTrackSpec,
|
|
22
|
+
FileMountpointSpec,
|
|
23
|
+
IceCandidate,
|
|
24
|
+
RtpMountpointSpec,
|
|
25
|
+
RtspMountpointSpec,
|
|
26
|
+
SessionDescription,
|
|
27
|
+
)
|
|
28
|
+
from .requests import (
|
|
29
|
+
ConfigureRequest,
|
|
30
|
+
CreateAudioMedia,
|
|
31
|
+
CreateDataMedia,
|
|
32
|
+
CreateMedia,
|
|
33
|
+
CreateRequest,
|
|
34
|
+
CreateVideoMedia,
|
|
35
|
+
DestroyRequest,
|
|
36
|
+
DisableRequest,
|
|
37
|
+
EditRequest,
|
|
38
|
+
EnableRequest,
|
|
39
|
+
InfoRequest,
|
|
40
|
+
KickAllRequest,
|
|
41
|
+
ListRequest,
|
|
42
|
+
MountPoint,
|
|
43
|
+
PauseRequest,
|
|
44
|
+
RecordingMedia,
|
|
45
|
+
RecordingRequest,
|
|
46
|
+
StartRequest,
|
|
47
|
+
StopRequest,
|
|
48
|
+
SwitchRequest,
|
|
49
|
+
WatchRequest,
|
|
50
|
+
)
|
|
51
|
+
from .requests import ConfigureStream as ProtocolConfigureStream
|
|
52
|
+
|
|
53
|
+
T = TypeVar("T")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StreamingPlugin(Plugin):
|
|
57
|
+
"""Concrete Streaming handle and typed convenience API.
|
|
58
|
+
|
|
59
|
+
``mountpoint`` and ``admin_key`` are optional convenience defaults. They
|
|
60
|
+
are package-owned state and never leak into core plugin construction.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
identifier = "streaming"
|
|
64
|
+
name = "janus.plugin.streaming"
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
session: Any,
|
|
70
|
+
mountpoint: str | int | None = None,
|
|
71
|
+
admin_key: str | None = None,
|
|
72
|
+
plugin_id: str | int | None = None,
|
|
73
|
+
**kwargs: Any,
|
|
74
|
+
) -> None:
|
|
75
|
+
super().__init__(session=session, plugin_id=plugin_id, **kwargs)
|
|
76
|
+
self._mountpoint = mountpoint
|
|
77
|
+
self._admin_key = admin_key
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def mountpoint(self) -> str | int | None:
|
|
81
|
+
return self._mountpoint
|
|
82
|
+
|
|
83
|
+
def _resolve_mountpoint_id(self, mountpoint_id: int | str | None = None) -> int:
|
|
84
|
+
target = mountpoint_id if mountpoint_id is not None else self._mountpoint
|
|
85
|
+
if target is None:
|
|
86
|
+
raise ValueError("mountpoint ID is required when the plugin is not bound")
|
|
87
|
+
return int(target)
|
|
88
|
+
|
|
89
|
+
async def list(self) -> JanusResponse:
|
|
90
|
+
return await self.send(ListRequest())
|
|
91
|
+
|
|
92
|
+
async def info(
|
|
93
|
+
self,
|
|
94
|
+
secret: str | None = None,
|
|
95
|
+
*,
|
|
96
|
+
mountpoint_id: int | str | None = None,
|
|
97
|
+
) -> JanusResponse:
|
|
98
|
+
return await self.send(
|
|
99
|
+
InfoRequest(id=self._resolve_mountpoint_id(mountpoint_id), secret=secret)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def create(
|
|
103
|
+
self,
|
|
104
|
+
*,
|
|
105
|
+
admin_key: str | None = None,
|
|
106
|
+
**kwargs: Unpack[MountPoint],
|
|
107
|
+
) -> JanusResponse:
|
|
108
|
+
effective_admin_key = self._admin_key if admin_key is None else admin_key
|
|
109
|
+
payload = CreateRequest.model_validate({"admin_key": effective_admin_key, **kwargs})
|
|
110
|
+
return await self.send(payload)
|
|
111
|
+
|
|
112
|
+
async def destroy(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
mountpoint_id: int | str | None = None,
|
|
116
|
+
secret: str | None = None,
|
|
117
|
+
permanent: bool = False,
|
|
118
|
+
) -> JanusResponse:
|
|
119
|
+
return await self.send(
|
|
120
|
+
DestroyRequest(
|
|
121
|
+
id=self._resolve_mountpoint_id(mountpoint_id),
|
|
122
|
+
permanent=permanent,
|
|
123
|
+
secret=secret,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def recording(
|
|
128
|
+
self,
|
|
129
|
+
action: Literal["start", "stop"],
|
|
130
|
+
media: builtins.list[RecordingMedia],
|
|
131
|
+
*,
|
|
132
|
+
mountpoint_id: int | str | None = None,
|
|
133
|
+
) -> JanusResponse:
|
|
134
|
+
return await self.send(
|
|
135
|
+
RecordingRequest(
|
|
136
|
+
action=action,
|
|
137
|
+
id=self._resolve_mountpoint_id(mountpoint_id),
|
|
138
|
+
media=media,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def edit(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
mountpoint_id: int | str | None = None,
|
|
146
|
+
secret: str | None = None,
|
|
147
|
+
new_description: str | None = None,
|
|
148
|
+
new_metadata: str | None = None,
|
|
149
|
+
new_secret: str | None = None,
|
|
150
|
+
new_pin: str | None = None,
|
|
151
|
+
new_is_private: bool | None = None,
|
|
152
|
+
edited_event: bool = False,
|
|
153
|
+
permanent: bool = False,
|
|
154
|
+
) -> JanusResponse:
|
|
155
|
+
return await self.send(
|
|
156
|
+
EditRequest(
|
|
157
|
+
id=self._resolve_mountpoint_id(mountpoint_id),
|
|
158
|
+
secret=secret,
|
|
159
|
+
new_description=new_description,
|
|
160
|
+
new_metadata=new_metadata,
|
|
161
|
+
new_secret=new_secret,
|
|
162
|
+
new_pin=new_pin,
|
|
163
|
+
new_is_private=new_is_private,
|
|
164
|
+
edited_event=edited_event,
|
|
165
|
+
permanent=permanent,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def enable(
|
|
170
|
+
self,
|
|
171
|
+
*,
|
|
172
|
+
secret: str | None = None,
|
|
173
|
+
mountpoint_id: int | str | None = None,
|
|
174
|
+
) -> JanusResponse:
|
|
175
|
+
return await self.send(
|
|
176
|
+
EnableRequest(id=self._resolve_mountpoint_id(mountpoint_id), secret=secret)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def disable(
|
|
180
|
+
self,
|
|
181
|
+
*,
|
|
182
|
+
mountpoint_id: int | str | None = None,
|
|
183
|
+
secret: str | None = None,
|
|
184
|
+
stop_recording: bool = True,
|
|
185
|
+
) -> JanusResponse:
|
|
186
|
+
return await self.send(
|
|
187
|
+
DisableRequest(
|
|
188
|
+
id=self._resolve_mountpoint_id(mountpoint_id),
|
|
189
|
+
stop_recording=stop_recording,
|
|
190
|
+
secret=secret,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def kick_all(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
secret: str | None = None,
|
|
198
|
+
mountpoint_id: int | str | None = None,
|
|
199
|
+
) -> JanusResponse:
|
|
200
|
+
return await self.send(
|
|
201
|
+
KickAllRequest(id=self._resolve_mountpoint_id(mountpoint_id), secret=secret)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def watch(
|
|
205
|
+
self,
|
|
206
|
+
*,
|
|
207
|
+
mountpoint_id: int | str | None = None,
|
|
208
|
+
pin: str | None = None,
|
|
209
|
+
media: builtins.list[str] | None = None,
|
|
210
|
+
offer_audio: bool | None = None,
|
|
211
|
+
offer_video: bool | None = None,
|
|
212
|
+
offer_data: bool | None = None,
|
|
213
|
+
) -> JanusResponse:
|
|
214
|
+
return await self.send(
|
|
215
|
+
WatchRequest(
|
|
216
|
+
id=self._resolve_mountpoint_id(mountpoint_id),
|
|
217
|
+
pin=pin,
|
|
218
|
+
media=list(dict.fromkeys(media or ())),
|
|
219
|
+
offer_audio=offer_audio,
|
|
220
|
+
offer_video=offer_video,
|
|
221
|
+
offer_data=offer_data,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
async def start_streaming(self, jsep: Jsep | None = None) -> JanusResponse:
|
|
226
|
+
return await self.send(StartRequest(), jsep=jsep)
|
|
227
|
+
|
|
228
|
+
async def pause(self) -> JanusResponse:
|
|
229
|
+
return await self.send(PauseRequest())
|
|
230
|
+
|
|
231
|
+
async def configure(self, streams: builtins.list[ProtocolConfigureStream]) -> JanusResponse:
|
|
232
|
+
return await self.send(ConfigureRequest(streams=streams))
|
|
233
|
+
|
|
234
|
+
async def switch(self, mountpoint_id: int | str) -> JanusResponse:
|
|
235
|
+
return await self.send(SwitchRequest(id=int(mountpoint_id)))
|
|
236
|
+
|
|
237
|
+
async def stop_streaming(self) -> JanusResponse:
|
|
238
|
+
return await self.send(StopRequest())
|
|
239
|
+
|
|
240
|
+
async def subscribe(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
mountpoint_id: int | str | None = None,
|
|
244
|
+
pin: str | None = None,
|
|
245
|
+
media: builtins.list[str] | None = None,
|
|
246
|
+
offer_audio: bool | None = None,
|
|
247
|
+
offer_video: bool | None = None,
|
|
248
|
+
offer_data: bool | None = None,
|
|
249
|
+
) -> JanusResponse:
|
|
250
|
+
"""Alias for :meth:`watch` retained for existing applications."""
|
|
251
|
+
|
|
252
|
+
return await self.watch(
|
|
253
|
+
mountpoint_id=mountpoint_id,
|
|
254
|
+
pin=pin,
|
|
255
|
+
media=media,
|
|
256
|
+
offer_audio=offer_audio,
|
|
257
|
+
offer_video=offer_video,
|
|
258
|
+
offer_data=offer_data,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def to_jsonable(value: Any, *, by_alias: bool = True) -> Any:
|
|
263
|
+
model_dump = getattr(value, "model_dump", None)
|
|
264
|
+
if callable(model_dump):
|
|
265
|
+
return model_dump(mode="json", by_alias=by_alias, exclude_none=True)
|
|
266
|
+
if isinstance(value, dict):
|
|
267
|
+
return value
|
|
268
|
+
attributes = getattr(value, "__dict__", None)
|
|
269
|
+
if isinstance(attributes, dict):
|
|
270
|
+
return {key: item for key, item in attributes.items() if not key.startswith("_")}
|
|
271
|
+
return value
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def plugin_payload(response: Any) -> dict[str, Any]:
|
|
275
|
+
if getattr(response, "janus", None) == "error":
|
|
276
|
+
error = getattr(response, "error", None)
|
|
277
|
+
raise JanusCommandError(
|
|
278
|
+
code=getattr(error, "code", None),
|
|
279
|
+
reason=getattr(error, "reason", "unknown Janus error"),
|
|
280
|
+
)
|
|
281
|
+
plugindata = getattr(response, "plugindata", None)
|
|
282
|
+
if plugindata is None:
|
|
283
|
+
raise StreamingProtocolError("expected a Janus plugin response containing plugindata")
|
|
284
|
+
payload = to_jsonable(plugindata.data)
|
|
285
|
+
if not isinstance(payload, dict):
|
|
286
|
+
raise StreamingProtocolError("Janus plugin payload is not an object")
|
|
287
|
+
if "error_code" in payload or ("error" in payload and "status" not in payload):
|
|
288
|
+
raise JanusCommandError(
|
|
289
|
+
code=int(payload["error_code"]) if payload.get("error_code") is not None else None,
|
|
290
|
+
reason=str(payload.get("error") or "Janus Streaming command failed"),
|
|
291
|
+
)
|
|
292
|
+
return payload
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def response_jsep(response: Any) -> SessionDescription:
|
|
296
|
+
jsep = getattr(response, "jsep", None)
|
|
297
|
+
if jsep is None:
|
|
298
|
+
payload = plugin_payload(response)
|
|
299
|
+
raw = payload.get("jsep")
|
|
300
|
+
if isinstance(raw, dict):
|
|
301
|
+
return SessionDescription.model_validate(raw)
|
|
302
|
+
raise StreamingProtocolError("Janus watch response did not contain JSEP")
|
|
303
|
+
raw = to_jsonable(jsep)
|
|
304
|
+
return SessionDescription.model_validate(raw)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def list_request() -> ListRequest:
|
|
308
|
+
return ListRequest()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def info_request(mountpoint_id: int, secret: str | None) -> InfoRequest:
|
|
312
|
+
return InfoRequest(id=mountpoint_id, secret=secret)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def rtp_create_request(spec: RtpMountpointSpec, admin_key: str | None) -> CreateRequest:
|
|
316
|
+
media: list[CreateMedia] = []
|
|
317
|
+
for item in spec.media:
|
|
318
|
+
if item.type == "audio":
|
|
319
|
+
media.append(
|
|
320
|
+
CreateAudioMedia(
|
|
321
|
+
mid=item.mid,
|
|
322
|
+
msid=item.msid,
|
|
323
|
+
label=item.label,
|
|
324
|
+
mcast=item.multicast,
|
|
325
|
+
iface=item.bind_interface,
|
|
326
|
+
port=item.port,
|
|
327
|
+
rtcpport=item.rtcp_port,
|
|
328
|
+
pt=item.payload_type,
|
|
329
|
+
codec=item.codec,
|
|
330
|
+
fmtp=item.fmtp,
|
|
331
|
+
skew=item.skew,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
elif item.type == "video":
|
|
335
|
+
media.append(
|
|
336
|
+
CreateVideoMedia(
|
|
337
|
+
mid=item.mid,
|
|
338
|
+
msid=item.msid,
|
|
339
|
+
label=item.label,
|
|
340
|
+
mcast=item.multicast,
|
|
341
|
+
iface=item.bind_interface,
|
|
342
|
+
port=item.port,
|
|
343
|
+
rtcpport=item.rtcp_port,
|
|
344
|
+
pt=item.payload_type,
|
|
345
|
+
codec=item.codec,
|
|
346
|
+
fmtp=item.fmtp,
|
|
347
|
+
skew=item.skew,
|
|
348
|
+
simulcast=item.simulcast,
|
|
349
|
+
port2=item.port2,
|
|
350
|
+
port3=item.port3,
|
|
351
|
+
svc=item.svc,
|
|
352
|
+
h264sps=item.h264_sps,
|
|
353
|
+
collision=item.collision_ms,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
data: DataTrackSpec = item
|
|
358
|
+
media.append(
|
|
359
|
+
CreateDataMedia(
|
|
360
|
+
mid=data.mid,
|
|
361
|
+
msid=data.msid,
|
|
362
|
+
label=data.label,
|
|
363
|
+
mcast=data.multicast,
|
|
364
|
+
iface=data.bind_interface,
|
|
365
|
+
port=data.port,
|
|
366
|
+
datatype=data.data_type,
|
|
367
|
+
databuffermsg=data.buffer_latest_message,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
return CreateRequest(
|
|
371
|
+
admin_key=admin_key,
|
|
372
|
+
type="rtp",
|
|
373
|
+
id=spec.id,
|
|
374
|
+
name=spec.name,
|
|
375
|
+
description=spec.description,
|
|
376
|
+
metadata=spec.metadata,
|
|
377
|
+
secret=spec.secret,
|
|
378
|
+
pin=spec.pin,
|
|
379
|
+
is_private=spec.private,
|
|
380
|
+
permanent=spec.permanent,
|
|
381
|
+
enabled=spec.enabled,
|
|
382
|
+
media=media,
|
|
383
|
+
threads=spec.threads,
|
|
384
|
+
bufferkf_ms=spec.buffer_keyframes_ms,
|
|
385
|
+
bufferkf_bytes=spec.buffer_keyframes_bytes,
|
|
386
|
+
srtpsuite=spec.srtp_suite,
|
|
387
|
+
srtpcrypto=spec.srtp_crypto,
|
|
388
|
+
e2ee=spec.e2ee,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def rtsp_create_request(spec: RtspMountpointSpec, admin_key: str | None) -> CreateRequest:
|
|
393
|
+
return CreateRequest(
|
|
394
|
+
admin_key=admin_key,
|
|
395
|
+
type="rtsp",
|
|
396
|
+
id=spec.id,
|
|
397
|
+
name=spec.name,
|
|
398
|
+
description=spec.description,
|
|
399
|
+
metadata=spec.metadata,
|
|
400
|
+
secret=spec.secret,
|
|
401
|
+
pin=spec.pin,
|
|
402
|
+
is_private=spec.private,
|
|
403
|
+
permanent=spec.permanent,
|
|
404
|
+
enabled=spec.enabled,
|
|
405
|
+
url=spec.url,
|
|
406
|
+
rtsp_user=spec.username,
|
|
407
|
+
rtsp_pwd=spec.password,
|
|
408
|
+
rtsp_reconnect_delay=spec.reconnect_delay_seconds,
|
|
409
|
+
rtsp_session_timeout=spec.session_timeout_seconds,
|
|
410
|
+
rtsp_timeout=spec.media_timeout_seconds,
|
|
411
|
+
rtsp_conn_timeout=spec.connection_timeout_seconds,
|
|
412
|
+
rtspiface=spec.bind_interface,
|
|
413
|
+
rtsp_notify_changes=spec.notify_changes,
|
|
414
|
+
rtsp_failcheck=spec.fail_check,
|
|
415
|
+
rtsp_quirk=spec.quirk,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def file_create_request(spec: FileMountpointSpec, admin_key: str | None) -> CreateRequest:
|
|
420
|
+
return CreateRequest(
|
|
421
|
+
admin_key=admin_key,
|
|
422
|
+
type=spec.type,
|
|
423
|
+
id=spec.id,
|
|
424
|
+
name=spec.name,
|
|
425
|
+
description=spec.description,
|
|
426
|
+
metadata=spec.metadata,
|
|
427
|
+
secret=spec.secret,
|
|
428
|
+
pin=spec.pin,
|
|
429
|
+
is_private=spec.private,
|
|
430
|
+
permanent=spec.permanent,
|
|
431
|
+
enabled=spec.enabled,
|
|
432
|
+
filename=spec.filename,
|
|
433
|
+
audio=spec.audio,
|
|
434
|
+
video=spec.video,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def destroy_request(mountpoint_id: int, secret: str | None, permanent: bool) -> DestroyRequest:
|
|
439
|
+
return DestroyRequest(id=mountpoint_id, secret=secret, permanent=permanent)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def enable_request(mountpoint_id: int, secret: str | None) -> EnableRequest:
|
|
443
|
+
return EnableRequest(id=mountpoint_id, secret=secret)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def disable_request(mountpoint_id: int, secret: str | None, stop_recording: bool) -> DisableRequest:
|
|
447
|
+
return DisableRequest(id=mountpoint_id, secret=secret, stop_recording=stop_recording)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def kick_all_request(mountpoint_id: int, secret: str | None) -> KickAllRequest:
|
|
451
|
+
return KickAllRequest(id=mountpoint_id, secret=secret)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def edit_request(mountpoint_id: int, **kwargs: Any) -> EditRequest:
|
|
455
|
+
return EditRequest(id=mountpoint_id, **kwargs)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def watch_request(
|
|
459
|
+
mountpoint_id: int,
|
|
460
|
+
*,
|
|
461
|
+
pin: str | None,
|
|
462
|
+
media: list[str],
|
|
463
|
+
offer_audio: bool | None,
|
|
464
|
+
offer_video: bool | None,
|
|
465
|
+
offer_data: bool | None,
|
|
466
|
+
) -> WatchRequest:
|
|
467
|
+
return WatchRequest(
|
|
468
|
+
id=mountpoint_id,
|
|
469
|
+
pin=pin,
|
|
470
|
+
media=media,
|
|
471
|
+
offer_audio=offer_audio,
|
|
472
|
+
offer_video=offer_video,
|
|
473
|
+
offer_data=offer_data,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def start_request() -> StartRequest:
|
|
478
|
+
return StartRequest()
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def pause_request() -> PauseRequest:
|
|
482
|
+
return PauseRequest()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def stop_request() -> StopRequest:
|
|
486
|
+
return StopRequest()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def switch_request(mountpoint_id: int) -> SwitchRequest:
|
|
490
|
+
return SwitchRequest(id=mountpoint_id)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def configure_request(streams: list[ConfigureStream]) -> ConfigureRequest:
|
|
494
|
+
values = [
|
|
495
|
+
ProtocolConfigureStream(
|
|
496
|
+
mid=item.mid,
|
|
497
|
+
send=item.send,
|
|
498
|
+
substream=item.substream,
|
|
499
|
+
temporal=item.temporal,
|
|
500
|
+
fallback=item.fallback_microseconds,
|
|
501
|
+
spatial_layer=item.spatial_layer,
|
|
502
|
+
temporal_layer=item.temporal_layer,
|
|
503
|
+
min_delay=item.min_delay,
|
|
504
|
+
max_delay=item.max_delay,
|
|
505
|
+
)
|
|
506
|
+
for item in streams
|
|
507
|
+
]
|
|
508
|
+
return ConfigureRequest(streams=values)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def answer_jsep(answer: SessionDescription) -> Jsep:
|
|
512
|
+
return Jsep(type="answer", sdp=answer.sdp)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def trickle_candidate(candidate: IceCandidate) -> TrickleCandidate:
|
|
516
|
+
return TrickleCandidate(
|
|
517
|
+
candidate=candidate.candidate,
|
|
518
|
+
sdpMid=candidate.sdp_mid,
|
|
519
|
+
sdpMLineIndex=candidate.sdp_mline_index,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def trickle_complete() -> TrickleCandidate:
|
|
524
|
+
return TrickleCandidate(completed=True)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def create_session_manager() -> Any:
|
|
528
|
+
"""Create and globally register janus-api's default session manager."""
|
|
529
|
+
|
|
530
|
+
from janus_api.conf import Janus
|
|
531
|
+
from janus_api.servers import JanusSessionManager
|
|
532
|
+
|
|
533
|
+
manager = JanusSessionManager()
|
|
534
|
+
Janus.set_manager(manager)
|
|
535
|
+
return manager
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class JanusStreamingHandle:
|
|
539
|
+
def __init__(self, plugin: Any, *, default_timeout: float) -> None:
|
|
540
|
+
if default_timeout <= 0:
|
|
541
|
+
raise ValueError("default_timeout must be greater than zero")
|
|
542
|
+
self._plugin = plugin
|
|
543
|
+
self._default_timeout = default_timeout
|
|
544
|
+
self._closed = False
|
|
545
|
+
|
|
546
|
+
@classmethod
|
|
547
|
+
async def attach(
|
|
548
|
+
cls,
|
|
549
|
+
session: Any,
|
|
550
|
+
*,
|
|
551
|
+
mountpoint_id: int | None = None,
|
|
552
|
+
admin_key: str | None = None,
|
|
553
|
+
attach_timeout: float = 5.0,
|
|
554
|
+
default_timeout: float = 10.0,
|
|
555
|
+
on_event: Callable[[Any], Any] | None = None,
|
|
556
|
+
) -> JanusStreamingHandle:
|
|
557
|
+
if attach_timeout <= 0:
|
|
558
|
+
raise ValueError("attach_timeout must be greater than zero")
|
|
559
|
+
if default_timeout <= 0:
|
|
560
|
+
raise ValueError("default_timeout must be greater than zero")
|
|
561
|
+
# Mountpoint IDs and the admin key belong in Streaming request bodies,
|
|
562
|
+
# not in core handle construction. Keep these parameters for the
|
|
563
|
+
# stable high-level factory signature used by admin/viewer clients.
|
|
564
|
+
_ = mountpoint_id, admin_key
|
|
565
|
+
kwargs: dict[str, Any] = {"session": session}
|
|
566
|
+
if on_event is not None:
|
|
567
|
+
kwargs["on_event"] = on_event
|
|
568
|
+
try:
|
|
569
|
+
async with asyncio.timeout(attach_timeout):
|
|
570
|
+
plugin = await StreamingPlugin(**kwargs).attach()
|
|
571
|
+
except TimeoutError as exc:
|
|
572
|
+
raise StreamingTimeout("attach", attach_timeout) from exc
|
|
573
|
+
except Exception as exc:
|
|
574
|
+
raise StreamingBackendError("could not attach Janus Streaming plugin") from exc
|
|
575
|
+
return cls(plugin, default_timeout=default_timeout)
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def handle_id(self) -> str:
|
|
579
|
+
return str(self._plugin.id)
|
|
580
|
+
|
|
581
|
+
async def request(
|
|
582
|
+
self,
|
|
583
|
+
body: Any,
|
|
584
|
+
*,
|
|
585
|
+
operation: str,
|
|
586
|
+
jsep: Any | None = None,
|
|
587
|
+
timeout_seconds: float | None = None,
|
|
588
|
+
) -> Any:
|
|
589
|
+
if self._closed:
|
|
590
|
+
raise StreamingBackendError("Janus Streaming handle is closed")
|
|
591
|
+
return await self._run(
|
|
592
|
+
operation,
|
|
593
|
+
self._plugin.send(body, jsep=jsep),
|
|
594
|
+
timeout_seconds=timeout_seconds,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
async def trickle(
|
|
598
|
+
self,
|
|
599
|
+
candidates: list[Any],
|
|
600
|
+
*,
|
|
601
|
+
timeout_seconds: float | None = None,
|
|
602
|
+
) -> Any:
|
|
603
|
+
if self._closed:
|
|
604
|
+
raise StreamingBackendError("Janus Streaming handle is closed")
|
|
605
|
+
return await self._run(
|
|
606
|
+
"trickle",
|
|
607
|
+
self._plugin.trickle(candidates),
|
|
608
|
+
timeout_seconds=timeout_seconds,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
async def close(self) -> None:
|
|
612
|
+
if self._closed:
|
|
613
|
+
return
|
|
614
|
+
self._closed = True
|
|
615
|
+
try:
|
|
616
|
+
await self._run("detach", self._plugin.detach(), timeout_seconds=3.0)
|
|
617
|
+
except StreamingBackendError:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
async def _run(
|
|
621
|
+
self,
|
|
622
|
+
operation: str,
|
|
623
|
+
awaitable: Awaitable[T],
|
|
624
|
+
*,
|
|
625
|
+
timeout_seconds: float | None,
|
|
626
|
+
) -> T:
|
|
627
|
+
effective = timeout_seconds if timeout_seconds is not None else self._default_timeout
|
|
628
|
+
if effective <= 0:
|
|
629
|
+
raise ValueError("timeout_seconds must be greater than zero")
|
|
630
|
+
try:
|
|
631
|
+
async with asyncio.timeout(effective):
|
|
632
|
+
return await awaitable
|
|
633
|
+
except TimeoutError as exc:
|
|
634
|
+
raise StreamingTimeout(operation, effective) from exc
|
|
635
|
+
except JanusCommandError:
|
|
636
|
+
raise
|
|
637
|
+
except Exception as exc:
|
|
638
|
+
raise StreamingBackendError(f"Janus Streaming operation {operation!r} failed") from exc
|