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.
Files changed (25) hide show
  1. livekit_api-1.2.1/PKG-INFO +59 -0
  2. livekit_api-1.2.1/README.md +29 -0
  3. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/__init__.py +10 -1
  4. livekit_api-1.2.1/livekit/api/_dial_timeout.py +51 -0
  5. livekit_api-1.2.1/livekit/api/_failover.py +162 -0
  6. livekit_api-1.2.1/livekit/api/_service.py +41 -0
  7. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/access_token.py +23 -10
  8. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/agent_dispatch_service.py +9 -2
  9. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/connector_service.py +30 -3
  10. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/egress_service.py +20 -2
  11. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/ingress_service.py +9 -2
  12. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/livekit_api.py +65 -12
  13. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/room_service.py +9 -2
  14. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/sip_service.py +81 -36
  15. livekit_api-1.2.1/livekit/api/twirp_client.py +291 -0
  16. livekit_api-1.2.1/livekit/api/version.py +1 -0
  17. {livekit_api-1.1.1 → livekit_api-1.2.1}/pyproject.toml +1 -1
  18. livekit_api-1.1.1/PKG-INFO +0 -36
  19. livekit_api-1.1.1/README.md +0 -6
  20. livekit_api-1.1.1/livekit/api/_service.py +0 -30
  21. livekit_api-1.1.1/livekit/api/twirp_client.py +0 -134
  22. livekit_api-1.1.1/livekit/api/version.py +0 -1
  23. {livekit_api-1.1.1 → livekit_api-1.2.1}/.gitignore +0 -0
  24. {livekit_api-1.1.1 → livekit_api-1.2.1}/livekit/api/py.typed +0 -0
  25. {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 TwirpError, TwirpErrorCode
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
- claims = jwt.decode(
237
- token,
238
- key=self.api_secret or "",
239
- issuer=self.api_key or "",
240
- algorithms=["HS256"],
241
- leeway=self._leeway.total_seconds(),
242
- options={"verify_signature": verify_signature},
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
- if claims.get("roomConfig"):
286
+ room_config = claims.get("roomConfig")
287
+ if room_config:
275
288
  grant_claims.room_config = ParseDict(
276
- claims.get("roomConfig"),
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__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
30
- super().__init__(session, url, api_key, api_secret)
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__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
39
- super().__init__(session, url, api_key, api_secret)
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, request: AcceptWhatsAppCallRequest
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__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
37
- super().__init__(session, url, api_key, api_secret)
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__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
32
- super().__init__(session, url, api_key, api_secret)
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(