Skip to content

Return response on transfer sip call #606

@mkharko

Description

@mkharko

Describe the problem

here is short description of issue:
I noticed a difference in SIP transfer behavior between livekit-cli and livekit-server-sdk (Node.js)

We are investigating SIP cold transfer behavior in LiveKit and noticed a discrepancy between the LiveKit CLI and the Node.js livekit-server-sdk.
What we tested
We tested SIP cold transfer in two ways:

  1. Using LiveKit CLI (v2.4.12)
  2. Using Node.js livekit-server-sdk (SipClient.transferSipParticipant)

Observed behavior
Using LiveKit CLI
When performing a SIP transfer via the CLI, the transfer command appears to wait for the actual transfer outcome.
 In cases where the destination party does not answer or rejects the call, the CLI command returns an error that can be unwrapped into a SIP status (e.g. busy, no-answer, etc.).
This aligns with the CLI implementation:
// TransferSIPParticipant will wait for call to be transferred and that can take some time.
// Default deadline is too short, thus, we must set a higher deadline for it.
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
return s.sipClient.TransferSIPParticipant(ctx, in)

and the presence of:
func SIPStatusFrom(err error) *livekit.SIPStatus

which allows extracting SIP-level failure details.
Using Node.js livekit-server-sdk
When calling:
sipClient.transferSipParticipant(roomName, identity, transferTo)

The promise resolves successfully as soon as the transfer request is accepted, even if:
• The destination does not answer,
• The call is rejected.
• or the SIP transfer ultimately fails.

No SIP status (busy / no-answer / failed) is returned or surfaced via the Node.js SDK call, which makes it difficult to determine whether the transfer actually succeeded.

here is thread: https://livekit-users.slack.com/archives/C07FVFM5NA1/p1767686742279039

Describe the proposed solution

async transferSipParticipant(
    roomName: string,
    participantIdentity: string,
    transferTo: string,
    opts?: TransferSipParticipantOptions,
  ): Promise<any> {
    if (opts === undefined) {
      opts = {};
    }

    const req = new TransferSIPParticipantRequest({
      participantIdentity: participantIdentity,
      roomName: roomName,
      transferTo: transferTo,
      playDialtone: opts.playDialtone,
      headers: opts.headers,
    }).toJson();

    try {
      return await this.rpc.request(
        svc,
        'TransferSIPParticipant',
        req,
        await this.authHeader({ roomAdmin: true, room: roomName }, { call: true }),
      );
    } catch (e) {
      if ((e as any)?.metadata?.sip_status || (e as any)?.metadata?.sip_status_code) {
        (e as any).sipStatus = (e as any).metadata.sip_status;
        (e as any).sipStatusCode = (e as any).metadata.sip_status_code;
      }
      throw e;
    }
  }

here is what have been changed:

Image

Alternatives considered

No response

Importance

I cannot use LiveKit without it

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions