diff --git a/lib/livekit/connector_service_client.rb b/lib/livekit/connector_service_client.rb index fcdca66..f89a718 100644 --- a/lib/livekit/connector_service_client.rb +++ b/lib/livekit/connector_service_client.rb @@ -2,6 +2,7 @@ require "livekit/auth_mixin" require 'livekit/utils' require 'livekit/failover' +require 'livekit/dial_timeout' module LiveKit # Client for LiveKit's Connector service, bridging WhatsApp and Twilio calls @@ -33,13 +34,22 @@ def dial_whatsapp_call(request) # Accepts an inbound WhatsApp call. # @param request [Proto::AcceptWhatsAppCallRequest] + # @param timeout [Numeric, nil] optional request timeout in seconds. When the + # request waits for an answer, it defaults to a longer value (dialing takes + # time) and is raised, if needed, to stay above the request's ringing_timeout. # @return [Proto::AcceptWhatsAppCallResponse] - def accept_whatsapp_call(request) - self.rpc( - :AcceptWhatsAppCall, - request, - headers: auth_header(video_grant: VideoGrant.new(roomCreate: true)), - ) + def accept_whatsapp_call(request, timeout: nil) + headers = auth_header(video_grant: VideoGrant.new(roomCreate: true)) + # Accept can block until the call is answered, so default the request timeout + # to the standard ring window; otherwise honor any user timeout. The caller + # overrides via +timeout+ and should set it above the ringing_timeout passed + # to dial_whatsapp_call (the two calls are separate). + if request.wait_until_answered + headers[Failover::TIMEOUT_HEADER] = (timeout || DialTimeout::DEFAULT_RINGING_TIMEOUT).to_s + elsif timeout + headers[Failover::TIMEOUT_HEADER] = timeout.to_s + end + self.rpc(:AcceptWhatsAppCall, request, headers: headers) end # Connects an established WhatsApp call (used for business-initiated calls). diff --git a/lib/livekit/dial_timeout.rb b/lib/livekit/dial_timeout.rb new file mode 100644 index 0000000..d67101e --- /dev/null +++ b/lib/livekit/dial_timeout.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module LiveKit + # Request-timeout handling shared by calls that dial a phone and wait for an + # answer (SIP CreateSIPParticipant/TransferSIPParticipant, WhatsApp + # AcceptWhatsAppCall). These take longer than a normal request, and the request + # must outlast ringing or it would abort before the call can be answered. + module DialTimeout + # Ring window (seconds) assumed when a request doesn't set a ringing timeout; + # matches the server default. A dialing request must outlast it. + DEFAULT_RINGING_TIMEOUT = 30 + # Keep the request timeout at least this many seconds above the ringing + # timeout, so the request doesn't abort before the call can be answered. + RINGING_TIMEOUT_MARGIN = 2 + + # Request timeout (seconds): the ring window plus a margin, so the request + # doesn't abort before the call can be answered. The ring window is + # +ringing_timeout+ (seconds) when set, else DEFAULT_RINGING_TIMEOUT. A longer + # user-supplied +timeout+ is honored; a shorter one is raised to the floor. + def self.resolve(timeout, ringing_timeout) + ring = ringing_timeout || DEFAULT_RINGING_TIMEOUT + floor = ring + RINGING_TIMEOUT_MARGIN + [timeout || floor, floor].max + end + end +end diff --git a/lib/livekit/sip_service_client.rb b/lib/livekit/sip_service_client.rb index d76850c..a9920c9 100644 --- a/lib/livekit/sip_service_client.rb +++ b/lib/livekit/sip_service_client.rb @@ -2,6 +2,7 @@ require "livekit/auth_mixin" require 'livekit/utils' require 'livekit/failover' +require 'livekit/dial_timeout' module LiveKit class SIPServiceClient < Twirp::Client @@ -9,10 +10,6 @@ class SIPServiceClient < Twirp::Client include AuthMixin attr_accessor :api_key, :api_secret - # Calls that dial a phone (CreateSIPParticipant with wait_until_answered, - # TransferSIPParticipant) take longer than a normal request. - SIP_DIAL_TIMEOUT = 30 - def initialize(base_url, api_key: nil, api_secret: nil, failover: true) super(LiveKit::Failover.connection(base_url, failover)) @api_key = api_key @@ -205,8 +202,14 @@ def create_sip_participant( # Optional, enable Krisp for this call krisp_enabled: false, # Optional, wait for the call to be answered before returning - wait_until_answered: false + wait_until_answered: false, + # Optional, request timeout in seconds. Defaults to a longer value when + # wait_until_answered is set (dialing takes time). + timeout: nil ) + # When waiting for an answer, pin the ring window explicitly so our request + # timeout doesn't depend on the server's default (which could change). + ringing_timeout = DialTimeout::DEFAULT_RINGING_TIMEOUT if wait_until_answered && ringing_timeout.nil? request = Proto::CreateSIPParticipantRequest.new( sip_trunk_id: sip_trunk_id, sip_call_to: sip_call_to, @@ -224,8 +227,10 @@ def create_sip_participant( wait_until_answered: wait_until_answered ) headers = auth_header(sip_grant: SIPGrant.new(call: true)) - # Dialing a phone and waiting for an answer takes longer than a normal request. - headers[Failover::TIMEOUT_HEADER] = SIP_DIAL_TIMEOUT.to_s if wait_until_answered + # When waiting for an answer, dialing takes longer than a normal request + # and the request must outlast ringing; otherwise honor any user timeout. + effective_timeout = wait_until_answered ? DialTimeout.resolve(timeout, ringing_timeout) : timeout + headers[Failover::TIMEOUT_HEADER] = effective_timeout.to_s if effective_timeout self.rpc(:CreateSIPParticipant, request, headers: headers) end @@ -233,18 +238,26 @@ def transfer_sip_participant( room_name, participant_identity, transfer_to, - play_dialtone: nil + play_dialtone: nil, + # Optional, max time for the transfer destination to answer, in seconds. + ringing_timeout: nil, + # Optional, request timeout in seconds. Defaults to a longer value since + # transferring dials a phone. + timeout: nil ) + # Transferring a call dials a phone and must outlast ringing. Pin the ring + # window explicitly so our request timeout doesn't depend on the server default. + ringing_timeout ||= DialTimeout::DEFAULT_RINGING_TIMEOUT request = Proto::TransferSIPParticipantRequest.new( room_name: room_name, participant_identity: participant_identity, transfer_to: transfer_to, play_dialtone: play_dialtone, + ringing_timeout: ringing_timeout, ) headers = auth_header(video_grant: VideoGrant.new(roomAdmin: true, room: room_name), sip_grant: SIPGrant.new(call: true)) - # Transferring a call dials a phone, which takes longer than a normal request. - headers[Failover::TIMEOUT_HEADER] = SIP_DIAL_TIMEOUT.to_s + headers[Failover::TIMEOUT_HEADER] = DialTimeout.resolve(timeout, ringing_timeout).to_s self.rpc(:TransferSIPParticipant, request, headers: headers) end