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,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
|