From 67618506d3ed5ba4263f23299bbc4d7f1a866df3 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Tue, 30 Jun 2026 17:03:20 +0200 Subject: [PATCH 1/8] improve timeout behavior standard API times out in 10s, and SIP dial APIs defaults to 30 we were not using a timeout correctly, which could affect retry behavior --- livekit-api/livekit/api/_failover.py | 28 ++++++++++++++++++------- livekit-api/livekit/api/livekit_api.py | 4 ++-- livekit-api/livekit/api/sip_service.py | 18 +++++++++------- livekit-api/livekit/api/twirp_client.py | 9 +++++++- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/livekit-api/livekit/api/_failover.py b/livekit-api/livekit/api/_failover.py index 49d239ed..4babb74c 100644 --- a/livekit-api/livekit/api/_failover.py +++ b/livekit-api/livekit/api/_failover.py @@ -31,16 +31,28 @@ FAILOVER_MAX_ATTEMPTS = 3 FAILOVER_BACKOFF_BASE = 0.2 # seconds - - -def failover_attempts(enabled: bool, host: Optional[str], force: bool = False) -> int: +# Below this per-request timeout (seconds) a retry is unlikely to help and many +# clients would retry in lockstep across regions, so a short request gets a +# single attempt (thundering-herd guard). +MIN_FAILOVER_TIMEOUT = 5.0 + + +def failover_attempts( + enabled: bool, + host: Optional[str], + force: bool = False, + timeout: Optional[float] = None, +) -> int: """Total request attempts for a host; 1 means no failover. Failover only - engages when enabled and the host is a LiveKit Cloud domain. ``force`` - bypasses the cloud-host check and is for internal testing only. + engages when enabled, the host is a LiveKit Cloud domain, and the request + timeout is long enough to retry. ``force`` bypasses the cloud-host check and + is for internal testing only. """ - if enabled and (force or (host is not None and is_cloud(host))): - return FAILOVER_MAX_ATTEMPTS - return 1 + if not (enabled and (force or (host is not None and is_cloud(host)))): + return 1 + if timeout is not None and 0 < timeout < MIN_FAILOVER_TIMEOUT: + return 1 + return FAILOVER_MAX_ATTEMPTS def is_cloud(host: str) -> bool: diff --git a/livekit-api/livekit/api/livekit_api.py b/livekit-api/livekit/api/livekit_api.py index bc0fcfaf..5973a918 100644 --- a/livekit-api/livekit/api/livekit_api.py +++ b/livekit-api/livekit/api/livekit_api.py @@ -39,7 +39,7 @@ def __init__( url: LiveKit server URL (read from `LIVEKIT_URL` environment variable if not provided) api_key: API key (read from `LIVEKIT_API_KEY` environment variable if not provided) api_secret: API secret (read from `LIVEKIT_API_SECRET` environment variable if not provided) - timeout: Request timeout (default: 60 seconds) + timeout: Request timeout (default: 10 seconds) session: aiohttp.ClientSession instance to use for requests, if not provided, a new one will be created """ url = url or os.getenv("LIVEKIT_URL") @@ -57,7 +57,7 @@ def __init__( if not self._session: self._custom_session = False if not timeout: - timeout = aiohttp.ClientTimeout(total=60) + timeout = aiohttp.ClientTimeout(total=10) self._session = aiohttp.ClientSession(timeout=timeout) self._room = RoomService(self._session, url, api_key, api_secret, failover) diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index 36d215f3..eb6bc7d8 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -38,6 +38,10 @@ from .access_token import VideoGrants, SIPGrants SVC = "SIP" + +# Calls that dial a phone (CreateSIPParticipant with wait_until_answered, +# TransferSIPParticipant) take longer than a normal request. +SIP_DIAL_TIMEOUT = 30.0 """@private""" @@ -782,16 +786,12 @@ async def create_sip_participant( """ client_timeout: Optional[aiohttp.ClientTimeout] = None if timeout: - # obay user specified timeout + # obey user specified timeout client_timeout = aiohttp.ClientTimeout(total=timeout) elif create.wait_until_answered: - # ensure default timeout isn't too short when using sync mode - if ( - self._client._session.timeout - and self._client._session.timeout.total - and self._client._session.timeout.total < 20 - ): - client_timeout = aiohttp.ClientTimeout(total=20) + # Dialing a phone and waiting for an answer takes longer than a + # normal call, so use a longer default. + client_timeout = aiohttp.ClientTimeout(total=SIP_DIAL_TIMEOUT) if trunk_id: create.sip_trunk_id = trunk_id @@ -831,6 +831,8 @@ async def transfer_sip_participant( sip=SIPGrants(call=True), ), SIPParticipantInfo, + # Transferring a call dials a phone, which takes longer than normal. + timeout=aiohttp.ClientTimeout(total=SIP_DIAL_TIMEOUT), ) def _admin_headers(self) -> dict[str, str]: diff --git a/livekit-api/livekit/api/twirp_client.py b/livekit-api/livekit/api/twirp_client.py index 1c8d9966..8eba94fd 100644 --- a/livekit-api/livekit/api/twirp_client.py +++ b/livekit-api/livekit/api/twirp_client.py @@ -150,8 +150,15 @@ async def request( headers["Content-Type"] = "application/protobuf" serialized_data = data.SerializeToString() + # The effective per-attempt timeout is the per-call override, or the + # session default; used to gate failover for short requests. + effective_timeout = timeout.total if timeout else None + if effective_timeout is None and self._session.timeout is not None: + effective_timeout = self._session.timeout.total host = urlparse(self._origin).hostname - max_attempts = failover_attempts(self._failover, host, self._failover_force) + max_attempts = failover_attempts( + self._failover, host, self._failover_force, effective_timeout + ) attempted = {host_key(self._origin)} region_origins: Optional[List[str]] = None current_origin = self._origin From fd4f2f9f627edbacbf3a399d7547b6e5f6a85b51 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Tue, 30 Jun 2026 17:26:05 +0200 Subject: [PATCH 2/8] allow users to override transfer timeout --- livekit-api/livekit/api/sip_service.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index eb6bc7d8..0fcf4036 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -809,16 +809,25 @@ async def create_sip_participant( ) async def transfer_sip_participant( - self, transfer: TransferSIPParticipantRequest + self, + transfer: TransferSIPParticipantRequest, + *, + timeout: Optional[float] = None, ) -> SIPParticipantInfo: """Transfer a SIP participant to a different room. Args: transfer: Request containing transfer details + timeout: Optional request timeout in seconds. Transferring dials a + phone, which takes longer than normal, so it defaults to a + longer timeout when unset. Returns: Updated SIP participant information """ + # Transferring a call dials a phone, which takes longer than a normal + # call, so use a longer default unless the user specified a timeout. + client_timeout = aiohttp.ClientTimeout(total=timeout if timeout else SIP_DIAL_TIMEOUT) return await self._client.request( SVC, "TransferSIPParticipant", @@ -831,8 +840,7 @@ async def transfer_sip_participant( sip=SIPGrants(call=True), ), SIPParticipantInfo, - # Transferring a call dials a phone, which takes longer than normal. - timeout=aiohttp.ClientTimeout(total=SIP_DIAL_TIMEOUT), + timeout=client_timeout, ) def _admin_headers(self) -> dict[str, str]: From 3e10827458b9a1e8c376a097db9f74e488922d08 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Tue, 30 Jun 2026 17:34:00 +0200 Subject: [PATCH 3/8] add buffer on top of ringing timeout --- livekit-api/livekit/api/sip_service.py | 31 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index 0fcf4036..9d9ef425 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -44,6 +44,22 @@ SIP_DIAL_TIMEOUT = 30.0 """@private""" +# A dialing request must outlast the ringing window, or it would abort before +# the call can be answered. Keep the request timeout at least this many seconds +# above the request's ringing_timeout. +RINGING_TIMEOUT_MARGIN = 2.0 +"""@private""" + + +def _dial_timeout(user_timeout: Optional[float], request) -> float: + """Request timeout (seconds) for a phone-dialing call: the user-supplied + value (or the dial default) raised, when needed, to stay at least + RINGING_TIMEOUT_MARGIN above the request's ringing_timeout.""" + effective = user_timeout if user_timeout else SIP_DIAL_TIMEOUT + if request.HasField("ringing_timeout"): + effective = max(effective, request.ringing_timeout.seconds + RINGING_TIMEOUT_MARGIN) + return effective + class SipService(Service): """Client for LiveKit SIP Service API @@ -785,13 +801,13 @@ async def create_sip_participant( SIPError: If the SIP operation fails """ client_timeout: Optional[aiohttp.ClientTimeout] = None - if timeout: + if create.wait_until_answered: + # Dialing a phone and waiting for an answer takes longer than a + # normal call, and the request must outlast ringing. + client_timeout = aiohttp.ClientTimeout(total=_dial_timeout(timeout, create)) + elif timeout: # obey user specified timeout client_timeout = aiohttp.ClientTimeout(total=timeout) - elif create.wait_until_answered: - # Dialing a phone and waiting for an answer takes longer than a - # normal call, so use a longer default. - client_timeout = aiohttp.ClientTimeout(total=SIP_DIAL_TIMEOUT) if trunk_id: create.sip_trunk_id = trunk_id @@ -826,8 +842,9 @@ async def transfer_sip_participant( Updated SIP participant information """ # Transferring a call dials a phone, which takes longer than a normal - # call, so use a longer default unless the user specified a timeout. - client_timeout = aiohttp.ClientTimeout(total=timeout if timeout else SIP_DIAL_TIMEOUT) + # call, so use a longer default unless the user specified a timeout, and + # keep the request alive past ringing so the destination can answer. + client_timeout = aiohttp.ClientTimeout(total=_dial_timeout(timeout, transfer)) return await self._client.request( SVC, "TransferSIPParticipant", From e9895fa90095b0f6fceecee57ac4c7d2bf8e9f59 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Tue, 30 Jun 2026 08:39:36 -0700 Subject: [PATCH 4/8] Apply suggestion from @devin-ai-integration[bot] Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- livekit-api/livekit/api/sip_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index 9d9ef425..b92868ca 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -38,6 +38,7 @@ from .access_token import VideoGrants, SIPGrants SVC = "SIP" +"""@private""" # Calls that dial a phone (CreateSIPParticipant with wait_until_answered, # TransferSIPParticipant) take longer than a normal request. From 753157a8ca099f1cf114166357b13e99463bff41 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Tue, 30 Jun 2026 17:41:27 +0200 Subject: [PATCH 5/8] fix types --- livekit-api/livekit/api/sip_service.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index b92868ca..c8d55e1c 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -2,7 +2,7 @@ import aiohttp import warnings -from typing import Optional +from typing import Optional, Union from livekit.protocol.models import ListUpdate from livekit.protocol.sip import ( @@ -52,7 +52,10 @@ """@private""" -def _dial_timeout(user_timeout: Optional[float], request) -> float: +def _dial_timeout( + user_timeout: Optional[float], + request: Union[CreateSIPParticipantRequest, TransferSIPParticipantRequest], +) -> float: """Request timeout (seconds) for a phone-dialing call: the user-supplied value (or the dial default) raised, when needed, to stay at least RINGING_TIMEOUT_MARGIN above the request's ringing_timeout.""" From 7498d6fdbb21a3bfacb9505cd95594591739e782 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Wed, 1 Jul 2026 10:16:04 +0200 Subject: [PATCH 6/8] also handle whatsapp dial timeout --- livekit-api/livekit/api/_dial_timeout.py | 28 ++++++++++++++++++++ livekit-api/livekit/api/connector_service.py | 20 +++++++++++++- livekit-api/livekit/api/sip_service.py | 27 ++----------------- 3 files changed, 49 insertions(+), 26 deletions(-) create mode 100644 livekit-api/livekit/api/_dial_timeout.py diff --git a/livekit-api/livekit/api/_dial_timeout.py b/livekit-api/livekit/api/_dial_timeout.py new file mode 100644 index 00000000..a01b6279 --- /dev/null +++ b/livekit-api/livekit/api/_dial_timeout.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import Optional + +# Calls that dial a phone (SIP CreateSIPParticipant with wait_until_answered and +# TransferSIPParticipant; WhatsApp AcceptWhatsAppCall with wait_until_answered) +# take longer than a normal request. +DIAL_TIMEOUT = 30.0 +"""@private""" + +# A dialing request must outlast the ringing window, or it would abort before +# the call can be answered. Keep the request timeout at least this many seconds +# above the request's ringing_timeout. +RINGING_TIMEOUT_MARGIN = 2.0 +"""@private""" + + +def dial_timeout(user_timeout: Optional[float], request) -> float: + """Request timeout (seconds) for a phone-dialing call: the user-supplied + value (or the dial default) raised, when needed, to stay at least + RINGING_TIMEOUT_MARGIN above the request's ringing_timeout. + + @private + """ + effective = user_timeout if user_timeout else DIAL_TIMEOUT + if request.HasField("ringing_timeout"): + effective = max(effective, request.ringing_timeout.seconds + RINGING_TIMEOUT_MARGIN) + return effective diff --git a/livekit-api/livekit/api/connector_service.py b/livekit-api/livekit/api/connector_service.py index d5eaf2a1..98fb759f 100644 --- a/livekit-api/livekit/api/connector_service.py +++ b/livekit-api/livekit/api/connector_service.py @@ -1,6 +1,7 @@ from __future__ import annotations import aiohttp +from typing import Optional from livekit.protocol.connector_whatsapp import ( DialWhatsAppCallRequest, @@ -17,6 +18,7 @@ ConnectTwilioCallResponse, ) from ._service import Service +from ._dial_timeout import dial_timeout from .access_token import VideoGrants SVC = "Connector" @@ -106,23 +108,39 @@ async def connect_whatsapp_call( ) async def accept_whatsapp_call( - self, request: AcceptWhatsAppCallRequest + self, + request: AcceptWhatsAppCallRequest, + *, + timeout: Optional[float] = None, ) -> AcceptWhatsAppCallResponse: """ Accept an inbound WhatsApp call Args: request: AcceptWhatsAppCallRequest containing call parameters and SDP + timeout: Optional request timeout in seconds. When the request waits + for an answer (wait_until_answered), it defaults to a longer value + (dialing takes time) and is raised, if needed, to stay above the + request's ringing_timeout. Returns: AcceptWhatsAppCallResponse with the room name """ + client_timeout: Optional[aiohttp.ClientTimeout] = None + if request.wait_until_answered: + # Waiting for the call to be answered dials a phone, which takes + # longer than a normal request and must outlast ringing. + client_timeout = aiohttp.ClientTimeout(total=dial_timeout(timeout, request)) + elif timeout: + client_timeout = aiohttp.ClientTimeout(total=timeout) + return await self._client.request( SVC, "AcceptWhatsAppCall", request, self._auth_header(VideoGrants(room_create=True)), AcceptWhatsAppCallResponse, + timeout=client_timeout, ) async def connect_twilio_call( diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index c8d55e1c..3a8c0df5 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -2,7 +2,7 @@ import aiohttp import warnings -from typing import Optional, Union +from typing import Optional from livekit.protocol.models import ListUpdate from livekit.protocol.sip import ( @@ -35,35 +35,12 @@ SIPTransport, ) from ._service import Service +from ._dial_timeout import dial_timeout as _dial_timeout from .access_token import VideoGrants, SIPGrants SVC = "SIP" """@private""" -# Calls that dial a phone (CreateSIPParticipant with wait_until_answered, -# TransferSIPParticipant) take longer than a normal request. -SIP_DIAL_TIMEOUT = 30.0 -"""@private""" - -# A dialing request must outlast the ringing window, or it would abort before -# the call can be answered. Keep the request timeout at least this many seconds -# above the request's ringing_timeout. -RINGING_TIMEOUT_MARGIN = 2.0 -"""@private""" - - -def _dial_timeout( - user_timeout: Optional[float], - request: Union[CreateSIPParticipantRequest, TransferSIPParticipantRequest], -) -> float: - """Request timeout (seconds) for a phone-dialing call: the user-supplied - value (or the dial default) raised, when needed, to stay at least - RINGING_TIMEOUT_MARGIN above the request's ringing_timeout.""" - effective = user_timeout if user_timeout else SIP_DIAL_TIMEOUT - if request.HasField("ringing_timeout"): - effective = max(effective, request.ringing_timeout.seconds + RINGING_TIMEOUT_MARGIN) - return effective - class SipService(Service): """Client for LiveKit SIP Service API From 69f17846a063c35471dbb9ed25fedb464894982e Mon Sep 17 00:00:00 2001 From: David Zhao Date: Wed, 1 Jul 2026 10:22:46 +0200 Subject: [PATCH 7/8] fix types --- livekit-api/livekit/api/_dial_timeout.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/livekit-api/livekit/api/_dial_timeout.py b/livekit-api/livekit/api/_dial_timeout.py index a01b6279..5c97887f 100644 --- a/livekit-api/livekit/api/_dial_timeout.py +++ b/livekit-api/livekit/api/_dial_timeout.py @@ -1,6 +1,18 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Union + +from livekit.protocol.connector_whatsapp import AcceptWhatsAppCallRequest +from livekit.protocol.sip import CreateSIPParticipantRequest, TransferSIPParticipantRequest + +# Requests that carry wait_until_answered / ringing_timeout and share the +# phone-dialing timeout behavior. +DialRequest = Union[ + CreateSIPParticipantRequest, + TransferSIPParticipantRequest, + AcceptWhatsAppCallRequest, +] +"""@private""" # Calls that dial a phone (SIP CreateSIPParticipant with wait_until_answered and # TransferSIPParticipant; WhatsApp AcceptWhatsAppCall with wait_until_answered) @@ -15,7 +27,7 @@ """@private""" -def dial_timeout(user_timeout: Optional[float], request) -> float: +def dial_timeout(user_timeout: Optional[float], request: DialRequest) -> float: """Request timeout (seconds) for a phone-dialing call: the user-supplied value (or the dial default) raised, when needed, to stay at least RINGING_TIMEOUT_MARGIN above the request's ringing_timeout. From a5c394aabcbaba9eb7731cadfa0bd9c97009daa7 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Wed, 1 Jul 2026 11:03:05 +0200 Subject: [PATCH 8/8] set default ringing timeout --- livekit-api/livekit/api/_dial_timeout.py | 36 ++++++++++++++------ livekit-api/livekit/api/connector_service.py | 6 ++-- livekit-api/livekit/api/sip_service.py | 11 +++--- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/livekit-api/livekit/api/_dial_timeout.py b/livekit-api/livekit/api/_dial_timeout.py index 5c97887f..a703b204 100644 --- a/livekit-api/livekit/api/_dial_timeout.py +++ b/livekit-api/livekit/api/_dial_timeout.py @@ -14,27 +14,41 @@ ] """@private""" -# Calls that dial a phone (SIP CreateSIPParticipant with wait_until_answered and -# TransferSIPParticipant; WhatsApp AcceptWhatsAppCall with wait_until_answered) -# take longer than a normal request. -DIAL_TIMEOUT = 30.0 +# Ring window (seconds) assumed when a request doesn't set ringing_timeout; +# matches the server default. A dialing request must outlast it. +DEFAULT_RINGING_TIMEOUT = 30.0 """@private""" # A dialing request must outlast the ringing window, or it would abort before # the call can be answered. Keep the request timeout at least this many seconds -# above the request's ringing_timeout. +# above the ringing timeout. RINGING_TIMEOUT_MARGIN = 2.0 """@private""" +def pin_ringing_timeout(request: DialRequest) -> None: + """Set the ring window explicitly on a dialing request when the caller left it + unset, so the derived request timeout doesn't depend on the server's default + (which could change out from under us). + + @private + """ + if not request.HasField("ringing_timeout"): + request.ringing_timeout.seconds = int(DEFAULT_RINGING_TIMEOUT) + + def dial_timeout(user_timeout: Optional[float], request: DialRequest) -> float: - """Request timeout (seconds) for a phone-dialing call: the user-supplied - value (or the dial default) raised, when needed, to stay at least - RINGING_TIMEOUT_MARGIN above the request's ringing_timeout. + """Request timeout (seconds) for a phone-dialing call: the ring window plus a + margin, so the request doesn't abort before the call can be answered. The + ring window is the request's ringing_timeout when set, else + DEFAULT_RINGING_TIMEOUT. A longer user_timeout is honored; a shorter one is + raised to the floor. @private """ - effective = user_timeout if user_timeout else DIAL_TIMEOUT if request.HasField("ringing_timeout"): - effective = max(effective, request.ringing_timeout.seconds + RINGING_TIMEOUT_MARGIN) - return effective + ring: float = request.ringing_timeout.seconds + else: + ring = DEFAULT_RINGING_TIMEOUT + floor = ring + RINGING_TIMEOUT_MARGIN + return max(user_timeout if user_timeout else floor, floor) diff --git a/livekit-api/livekit/api/connector_service.py b/livekit-api/livekit/api/connector_service.py index 98fb759f..2c464bde 100644 --- a/livekit-api/livekit/api/connector_service.py +++ b/livekit-api/livekit/api/connector_service.py @@ -18,7 +18,7 @@ ConnectTwilioCallResponse, ) from ._service import Service -from ._dial_timeout import dial_timeout +from ._dial_timeout import dial_timeout, pin_ringing_timeout from .access_token import VideoGrants SVC = "Connector" @@ -129,7 +129,9 @@ async def accept_whatsapp_call( client_timeout: Optional[aiohttp.ClientTimeout] = None if request.wait_until_answered: # Waiting for the call to be answered dials a phone, which takes - # longer than a normal request and must outlast ringing. + # longer than a normal request and must outlast ringing. Pin the ring + # window so the timeout doesn't depend on the server's default. + pin_ringing_timeout(request) client_timeout = aiohttp.ClientTimeout(total=dial_timeout(timeout, request)) elif timeout: client_timeout = aiohttp.ClientTimeout(total=timeout) diff --git a/livekit-api/livekit/api/sip_service.py b/livekit-api/livekit/api/sip_service.py index 3a8c0df5..6ad7c483 100644 --- a/livekit-api/livekit/api/sip_service.py +++ b/livekit-api/livekit/api/sip_service.py @@ -35,7 +35,7 @@ SIPTransport, ) from ._service import Service -from ._dial_timeout import dial_timeout as _dial_timeout +from ._dial_timeout import dial_timeout as _dial_timeout, pin_ringing_timeout as _pin_ringing_timeout from .access_token import VideoGrants, SIPGrants SVC = "SIP" @@ -784,7 +784,9 @@ async def create_sip_participant( client_timeout: Optional[aiohttp.ClientTimeout] = None if create.wait_until_answered: # Dialing a phone and waiting for an answer takes longer than a - # normal call, and the request must outlast ringing. + # normal call, and the request must outlast ringing. Pin the ring + # window so the timeout doesn't depend on the server's default. + _pin_ringing_timeout(create) client_timeout = aiohttp.ClientTimeout(total=_dial_timeout(timeout, create)) elif timeout: # obey user specified timeout @@ -823,8 +825,9 @@ async def transfer_sip_participant( Updated SIP participant information """ # Transferring a call dials a phone, which takes longer than a normal - # call, so use a longer default unless the user specified a timeout, and - # keep the request alive past ringing so the destination can answer. + # call, so keep the request alive past ringing. Pin the ring window so the + # timeout doesn't depend on the server's default. + _pin_ringing_timeout(transfer) client_timeout = aiohttp.ClientTimeout(total=_dial_timeout(timeout, transfer)) return await self._client.request( SVC,