PyAgoraRTC 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.
- pyagorartc/__init__.py +67 -0
- pyagorartc/ap/__init__.py +17 -0
- pyagorartc/ap/client.py +244 -0
- pyagorartc/ap/password.py +14 -0
- pyagorartc/ap/response.py +329 -0
- pyagorartc/const.py +55 -0
- pyagorartc/exceptions.py +61 -0
- pyagorartc/models.py +223 -0
- pyagorartc/py.typed +0 -0
- pyagorartc/rtm/__init__.py +5 -0
- pyagorartc/rtm/client.py +170 -0
- pyagorartc/sdp/__init__.py +26 -0
- pyagorartc/sdp/answer.py +320 -0
- pyagorartc/sdp/candidates.py +103 -0
- pyagorartc/sdp/offer.py +260 -0
- pyagorartc/session/__init__.py +87 -0
- pyagorartc/session/messages.py +570 -0
- pyagorartc/session/recovery.py +166 -0
- pyagorartc/session/session.py +696 -0
- pyagorartc/session/transport.py +130 -0
- pyagorartc-0.2.0.dist-info/METADATA +151 -0
- pyagorartc-0.2.0.dist-info/RECORD +24 -0
- pyagorartc-0.2.0.dist-info/WHEEL +4 -0
- pyagorartc-0.2.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Timing policies the session runtime applies: peer recovery, keep-alive cadence and renew debounce.
|
|
2
|
+
|
|
3
|
+
Pure: each policy reads an injected monotonic clock (or a ``now`` the caller passes) and owns no task;
|
|
4
|
+
the runtime owns every loop and timer (D13).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from pyagorartc.const import (
|
|
14
|
+
KEEPALIVE_INTERVAL_S,
|
|
15
|
+
PEER_RECOVER_COOLDOWN_S,
|
|
16
|
+
PEER_RECOVER_MAX_ATTEMPTS,
|
|
17
|
+
PEER_RECOVER_RESET_S,
|
|
18
|
+
PEER_REJOIN_DEBOUNCE_S,
|
|
19
|
+
RENEW_TOKEN_DEBOUNCE_S,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
_LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PeerRecovery:
|
|
29
|
+
"""When a publisher that left the channel should be recovered (D14; HA-Luba ``_peer_recovery``).
|
|
30
|
+
|
|
31
|
+
The runtime, on ``on_user_offline`` for a uid other than its own and only when ``on_peer_left``
|
|
32
|
+
is set: cancels any pending recovery timer (only the latest peer-left counts), then calls
|
|
33
|
+
``peer_left``; if that returns a delay, it sleeps that long and calls ``should_recover`` with
|
|
34
|
+
whether the uid is back online, invoking ``on_peer_left(uid)`` when it returns True. It skips
|
|
35
|
+
all of this once the session is no longer joined, and calls ``reset`` after a successful join.
|
|
36
|
+
|
|
37
|
+
One budget covers every uid, as shipped: at most ``max_attempts`` recoveries, ``cooldown_s``
|
|
38
|
+
apart; a stream that runs longer than ``reset_after_s`` since the last recovery earns a fresh budget.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
debounce_s: float = PEER_REJOIN_DEBOUNCE_S,
|
|
45
|
+
cooldown_s: float = PEER_RECOVER_COOLDOWN_S,
|
|
46
|
+
max_attempts: int = PEER_RECOVER_MAX_ATTEMPTS,
|
|
47
|
+
reset_after_s: float = PEER_RECOVER_RESET_S,
|
|
48
|
+
clock: Callable[[], float],
|
|
49
|
+
) -> None:
|
|
50
|
+
self.debounce_s = debounce_s
|
|
51
|
+
self.cooldown_s = cooldown_s
|
|
52
|
+
self.max_attempts = max_attempts
|
|
53
|
+
self.reset_after_s = reset_after_s
|
|
54
|
+
self._clock = clock
|
|
55
|
+
self._attempts = 0
|
|
56
|
+
self._last_recovered_at: float | None = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def attempts(self) -> int:
|
|
60
|
+
"""Recoveries counted against the current budget."""
|
|
61
|
+
return self._attempts
|
|
62
|
+
|
|
63
|
+
def peer_left(self, uid: int, now: float | None = None) -> float | None:
|
|
64
|
+
"""Seconds to wait for ``uid`` to rejoin, or ``None`` when no recovery could follow.
|
|
65
|
+
|
|
66
|
+
``None`` means ``should_recover`` would refuse at the end of the debounce (attempt cap or
|
|
67
|
+
cooldown), so the runtime need not arm a timer. Records nothing.
|
|
68
|
+
"""
|
|
69
|
+
due = (self._clock() if now is None else now) + self.debounce_s
|
|
70
|
+
if (reason := self._refusal(due)) is not None:
|
|
71
|
+
_LOGGER.debug("Peer %s left; no recovery can follow (%s)", uid, reason)
|
|
72
|
+
return None
|
|
73
|
+
return self.debounce_s
|
|
74
|
+
|
|
75
|
+
def should_recover(self, uid: int, now: float | None = None, *, peer_present: bool) -> bool:
|
|
76
|
+
"""Whether to recover ``uid`` now, after the debounce; records the attempt when True."""
|
|
77
|
+
if peer_present:
|
|
78
|
+
return False
|
|
79
|
+
now = self._clock() if now is None else now
|
|
80
|
+
self._apply_reset(now)
|
|
81
|
+
if (reason := self._refusal(now)) is not None:
|
|
82
|
+
if self._attempts >= self.max_attempts:
|
|
83
|
+
_LOGGER.warning(
|
|
84
|
+
"Peer %s left the channel %s times without the stream settling; not recovering it again",
|
|
85
|
+
uid,
|
|
86
|
+
self._attempts,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
_LOGGER.debug("Peer %s still gone; recovery skipped (%s)", uid, reason)
|
|
90
|
+
return False
|
|
91
|
+
self._last_recovered_at = now
|
|
92
|
+
self._attempts += 1
|
|
93
|
+
_LOGGER.debug("Recovering peer %s (attempt %s/%s)", uid, self._attempts, self.max_attempts)
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def reset(self) -> None:
|
|
97
|
+
"""Forget every attempt and the cooldown (a fresh session)."""
|
|
98
|
+
self._attempts = 0
|
|
99
|
+
self._last_recovered_at = None
|
|
100
|
+
|
|
101
|
+
def _apply_reset(self, now: float) -> None:
|
|
102
|
+
if self._attempts and self._since_last(now) > self.reset_after_s:
|
|
103
|
+
self._attempts = 0
|
|
104
|
+
|
|
105
|
+
def _refusal(self, now: float) -> str | None:
|
|
106
|
+
attempts = self._attempts
|
|
107
|
+
if attempts and self._since_last(now) > self.reset_after_s:
|
|
108
|
+
attempts = 0
|
|
109
|
+
if attempts >= self.max_attempts:
|
|
110
|
+
return "attempt cap reached"
|
|
111
|
+
if self._since_last(now) < self.cooldown_s:
|
|
112
|
+
return "inside cooldown"
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def _since_last(self, now: float) -> float:
|
|
116
|
+
return float("inf") if self._last_recovered_at is None else now - self._last_recovered_at
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class Keepalive:
|
|
121
|
+
"""Cadence and deadline for the host's periodic keep-alive callback (D15).
|
|
122
|
+
|
|
123
|
+
``deadline`` is absolute monotonic seconds; ``None`` means none. The runtime's loop: close with
|
|
124
|
+
``CloseReason.DEADLINE`` once ``deadline_reached``; otherwise run the callback (stopping the
|
|
125
|
+
callback, not the deadline, when it returns False) and sleep ``delay``.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
interval_s: float = KEEPALIVE_INTERVAL_S
|
|
129
|
+
deadline: float | None = None
|
|
130
|
+
|
|
131
|
+
def next_due(self, now: float) -> float:
|
|
132
|
+
"""The monotonic instant of the next tick: one interval on, but never past the deadline."""
|
|
133
|
+
due = now + self.interval_s
|
|
134
|
+
return due if self.deadline is None else min(due, self.deadline)
|
|
135
|
+
|
|
136
|
+
def delay(self, now: float) -> float:
|
|
137
|
+
"""Seconds from ``now`` until ``next_due``; 0 once the deadline has passed."""
|
|
138
|
+
return max(0.0, self.next_due(now) - now)
|
|
139
|
+
|
|
140
|
+
def deadline_reached(self, now: float) -> bool:
|
|
141
|
+
"""Whether the session's budget is spent."""
|
|
142
|
+
return self.deadline is not None and now >= self.deadline
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RenewDebounce:
|
|
146
|
+
"""At most one ``renew_token`` per window (D8; ``will_expire`` repeats about once a second).
|
|
147
|
+
|
|
148
|
+
``clear`` on ``on_token_privilege_did_expire``, or when a renew failed to send, so the next goes out.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, *, window_s: float = RENEW_TOKEN_DEBOUNCE_S, clock: Callable[[], float]) -> None:
|
|
152
|
+
self.window_s = window_s
|
|
153
|
+
self._clock = clock
|
|
154
|
+
self._last_sent_at: float | None = None
|
|
155
|
+
|
|
156
|
+
def should_send(self, now: float | None = None) -> bool:
|
|
157
|
+
"""Whether to send a renew now; records the send when True."""
|
|
158
|
+
now = self._clock() if now is None else now
|
|
159
|
+
if self._last_sent_at is not None and now - self._last_sent_at < self.window_s:
|
|
160
|
+
return False
|
|
161
|
+
self._last_sent_at = now
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def clear(self) -> None:
|
|
165
|
+
"""Let the next renew through regardless of the window."""
|
|
166
|
+
self._last_sent_at = None
|