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,143 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from . import _compat
9
+ from .admin import StreamingAdminClient
10
+ from .errors import StreamingNotStarted
11
+ from .viewer import StreamingViewer
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class StreamingRuntimeSettings:
16
+ admin_key: str | None = field(default=None, repr=False)
17
+ request_timeout: float = 10.0
18
+ negotiation_timeout: float = 15.0
19
+ attach_timeout: float = 5.0
20
+ max_admin_concurrency: int = 32
21
+
22
+ def __post_init__(self) -> None:
23
+ if self.request_timeout <= 0:
24
+ raise ValueError("request_timeout must be greater than zero")
25
+ if self.negotiation_timeout <= 0:
26
+ raise ValueError("negotiation_timeout must be greater than zero")
27
+ if self.attach_timeout <= 0:
28
+ raise ValueError("attach_timeout must be greater than zero")
29
+ if self.max_admin_concurrency < 1:
30
+ raise ValueError("max_admin_concurrency must be at least one")
31
+
32
+
33
+ class StreamingRuntime:
34
+ """Owns Streaming-plugin resources for an already-created Janus session."""
35
+
36
+ def __init__(self, session: Any, settings: StreamingRuntimeSettings | None = None) -> None:
37
+ self.session = session
38
+ self.settings = settings or StreamingRuntimeSettings()
39
+ self._admin: StreamingAdminClient | None = None
40
+ self._viewers: set[StreamingViewer] = set()
41
+ self._lock = asyncio.Lock()
42
+
43
+ @property
44
+ def admin(self) -> StreamingAdminClient:
45
+ if self._admin is None:
46
+ raise StreamingNotStarted("StreamingRuntime.start() has not been called")
47
+ return self._admin
48
+
49
+ async def start(self) -> None:
50
+ async with self._lock:
51
+ if self._admin is not None:
52
+ return
53
+ admin = StreamingAdminClient(
54
+ self.session,
55
+ admin_key=self.settings.admin_key,
56
+ request_timeout=self.settings.request_timeout,
57
+ attach_timeout=self.settings.attach_timeout,
58
+ max_concurrency=self.settings.max_admin_concurrency,
59
+ )
60
+ await admin.start()
61
+ self._admin = admin
62
+
63
+ def create_viewer(self, mountpoint_id: int, **kwargs: Any) -> StreamingViewer:
64
+ viewer = StreamingViewer(
65
+ self.session,
66
+ mountpoint_id=mountpoint_id,
67
+ request_timeout=self.settings.request_timeout,
68
+ negotiation_timeout=self.settings.negotiation_timeout,
69
+ attach_timeout=self.settings.attach_timeout,
70
+ **kwargs,
71
+ )
72
+ self._viewers.add(viewer)
73
+ return viewer
74
+
75
+ async def release_viewer(self, viewer: StreamingViewer) -> None:
76
+ try:
77
+ await viewer.close()
78
+ finally:
79
+ self._viewers.discard(viewer)
80
+
81
+ async def close(self) -> None:
82
+ async with self._lock:
83
+ admin, self._admin = self._admin, None
84
+ viewers = tuple(self._viewers)
85
+ self._viewers.clear()
86
+ if viewers:
87
+ await asyncio.gather(*(viewer.close() for viewer in viewers), return_exceptions=True)
88
+ if admin is not None:
89
+ await admin.close()
90
+
91
+
92
+ class ManagedStreamingRuntime:
93
+ """Optional helper that also owns `janus-api`'s session manager.
94
+
95
+ The import is lazy so applications that inject an existing Janus session do
96
+ not import the `janus-api` server/Redis stack.
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ settings: StreamingRuntimeSettings | None = None,
102
+ *,
103
+ manager_factory: Callable[[], Any] | None = None,
104
+ ) -> None:
105
+ self.settings = settings or StreamingRuntimeSettings()
106
+ self._manager_factory = manager_factory
107
+ self._manager: Any | None = None
108
+ self._runtime: StreamingRuntime | None = None
109
+
110
+ @property
111
+ def runtime(self) -> StreamingRuntime:
112
+ if self._runtime is None:
113
+ raise StreamingNotStarted("ManagedStreamingRuntime.start() has not been called")
114
+ return self._runtime
115
+
116
+ async def start(self) -> None:
117
+ if self._runtime is not None:
118
+ return
119
+ if self._manager_factory is None:
120
+ manager = _compat.create_session_manager()
121
+ else:
122
+ manager = self._manager_factory()
123
+ await manager.start()
124
+ session = manager.get_session()
125
+ if session is None:
126
+ await manager.stop()
127
+ raise StreamingNotStarted("janus-api started without an available session")
128
+ runtime = StreamingRuntime(session, self.settings)
129
+ try:
130
+ await runtime.start()
131
+ except BaseException:
132
+ await manager.stop()
133
+ raise
134
+ self._manager = manager
135
+ self._runtime = runtime
136
+
137
+ async def close(self) -> None:
138
+ runtime, self._runtime = self._runtime, None
139
+ manager, self._manager = self._manager, None
140
+ if runtime is not None:
141
+ await runtime.close()
142
+ if manager is not None:
143
+ await manager.stop()
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable, Iterable
5
+ from contextlib import suppress
6
+ from typing import Any
7
+
8
+ from . import _compat
9
+ from .errors import InvalidViewerState, StreamingProtocolError
10
+ from .models import ConfigureStream, IceCandidate, SessionDescription, ViewerState
11
+
12
+ HandleFactory = Callable[..., Awaitable[_compat.JanusStreamingHandle]]
13
+
14
+
15
+ class StreamingViewer:
16
+ """One Janus Streaming handle and one browser PeerConnection."""
17
+
18
+ def __init__(
19
+ self,
20
+ session: Any,
21
+ *,
22
+ mountpoint_id: int,
23
+ request_timeout: float = 10.0,
24
+ negotiation_timeout: float = 15.0,
25
+ attach_timeout: float = 5.0,
26
+ handle_factory: HandleFactory | None = None,
27
+ on_event: Callable[[Any], Any] | None = None,
28
+ ) -> None:
29
+ if mountpoint_id < 0:
30
+ raise ValueError("mountpoint_id must not be negative")
31
+ if request_timeout <= 0:
32
+ raise ValueError("request_timeout must be greater than zero")
33
+ if negotiation_timeout <= 0:
34
+ raise ValueError("negotiation_timeout must be greater than zero")
35
+ if attach_timeout <= 0:
36
+ raise ValueError("attach_timeout must be greater than zero")
37
+ self.mountpoint_id = mountpoint_id
38
+ self.state = ViewerState.NEW
39
+ self._session = session
40
+ self._request_timeout = request_timeout
41
+ self._negotiation_timeout = negotiation_timeout
42
+ self._attach_timeout = attach_timeout
43
+ self._handle_factory = handle_factory or _compat.JanusStreamingHandle.attach
44
+ self._on_event = on_event
45
+ self._handle: _compat.JanusStreamingHandle | None = None
46
+ self._state_lock = asyncio.Lock()
47
+
48
+ async def prepare(
49
+ self,
50
+ *,
51
+ pin: str | None = None,
52
+ media: Iterable[str] = (),
53
+ offer_audio: bool | None = None,
54
+ offer_video: bool | None = None,
55
+ offer_data: bool | None = None,
56
+ ) -> SessionDescription:
57
+ async with self._state_lock:
58
+ self._require_state(ViewerState.NEW)
59
+ selected = list(dict.fromkeys(media))
60
+ self._handle = await self._handle_factory(
61
+ self._session,
62
+ mountpoint_id=self.mountpoint_id,
63
+ attach_timeout=self._attach_timeout,
64
+ default_timeout=self._request_timeout,
65
+ on_event=self._on_event,
66
+ )
67
+ try:
68
+ response = await self._handle.request(
69
+ _compat.watch_request(
70
+ self.mountpoint_id,
71
+ pin=pin,
72
+ media=selected,
73
+ offer_audio=offer_audio,
74
+ offer_video=offer_video,
75
+ offer_data=offer_data,
76
+ ),
77
+ operation="watch",
78
+ timeout_seconds=self._negotiation_timeout,
79
+ )
80
+ offer = _compat.response_jsep(response)
81
+ if offer.type != "offer":
82
+ raise StreamingProtocolError("watch response JSEP was not an offer")
83
+ self.state = ViewerState.OFFERED
84
+ return offer
85
+ except BaseException:
86
+ self.state = ViewerState.FAILED
87
+ if self._handle is not None:
88
+ with suppress(Exception):
89
+ await asyncio.shield(self._handle.close())
90
+ self._handle = None
91
+ raise
92
+
93
+ async def accept_answer(self, answer: SessionDescription) -> None:
94
+ if answer.type != "answer":
95
+ raise ValueError("expected an SDP answer")
96
+ async with self._state_lock:
97
+ self._require_state(ViewerState.OFFERED)
98
+ await self._require_handle().request(
99
+ _compat.start_request(),
100
+ operation="start",
101
+ jsep=_compat.answer_jsep(answer),
102
+ timeout_seconds=self._negotiation_timeout,
103
+ )
104
+ self.state = ViewerState.ACTIVE
105
+
106
+ async def add_ice_candidate(self, candidate: IceCandidate) -> None:
107
+ self._require_state(ViewerState.OFFERED, ViewerState.ACTIVE, ViewerState.PAUSED)
108
+ await self._require_handle().trickle([_compat.trickle_candidate(candidate)])
109
+
110
+ async def complete_ice(self) -> None:
111
+ self._require_state(ViewerState.OFFERED, ViewerState.ACTIVE, ViewerState.PAUSED)
112
+ await self._require_handle().trickle([_compat.trickle_complete()])
113
+
114
+ async def pause(self) -> None:
115
+ async with self._state_lock:
116
+ self._require_state(ViewerState.ACTIVE)
117
+ await self._require_handle().request(_compat.pause_request(), operation="pause")
118
+ self.state = ViewerState.PAUSED
119
+
120
+ async def resume(self) -> None:
121
+ async with self._state_lock:
122
+ self._require_state(ViewerState.PAUSED)
123
+ await self._require_handle().request(_compat.start_request(), operation="resume")
124
+ self.state = ViewerState.ACTIVE
125
+
126
+ async def configure(self, streams: list[ConfigureStream]) -> None:
127
+ self._require_state(ViewerState.ACTIVE, ViewerState.PAUSED)
128
+ await self._require_handle().request(
129
+ _compat.configure_request(streams),
130
+ operation="configure",
131
+ )
132
+
133
+ async def switch(self, mountpoint_id: int) -> None:
134
+ if mountpoint_id < 0:
135
+ raise ValueError("mountpoint_id must not be negative")
136
+ self._require_state(ViewerState.ACTIVE, ViewerState.PAUSED)
137
+ await self._require_handle().request(
138
+ _compat.switch_request(mountpoint_id),
139
+ operation="switch",
140
+ )
141
+ self.mountpoint_id = mountpoint_id
142
+
143
+ async def close(self) -> None:
144
+ async with self._state_lock:
145
+ if self.state is ViewerState.CLOSED:
146
+ return
147
+ handle, self._handle = self._handle, None
148
+ previous = self.state
149
+ self.state = ViewerState.CLOSED
150
+ if handle is None:
151
+ return
152
+ if previous in {
153
+ ViewerState.OFFERED,
154
+ ViewerState.ACTIVE,
155
+ ViewerState.PAUSED,
156
+ ViewerState.FAILED,
157
+ }:
158
+ with suppress(Exception):
159
+ await handle.request(_compat.stop_request(), operation="stop", timeout_seconds=3.0)
160
+ with suppress(Exception):
161
+ await handle.close()
162
+
163
+ def _require_state(self, *states: ViewerState) -> None:
164
+ if self.state not in states:
165
+ raise InvalidViewerState(self.state, states)
166
+
167
+ def _require_handle(self) -> _compat.JanusStreamingHandle:
168
+ if self._handle is None:
169
+ raise StreamingProtocolError("viewer has no attached Janus handle")
170
+ return self._handle