livekit-api 1.1.1__tar.gz → 1.2.1__tar.gz
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.
- livekit_api-1.2.1/PKG-INFO +59 -0
- livekit_api-1.2.1/README.md +29 -0
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/__init__.py +10 -1
- livekit_api-1.2.1/livekit/api/_dial_timeout.py +51 -0
- livekit_api-1.2.1/livekit/api/_failover.py +162 -0
- livekit_api-1.2.1/livekit/api/_service.py +41 -0
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/access_token.py +23 -10
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/agent_dispatch_service.py +9 -2
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/connector_service.py +30 -3
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/egress_service.py +20 -2
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/ingress_service.py +9 -2
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/livekit_api.py +65 -12
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/room_service.py +9 -2
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/sip_service.py +81 -36
- livekit_api-1.2.1/livekit/api/twirp_client.py +291 -0
- livekit_api-1.2.1/livekit/api/version.py +1 -0
- {livekit_api-1.1.1 → livekit_api-1.2.1}/pyproject.toml +1 -1
- livekit_api-1.1.1/PKG-INFO +0 -36
- livekit_api-1.1.1/README.md +0 -6
- livekit_api-1.1.1/livekit/api/_service.py +0 -30
- livekit_api-1.1.1/livekit/api/twirp_client.py +0 -134
- livekit_api-1.1.1/livekit/api/version.py +0 -1
- {livekit_api-1.1.1 → livekit_api-1.2.1}/.gitignore +0 -0
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/py.typed +0 -0
- {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/webhook.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: livekit-api
|
|
3
|
+
Version: 1.2.1
|
|
4
|
+
Summary: Python Server API for LiveKit
|
|
5
|
+
Project-URL: Documentation, https://docs.livekit.io
|
|
6
|
+
Project-URL: Website, https://livekit.io/
|
|
7
|
+
Project-URL: Source, https://github.com/livekit/python-sdks/
|
|
8
|
+
Author-email: LiveKit <support@livekit.io>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: audio,livekit,realtime,video,webrtc
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.9.0
|
|
24
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
25
|
+
Requires-Dist: livekit-protocol<2.0.0,>=1.1.25
|
|
26
|
+
Requires-Dist: protobuf>=4
|
|
27
|
+
Requires-Dist: pyjwt>=2.0.0
|
|
28
|
+
Requires-Dist: types-protobuf>=4
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# LiveKit Server APIs
|
|
32
|
+
|
|
33
|
+
Access LiveKit server APIs and generate access tokens.
|
|
34
|
+
|
|
35
|
+
See https://docs.livekit.io/reference/server/server-apis for more information.
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Every request to the server APIs is authenticated. `LiveKitAPI` supports two modes:
|
|
40
|
+
|
|
41
|
+
- **API key & secret** — recommended for backend use. The SDK signs a short-lived token per request from your key and secret. Keep your API secret on the server; never ship it to a client.
|
|
42
|
+
- **Access token** — for client-side use where the API secret must not be exposed. Pass a pre-signed [access token](https://docs.livekit.io/home/get-started/authentication/) that already carries the grants for the operations you'll perform; the SDK sends it verbatim.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from livekit import api
|
|
46
|
+
|
|
47
|
+
# API key & secret: set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET,
|
|
48
|
+
# then construct with no arguments...
|
|
49
|
+
lkapi = api.LiveKitAPI()
|
|
50
|
+
|
|
51
|
+
# ...or pass any of them explicitly to override the corresponding env var:
|
|
52
|
+
lkapi = api.LiveKitAPI("https://my.livekit.host", api_key="api-key", api_secret="api-secret")
|
|
53
|
+
|
|
54
|
+
# Pre-signed access token (client-side): with LIVEKIT_URL set, pass just the token:
|
|
55
|
+
lkapi = api.LiveKitAPI.with_token("a-pre-signed-token")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The url and credentials fall back to the `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, and `LIVEKIT_TOKEN` environment variables. Values you pass explicitly take precedence; the environment variables are used only as a fallback for arguments you omit — an ambient `LIVEKIT_TOKEN`, for example, won't override an explicitly-provided API key and secret.
|
|
59
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# LiveKit Server APIs
|
|
2
|
+
|
|
3
|
+
Access LiveKit server APIs and generate access tokens.
|
|
4
|
+
|
|
5
|
+
See https://docs.livekit.io/reference/server/server-apis for more information.
|
|
6
|
+
|
|
7
|
+
## Authentication
|
|
8
|
+
|
|
9
|
+
Every request to the server APIs is authenticated. `LiveKitAPI` supports two modes:
|
|
10
|
+
|
|
11
|
+
- **API key & secret** — recommended for backend use. The SDK signs a short-lived token per request from your key and secret. Keep your API secret on the server; never ship it to a client.
|
|
12
|
+
- **Access token** — for client-side use where the API secret must not be exposed. Pass a pre-signed [access token](https://docs.livekit.io/home/get-started/authentication/) that already carries the grants for the operations you'll perform; the SDK sends it verbatim.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from livekit import api
|
|
16
|
+
|
|
17
|
+
# API key & secret: set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET,
|
|
18
|
+
# then construct with no arguments...
|
|
19
|
+
lkapi = api.LiveKitAPI()
|
|
20
|
+
|
|
21
|
+
# ...or pass any of them explicitly to override the corresponding env var:
|
|
22
|
+
lkapi = api.LiveKitAPI("https://my.livekit.host", api_key="api-key", api_secret="api-secret")
|
|
23
|
+
|
|
24
|
+
# Pre-signed access token (client-side): with LIVEKIT_URL set, pass just the token:
|
|
25
|
+
lkapi = api.LiveKitAPI.with_token("a-pre-signed-token")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The url and credentials fall back to the `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, and `LIVEKIT_TOKEN` environment variables. Values you pass explicitly take precedence; the environment variables are used only as a fallback for arguments you omit — an ambient `LIVEKIT_TOKEN`, for example, won't override an explicitly-provided API key and secret.
|
|
29
|
+
|
|
@@ -36,7 +36,13 @@ from livekit.protocol.sip import *
|
|
|
36
36
|
from livekit.protocol.connector_whatsapp import *
|
|
37
37
|
from livekit.protocol.connector_twilio import *
|
|
38
38
|
|
|
39
|
-
from .twirp_client import
|
|
39
|
+
from .twirp_client import (
|
|
40
|
+
ServerError,
|
|
41
|
+
ServerErrorCode,
|
|
42
|
+
SipCallError,
|
|
43
|
+
TwirpError,
|
|
44
|
+
TwirpErrorCode,
|
|
45
|
+
)
|
|
40
46
|
from .livekit_api import LiveKitAPI
|
|
41
47
|
from .access_token import (
|
|
42
48
|
InferenceGrants,
|
|
@@ -64,6 +70,9 @@ __all__ = [
|
|
|
64
70
|
"AccessToken",
|
|
65
71
|
"TokenVerifier",
|
|
66
72
|
"WebhookReceiver",
|
|
73
|
+
"ServerError",
|
|
74
|
+
"ServerErrorCode",
|
|
67
75
|
"TwirpError",
|
|
68
76
|
"TwirpErrorCode",
|
|
77
|
+
"SipCallError",
|
|
69
78
|
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
from livekit.protocol.sip import CreateSIPParticipantRequest, TransferSIPParticipantRequest
|
|
6
|
+
|
|
7
|
+
# Requests that carry ringing_timeout and share the phone-dialing timeout behavior.
|
|
8
|
+
DialRequest = Union[
|
|
9
|
+
CreateSIPParticipantRequest,
|
|
10
|
+
TransferSIPParticipantRequest,
|
|
11
|
+
]
|
|
12
|
+
"""@private"""
|
|
13
|
+
|
|
14
|
+
# Ring window (seconds) assumed when a request doesn't set ringing_timeout;
|
|
15
|
+
# matches the server default. A dialing request must outlast it.
|
|
16
|
+
DEFAULT_RINGING_TIMEOUT = 30.0
|
|
17
|
+
"""@private"""
|
|
18
|
+
|
|
19
|
+
# A dialing request must outlast the ringing window, or it would abort before
|
|
20
|
+
# the call can be answered. Keep the request timeout at least this many seconds
|
|
21
|
+
# above the ringing timeout.
|
|
22
|
+
RINGING_TIMEOUT_MARGIN = 2.0
|
|
23
|
+
"""@private"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pin_ringing_timeout(request: DialRequest) -> None:
|
|
27
|
+
"""Set the ring window explicitly on a dialing request when the caller left it
|
|
28
|
+
unset, so the derived request timeout doesn't depend on the server's default
|
|
29
|
+
(which could change out from under us).
|
|
30
|
+
|
|
31
|
+
@private
|
|
32
|
+
"""
|
|
33
|
+
if not request.HasField("ringing_timeout"):
|
|
34
|
+
request.ringing_timeout.seconds = int(DEFAULT_RINGING_TIMEOUT)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dial_timeout(user_timeout: Optional[float], request: DialRequest) -> float:
|
|
38
|
+
"""Request timeout (seconds) for a phone-dialing call: the ring window plus a
|
|
39
|
+
margin, so the request doesn't abort before the call can be answered. The
|
|
40
|
+
ring window is the request's ringing_timeout when set, else
|
|
41
|
+
DEFAULT_RINGING_TIMEOUT. A longer user_timeout is honored; a shorter one is
|
|
42
|
+
raised to the floor.
|
|
43
|
+
|
|
44
|
+
@private
|
|
45
|
+
"""
|
|
46
|
+
if request.HasField("ringing_timeout"):
|
|
47
|
+
ring: float = request.ringing_timeout.seconds
|
|
48
|
+
else:
|
|
49
|
+
ring = DEFAULT_RINGING_TIMEOUT
|
|
50
|
+
floor = ring + RINGING_TIMEOUT_MARGIN
|
|
51
|
+
return max(user_timeout if user_timeout else floor, floor)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright 2026 LiveKit, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Region failover for the Twirp API clients.
|
|
16
|
+
|
|
17
|
+
On a retryable failure (any transport error or HTTP 5xx) the client discovers
|
|
18
|
+
alternative LiveKit Cloud regions via ``/settings/regions`` and replays the
|
|
19
|
+
request against the next region, with exponential backoff. 4xx responses are
|
|
20
|
+
returned immediately.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Dict, List, Optional
|
|
28
|
+
from urllib.parse import urlparse
|
|
29
|
+
|
|
30
|
+
import aiohttp
|
|
31
|
+
|
|
32
|
+
FAILOVER_MAX_ATTEMPTS = 3
|
|
33
|
+
FAILOVER_BACKOFF_BASE = 0.2 # seconds
|
|
34
|
+
# Below this per-request timeout (seconds) a retry is unlikely to help and many
|
|
35
|
+
# clients would retry in lockstep across regions, so a short request gets a
|
|
36
|
+
# single attempt (thundering-herd guard).
|
|
37
|
+
MIN_FAILOVER_TIMEOUT = 5.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def failover_attempts(
|
|
41
|
+
enabled: bool,
|
|
42
|
+
host: Optional[str],
|
|
43
|
+
force: bool = False,
|
|
44
|
+
timeout: Optional[float] = None,
|
|
45
|
+
) -> int:
|
|
46
|
+
"""Total request attempts for a host; 1 means no failover. Failover only
|
|
47
|
+
engages when enabled, the host is a LiveKit Cloud domain, and the request
|
|
48
|
+
timeout is long enough to retry. ``force`` bypasses the cloud-host check and
|
|
49
|
+
is for internal testing only.
|
|
50
|
+
"""
|
|
51
|
+
if not (enabled and (force or (host is not None and is_cloud(host)))):
|
|
52
|
+
return 1
|
|
53
|
+
if timeout is not None and 0 < timeout < MIN_FAILOVER_TIMEOUT:
|
|
54
|
+
return 1
|
|
55
|
+
return FAILOVER_MAX_ATTEMPTS
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_cloud(host: str) -> bool:
|
|
59
|
+
# Failover only engages for LiveKit Cloud project domains.
|
|
60
|
+
return host.endswith(".livekit.cloud")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def to_http(url: str) -> str:
|
|
64
|
+
"""Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https)."""
|
|
65
|
+
if url.startswith("ws"):
|
|
66
|
+
return "http" + url[2:]
|
|
67
|
+
return url
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def origin_of(url: str) -> str:
|
|
71
|
+
"""Returns the scheme://host[:port] origin of a URL, dropping any path."""
|
|
72
|
+
parsed = urlparse(url)
|
|
73
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def host_key(url: str) -> str:
|
|
77
|
+
"""A stable key identifying a host (including port) for dedup across attempts."""
|
|
78
|
+
return urlparse(url).netloc.lower()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def pick_next(region_origins: List[str], attempted: set[str]) -> Optional[str]:
|
|
82
|
+
"""Returns the first region origin whose host has not yet been attempted."""
|
|
83
|
+
for origin in region_origins:
|
|
84
|
+
if host_key(origin) not in attempted:
|
|
85
|
+
return origin
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class _CacheEntry:
|
|
91
|
+
origins: List[str]
|
|
92
|
+
fetched_at: float
|
|
93
|
+
ttl: float
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RegionCache:
|
|
97
|
+
"""Process-wide cache of the LiveKit Cloud region list, keyed by host."""
|
|
98
|
+
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
self._entries: Dict[str, _CacheEntry] = {}
|
|
101
|
+
|
|
102
|
+
async def region_origins(
|
|
103
|
+
self,
|
|
104
|
+
session: aiohttp.ClientSession,
|
|
105
|
+
origin: str,
|
|
106
|
+
headers: Dict[str, str],
|
|
107
|
+
) -> List[str]:
|
|
108
|
+
"""Returns alternative region origins for ``origin``, fetching
|
|
109
|
+
``/settings/regions`` if the cache is stale. Best-effort: on a fetch
|
|
110
|
+
failure it serves a stale cached list when available, otherwise an empty
|
|
111
|
+
list. Forwards ``headers`` so a valid token — and any test directives —
|
|
112
|
+
reach the discovery endpoint."""
|
|
113
|
+
key = host_key(origin)
|
|
114
|
+
entry = self._entries.get(key)
|
|
115
|
+
if entry is not None and (time.monotonic() - entry.fetched_at) < entry.ttl:
|
|
116
|
+
return entry.origins
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
origins, ttl = await self._fetch(session, origin, headers)
|
|
120
|
+
except Exception:
|
|
121
|
+
return entry.origins if entry is not None else []
|
|
122
|
+
|
|
123
|
+
# A zero TTL (e.g. Cache-Control: max-age=0) means "do not cache".
|
|
124
|
+
if ttl > 0:
|
|
125
|
+
self._entries[key] = _CacheEntry(origins, time.monotonic(), ttl)
|
|
126
|
+
return origins
|
|
127
|
+
|
|
128
|
+
async def _fetch(
|
|
129
|
+
self,
|
|
130
|
+
session: aiohttp.ClientSession,
|
|
131
|
+
origin: str,
|
|
132
|
+
headers: Dict[str, str],
|
|
133
|
+
) -> tuple[List[str], float]:
|
|
134
|
+
fetch_headers = {
|
|
135
|
+
k: v for k, v in headers.items() if k.lower() not in ("content-type", "content-length")
|
|
136
|
+
}
|
|
137
|
+
# Short timeout so a slow/unreachable discovery endpoint doesn't stall
|
|
138
|
+
# the failover path.
|
|
139
|
+
async with session.get(
|
|
140
|
+
f"{origin}/settings/regions",
|
|
141
|
+
headers=fetch_headers,
|
|
142
|
+
timeout=aiohttp.ClientTimeout(total=2),
|
|
143
|
+
) as resp:
|
|
144
|
+
if resp.status != 200:
|
|
145
|
+
raise RuntimeError(f"region discovery failed: {resp.status}")
|
|
146
|
+
ttl = _parse_max_age(resp.headers.get("Cache-Control"))
|
|
147
|
+
body = await resp.json()
|
|
148
|
+
origins = [origin_of(to_http(r["url"])) for r in body.get("regions", []) if r.get("url")]
|
|
149
|
+
return origins, ttl
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _parse_max_age(cache_control: Optional[str]) -> float:
|
|
153
|
+
if not cache_control:
|
|
154
|
+
return 0.0
|
|
155
|
+
for directive in cache_control.split(","):
|
|
156
|
+
directive = directive.strip().lower()
|
|
157
|
+
if directive.startswith("max-age="):
|
|
158
|
+
try:
|
|
159
|
+
return float(int(directive[len("max-age=") :]))
|
|
160
|
+
except ValueError:
|
|
161
|
+
return 0.0
|
|
162
|
+
return 0.0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from .twirp_client import TwirpClient
|
|
6
|
+
from .access_token import AccessToken, VideoGrants, SIPGrants
|
|
7
|
+
|
|
8
|
+
AUTHORIZATION = "authorization"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Service(ABC):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
session: aiohttp.ClientSession,
|
|
15
|
+
host: str,
|
|
16
|
+
api_key: str,
|
|
17
|
+
api_secret: str,
|
|
18
|
+
failover: bool = True,
|
|
19
|
+
):
|
|
20
|
+
self._client = TwirpClient(session, host, "livekit", failover=failover)
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self.api_secret = api_secret
|
|
23
|
+
# A pre-signed token set by LiveKitAPI for token auth; sent verbatim,
|
|
24
|
+
# skipping per-call signing. Per-service constructors stay key/secret-only.
|
|
25
|
+
self._token: str | None = None
|
|
26
|
+
|
|
27
|
+
def _auth_header(
|
|
28
|
+
self, grants: VideoGrants | None, sip: SIPGrants | None = None
|
|
29
|
+
) -> dict[str, str]:
|
|
30
|
+
# A pre-signed token is sent verbatim; the caller is responsible for its grants.
|
|
31
|
+
if self._token:
|
|
32
|
+
return {AUTHORIZATION: "Bearer {}".format(self._token)}
|
|
33
|
+
|
|
34
|
+
tok = AccessToken(self.api_key, self.api_secret)
|
|
35
|
+
if grants:
|
|
36
|
+
tok.with_grants(grants)
|
|
37
|
+
if sip is not None:
|
|
38
|
+
tok.with_sip_grants(sip)
|
|
39
|
+
|
|
40
|
+
token = tok.to_jwt()
|
|
41
|
+
return {AUTHORIZATION: "Bearer {}".format(token)}
|
|
@@ -233,14 +233,26 @@ class TokenVerifier:
|
|
|
233
233
|
if verify_signature and (not self.api_key or not self.api_secret):
|
|
234
234
|
raise ValueError("api_key and api_secret must be set")
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
236
|
+
# First-party minters always set exp. Without this, a hand-rolled token
|
|
237
|
+
# with a valid signature and no exp verifies forever (livekit/protocol#1706).
|
|
238
|
+
if verify_signature:
|
|
239
|
+
claims = jwt.decode(
|
|
240
|
+
token,
|
|
241
|
+
key=self.api_secret or "",
|
|
242
|
+
issuer=self.api_key or "",
|
|
243
|
+
algorithms=["HS256"],
|
|
244
|
+
leeway=self._leeway.total_seconds(),
|
|
245
|
+
options={"verify_signature": True, "require": ["exp"]},
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
claims = jwt.decode(
|
|
249
|
+
token,
|
|
250
|
+
key=self.api_secret or "",
|
|
251
|
+
issuer=self.api_key or "",
|
|
252
|
+
algorithms=["HS256"],
|
|
253
|
+
leeway=self._leeway.total_seconds(),
|
|
254
|
+
options={"verify_signature": False},
|
|
255
|
+
)
|
|
244
256
|
video_dict = claims.get("video", dict())
|
|
245
257
|
video_dict = {camel_to_snake(k): v for k, v in video_dict.items()}
|
|
246
258
|
video_dict = {k: v for k, v in video_dict.items() if k in VideoGrants.__dataclass_fields__}
|
|
@@ -271,9 +283,10 @@ class TokenVerifier:
|
|
|
271
283
|
|
|
272
284
|
if claims.get("roomPreset"):
|
|
273
285
|
grant_claims.room_preset = claims.get("roomPreset")
|
|
274
|
-
|
|
286
|
+
room_config = claims.get("roomConfig")
|
|
287
|
+
if room_config:
|
|
275
288
|
grant_claims.room_config = ParseDict(
|
|
276
|
-
|
|
289
|
+
room_config,
|
|
277
290
|
RoomConfiguration(),
|
|
278
291
|
ignore_unknown_fields=True,
|
|
279
292
|
)
|
|
@@ -26,8 +26,15 @@ class AgentDispatchService(Service):
|
|
|
26
26
|
```
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(
|
|
30
|
-
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
session: aiohttp.ClientSession,
|
|
32
|
+
url: str,
|
|
33
|
+
api_key: str,
|
|
34
|
+
api_secret: str,
|
|
35
|
+
failover: bool = True,
|
|
36
|
+
):
|
|
37
|
+
super().__init__(session, url, api_key, api_secret, failover=failover)
|
|
31
38
|
|
|
32
39
|
async def create_dispatch(self, req: CreateAgentDispatchRequest) -> AgentDispatch:
|
|
33
40
|
"""Create an explicit dispatch for an agent to join a room.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import aiohttp
|
|
4
|
+
from typing import Optional
|
|
4
5
|
|
|
5
6
|
from livekit.protocol.connector_whatsapp import (
|
|
6
7
|
DialWhatsAppCallRequest,
|
|
@@ -17,6 +18,7 @@ from livekit.protocol.connector_twilio import (
|
|
|
17
18
|
ConnectTwilioCallResponse,
|
|
18
19
|
)
|
|
19
20
|
from ._service import Service
|
|
21
|
+
from ._dial_timeout import DEFAULT_RINGING_TIMEOUT
|
|
20
22
|
from .access_token import VideoGrants
|
|
21
23
|
|
|
22
24
|
SVC = "Connector"
|
|
@@ -35,8 +37,15 @@ class ConnectorService(Service):
|
|
|
35
37
|
```
|
|
36
38
|
"""
|
|
37
39
|
|
|
38
|
-
def __init__(
|
|
39
|
-
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
session: aiohttp.ClientSession,
|
|
43
|
+
url: str,
|
|
44
|
+
api_key: str,
|
|
45
|
+
api_secret: str,
|
|
46
|
+
failover: bool = True,
|
|
47
|
+
):
|
|
48
|
+
super().__init__(session, url, api_key, api_secret, failover=failover)
|
|
40
49
|
|
|
41
50
|
async def dial_whatsapp_call(
|
|
42
51
|
self, request: DialWhatsAppCallRequest
|
|
@@ -99,23 +108,41 @@ class ConnectorService(Service):
|
|
|
99
108
|
)
|
|
100
109
|
|
|
101
110
|
async def accept_whatsapp_call(
|
|
102
|
-
self,
|
|
111
|
+
self,
|
|
112
|
+
request: AcceptWhatsAppCallRequest,
|
|
113
|
+
*,
|
|
114
|
+
timeout: Optional[float] = None,
|
|
103
115
|
) -> AcceptWhatsAppCallResponse:
|
|
104
116
|
"""
|
|
105
117
|
Accept an inbound WhatsApp call
|
|
106
118
|
|
|
107
119
|
Args:
|
|
108
120
|
request: AcceptWhatsAppCallRequest containing call parameters and SDP
|
|
121
|
+
timeout: Optional request timeout in seconds. When the request waits
|
|
122
|
+
for the inbound party to join (wait_until_answered), it defaults
|
|
123
|
+
to the standard ring window.
|
|
109
124
|
|
|
110
125
|
Returns:
|
|
111
126
|
AcceptWhatsAppCallResponse with the room name
|
|
112
127
|
"""
|
|
128
|
+
client_timeout: Optional[aiohttp.ClientTimeout] = None
|
|
129
|
+
if request.wait_until_answered:
|
|
130
|
+
# Waiting for the inbound party to join can block, so default the
|
|
131
|
+
# request timeout to the standard ring window; the caller overrides
|
|
132
|
+
# via timeout.
|
|
133
|
+
client_timeout = aiohttp.ClientTimeout(
|
|
134
|
+
total=timeout if timeout else DEFAULT_RINGING_TIMEOUT
|
|
135
|
+
)
|
|
136
|
+
elif timeout:
|
|
137
|
+
client_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
138
|
+
|
|
113
139
|
return await self._client.request(
|
|
114
140
|
SVC,
|
|
115
141
|
"AcceptWhatsAppCall",
|
|
116
142
|
request,
|
|
117
143
|
self._auth_header(VideoGrants(room_create=True)),
|
|
118
144
|
AcceptWhatsAppCallResponse,
|
|
145
|
+
timeout=client_timeout,
|
|
119
146
|
)
|
|
120
147
|
|
|
121
148
|
async def connect_twilio_call(
|
|
@@ -9,6 +9,7 @@ from livekit.protocol.egress import (
|
|
|
9
9
|
UpdateStreamRequest,
|
|
10
10
|
ListEgressRequest,
|
|
11
11
|
StopEgressRequest,
|
|
12
|
+
StartEgressRequest,
|
|
12
13
|
EgressInfo,
|
|
13
14
|
ListEgressResponse,
|
|
14
15
|
)
|
|
@@ -33,8 +34,15 @@ class EgressService(Service):
|
|
|
33
34
|
Also see https://docs.livekit.io/home/egress/overview/
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
|
-
def __init__(
|
|
37
|
-
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
session: aiohttp.ClientSession,
|
|
40
|
+
url: str,
|
|
41
|
+
api_key: str,
|
|
42
|
+
api_secret: str,
|
|
43
|
+
failover: bool = True,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(session, url, api_key, api_secret, failover=failover)
|
|
38
46
|
|
|
39
47
|
async def start_room_composite_egress(self, start: RoomCompositeEgressRequest) -> EgressInfo:
|
|
40
48
|
"""Starts a composite recording of a room."""
|
|
@@ -86,6 +94,16 @@ class EgressService(Service):
|
|
|
86
94
|
EgressInfo,
|
|
87
95
|
)
|
|
88
96
|
|
|
97
|
+
async def start_egress(self, start: StartEgressRequest) -> EgressInfo:
|
|
98
|
+
"""Starts an egress; the request's source and outputs determine the egress type."""
|
|
99
|
+
return await self._client.request(
|
|
100
|
+
SVC,
|
|
101
|
+
"StartEgress",
|
|
102
|
+
start,
|
|
103
|
+
self._auth_header(VideoGrants(room_record=True)),
|
|
104
|
+
EgressInfo,
|
|
105
|
+
)
|
|
106
|
+
|
|
89
107
|
async def update_layout(self, update: UpdateLayoutRequest) -> EgressInfo:
|
|
90
108
|
"""Updates the layout of a composite recording."""
|
|
91
109
|
return await self._client.request(
|
|
@@ -28,8 +28,15 @@ class IngressService(Service):
|
|
|
28
28
|
Also see https://docs.livekit.io/home/ingress/overview/
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
def __init__(
|
|
32
|
-
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
session: aiohttp.ClientSession,
|
|
34
|
+
url: str,
|
|
35
|
+
api_key: str,
|
|
36
|
+
api_secret: str,
|
|
37
|
+
failover: bool = True,
|
|
38
|
+
):
|
|
39
|
+
super().__init__(session, url, api_key, api_secret, failover=failover)
|
|
33
40
|
|
|
34
41
|
async def create_ingress(self, create: CreateIngressRequest) -> IngressInfo:
|
|
35
42
|
return await self._client.request(
|