import type { Observable } from 'rxjs'; import type { DtmfEvent, SipInfo, SipSignal } from './events'; import type { MediaChannel } from './media-channel'; /** * SIP call state visible to script developers. * * ### Lifecycle * * ``` * outbound 183 w/ SDP * idle ──► ringing ─────────────────► early ──► active ──► terminated * │ ▲ ▲ * │ inbound │ │ * │ sendProgress()───────┘ │ * └───────────────────────────────────┘ (early may be skipped) * ``` * * | State | Meaning | How you get here | * |---|---|---| * | **`idle`** | Call object exists but no SIP signalling has started yet. | Initial state. | * | **`ringing`** | INVITE sent (outbound) or received (inbound); remote party is ringing. No media yet. | Automatic after INVITE. | * | **`early`** | RTP pipeline is up — you can **send and receive audio**, run ASR, play TTS. This is the "pre-answer" phase before final 200 OK. | **Outbound:** remote sends 183 Session Progress with SDP. **Inbound:** your script calls `sip.sendProgress()`. | * | **`active`** | 200 OK received/sent — the call is fully established. | **Outbound:** remote answers. **Inbound:** your script calls `sip.answer()`. | * | **`holding`** | Local hold is active. | Your script calls `sip.hold()`. | * | **`terminated`** | Call ended (BYE, CANCEL, or error). No further audio is possible. | Either side hangs up, or network error. | * * > **Note:** not every call goes through every state. A fast answer may jump * > `ringing` → `active` without `early`. An unanswered outbound call may go * > `ringing` → `terminated`. Inbound calls stay in `ringing` until you call * > either `sendProgress()` (→ `early`) or `answer()` (→ `active`). */ export type SipState = 'idle' | 'ringing' | 'early' | 'active' | 'holding' | 'terminated'; /** * A SIP 1xx provisional response forwarded to the script. * * Provisional responses are sent by the remote party **before** the final answer (200 OK). * Common examples: * * | Code | Phrase | Typical meaning | * |------|--------|-----------------| * | 100 | Trying | Request received, processing | * | 180 | Ringing | Remote phone is ringing (may carry SDP → early media) | * | 183 | Session Progress | Early media available — SDP is present, RTP can flow | * * Subscribe to `sip.progress$` to track every provisional response. The `sdp` field * is present only when the response carries a Session Description (media offer/answer). */ export interface SipProgressEvent { /** SIP status code (100–199). */ statusCode: number; /** Human-readable reason phrase, e.g. `"Ringing"`, `"Session Progress"`. */ statusPhrase: string; /** Raw SDP body when the provisional response carries a media description. */ sdp?: string; } /** * SIP channel API — call-state observables, DTMF, INFO, hold/mute/hangup, * outbound **`makeCall`**, **`bridge`**, and **pre-answer media**. * * --- * * ### Pre-answer media (early media) * * Both **inbound** and **outbound** calls support full-duplex audio **before** the * 200 OK answer. In the `"early"` state the RTP pipeline is fully operational — * ASR, TTS, `audio.say()`, `audio.play()`, and DTMF all work **exactly the same** * as in the `"active"` state. * * #### Outbound calls * * The remote side may send **183 Session Progress** with SDP (e.g. an IVR greeting, * ringback tone, or DTMF challenge). The call enters `"early"` automatically. * * #### Inbound calls * * Call `sip.sendProgress()` to send **183 Session Progress** to the caller. The call * enters `"early"` and audio flows in both directions before final 200 OK. The API * does not mark the call answered until `sip.answer()`; external billing still depends * on carrier policy. Call `sip.answer()` later to complete the answer. * * --- * * ### Quick examples * * #### Outbound — interact with an IVR before answer * * ```ts * const call = await channel.sip.makeCall({ sipUri: 'sip:+1234@trunk.example.com' }); * * // Wait until RTP is ready (early media or full answer) * await call.sip.waitForEarly(); * // Audio flows — start ASR to listen to the remote IVR * const asr = await call.createAsr(); * asr.result$.subscribe((text) => { * if (/press 1/i.test(text)) call.sip.sendDtmf('1'); * }); * * // Optionally wait for the actual answer * await call.sip.waitForAnswer(); * await call.audio.say('Hello! We are calling about your order.'); * ``` * * #### Inbound — collect data before the final answer * * ```ts * // Inbound call arrives — state is 'ringing' * channel.sip.sendProgress(); * // State is now 'early' — full duplex audio before final 200 OK * * const asr = await channel.createAsr(); * await channel.audio.say('Please say your account number.'); * const account = await firstValueFrom(asr.result$); * * // Now send final answer * channel.sip.answer(); * await channel.audio.say(`Thank you! Looking up account ${account}…`); * ``` * * #### Inbound — simple answer (no pre-answer) * * ```ts * // Inbound call — answer immediately * channel.sip.answer(); * await channel.audio.say('Welcome!'); * ``` * * #### Audio auto-buffering * * You do **not** need to manually wait for early/answer before calling `audio.say()` * or `audio.play()`. These methods automatically defer until the RTP pipeline is ready * and resolve as no-ops if the call terminates first. The explicit `waitForEarly()` / * `waitForAnswer()` await points are useful when you need to **sequence logic** around * call state (e.g. create ASR only after media is available). */ /** * SIP headers captured from an **inbound INVITE** at call setup (start-of-call only). * * Keys use the on-wire header name (`Diversion`, `P-Asserted-Identity`, `X-Trunk-Id`, * `X-language`, …). Values are a single string or a string array when the same header * appears multiple times (e.g. several `Diversion` hops). * * **Snapshot only** — does not update on 180/200, re-INVITE, or BYE. Outbound B-legs / * WS / headless typically have no snapshot (`undefined`). For mid-call peer signals use * {@link ChannelSip.sipInfo$} (body/contentType only). Casing matches what Sofia exposes. */ export type SipInviteHeaders = Record; /** * Structured parse of {@link ChannelSip.remoteSdp}. * * Parses selected session lines (`o=`, `s=`, `c=`, `m=audio`) and all `a=` attribute lines. * Attribute keys are the part **before** the first colon in `a=name:value` (e.g. `rtpmap`, * `fmtp`, vendor `x-custom`). Flag attributes without a colon (e.g. `sendrecv`) map to `""`. * Duplicate attribute names become `string[]`. * * @example * ```ts * const d = channel.sip.getRemoteSdpDetails(); * d?.session.connection; // "IN IP4 203.0.113.5" * d?.attributes.rtpmap; // "0 PCMU/8000" * d?.attributes['x-vendor-id']; // custom a=x-vendor-id:abc * ``` */ export interface ParsedSdpDetails { /** Selected SDP session description lines (not a full SDP grammar parse). */ session: { /** `o=` origin line (without prefix). */ origin?: string; /** `s=` session name. */ sessionName?: string; /** Last `c=` connection line seen (session or media level). */ connection?: string; /** `m=audio` media line (without prefix). */ mediaAudio?: string; }; /** SDP `a=` attributes keyed by attribute name. */ attributes: Record; } export interface ChannelSip { /** * Emits every time the remote party sends a DTMF digit (RFC 2833 in-band or SIP INFO). * * ```ts * sip.dtmf$.subscribe(({ digit, duration }) => { * console.log(`User pressed ${digit}`); * }); * ``` */ readonly dtmf$: Observable; /** * Mid-call SIP INFO stream — use when the peer signals **during** the call * (language change, vendor payload, etc.). * * Each event is {@link SipInfo}: **`contentType` + `body` only**. Headers on * the INFO request are not exposed. This does **not** update * {@link inviteSipHeaders}. Prefer {@link dtmf$} for DTMF. * **Hot stream** — events before subscribe are not replayed. * * ```ts * sip.sipInfo$.subscribe(({ contentType, body }) => { * logger.log('SIP INFO', { contentType, body }); * }); * ``` */ readonly sipInfo$: Observable; /** * Low-level SIP state-change events from the underlying Sofia stack. * * Prefer the higher-level **`state$`**, **`progress$`**, **`early$`**, **`answered$`** * observables unless you need raw Sofia event details. The `sdp` field is present * only when that specific state callback carries an SDP body. */ readonly sipSignal$: Observable; /** * Latest negotiated **remote party SDP** body (raw text). * * **Inbound:** usually present from INVITE. **Outbound:** often empty until 183/200 * with SDP; may change on re-INVITE. Synchronous getter — re-read after * {@link progress$} / {@link sipSignal$} events that carry `sdp`. Undefined on WS/headless. */ readonly remoteSdp?: string; /** * **Start-of-call** snapshot of SIP headers from the **inbound INVITE** * (routing, `X-Trunk-Id`, `X-Neuro-UUID`, `X-language`, …). See {@link SipInviteHeaders}. * * Does **not** update on 180/200/re-INVITE/BYE. Outbound B-legs, WS, and headless * usually have `undefined`. For mid-call peer signals use {@link sipInfo$} * (body/contentType only — INFO headers are not available). Live response * headers are not exposed; {@link sipSignal$} has status code/phrase/SDP only. * * ```ts * const h = channel.sip.inviteSipHeaders; * const diversion = h?.Diversion; // string | string[] | undefined * const trunk = h?.['X-Trunk-Id']; * ``` */ readonly inviteSipHeaders?: SipInviteHeaders; /** * Parse the current {@link remoteSdp} into {@link ParsedSdpDetails}. * * Returns `null` when no remote SDP is available yet. Re-parses on every call from the * latest `remoteSdp` value (no separate cache in the host channel). */ getRemoteSdpDetails(): ParsedSdpDetails | null; /** * Live observable of the current call state. * * Emits the new {@link SipState} every time the call transitions. Starts with the * state the call was in when the script began (typically `"ringing"` for inbound, * `"idle"` or `"ringing"` for outbound). * * ```ts * sip.state$.subscribe((state) => { * console.log(`Call state → ${state}`); * }); * ``` * * > **Tip:** for one-shot checks use the synchronous `sip.state` getter instead. */ readonly state$: Observable; /** * Current call state at the moment of access (synchronous). * * Returns one of: `'idle'`, `'ringing'`, `'early'`, `'active'`, `'holding'`, `'terminated'`. * * ```ts * if (sip.state === 'active') { * await audio.say('Call is live'); * } * ``` */ readonly state: SipState; /** * Whether the call has been answered — `true` after 200 OK is received (outbound) * or sent (inbound via `sip.answer()`). * * This is a synchronous getter. For an awaitable version use {@link waitForAnswer}. * * ```ts * if (!sip.isAnswered) { * console.log('Still waiting for answer…'); * } * ``` */ readonly isAnswered: boolean; /** * Emits every SIP 1xx provisional response (180 Ringing, 183 Session Progress, etc.). * * Useful for tracking ringing state, ringback-tone detection, or reading SDP from * early 183 responses. Each event is a {@link SipProgressEvent} with `statusCode`, * `statusPhrase`, and optionally `sdp`. * * ```ts * sip.progress$.subscribe(({ statusCode, statusPhrase }) => { * console.log(`1xx: ${statusCode} ${statusPhrase}`); * }); * ``` * * > **Note:** `progress$` may emit **zero** events if the remote party answers * > immediately (200 OK without any provisional). */ readonly progress$: Observable; /** * Emits **once** when early media becomes available — the RTP pipeline is up and * audio can be sent/received **before** the call is formally answered. * * **Outbound:** fires automatically when the remote side sends a 1xx with SDP * (typically **183 Session Progress**). * * **Inbound:** fires after your script calls `sip.sendProgress()`. * * After this event fires: * - `audio.say()` / `audio.play()` will be heard by the remote party * - `createAsr()` will receive remote audio * - `dtmf$` will deliver in-band DTMF * - The API has not marked the call answered yet; carrier billing policy may vary * * If the call is answered without any early media (outbound: no 183 with SDP; * inbound: `answer()` called directly), `early$` **may not emit at all** — * use `answered$` or `waitForAnswer()` instead. * * ```ts * sip.early$.subscribe(() => { * console.log('Early media — RTP is flowing before answer'); * }); * ``` */ readonly early$: Observable; /** * Emits **once** when the call is answered (200 OK received for outbound, * or sent for inbound after `sip.answer()`). * * After this event the call state is `"active"` and the call is fully established. * * ```ts * sip.answered$.subscribe(() => { * console.log('Call answered — full duplex'); * }); * ``` * * > For the awaitable version see {@link waitForAnswer}. */ readonly answered$: Observable; /** * Returns a `Promise` that resolves when the call is answered (200 OK). * * If the call is **already answered** at the time of calling, resolves immediately. * * Useful in outbound scenarios where you want to block until the remote party picks up: * * ```ts * const call = await channel.sip.makeCall({ sipUri: destination }); * await call.sip.waitForAnswer(); * await call.audio.say('You have picked up!'); * ``` * * > **Warning:** if the call is never answered (busy, timeout, rejection) and your * > script does not handle `events.terminated$`, this Promise will remain pending * > until the call terminates (at which point the script exits). */ waitForAnswer(): Promise; /** * Returns a `Promise` that resolves as soon as the RTP pipeline is ready — * either on **early media** or on **answer**, whichever comes first. * * If the call is already in `"early"` or `"active"` state, resolves immediately. * * #### Outbound usage * * The recommended await point when you want to start ASR / play audio * **as early as possible** (including pre-answer): * * ```ts * const call = await channel.sip.makeCall({ sipUri: destination }); * * // Don't wait for full answer — start as soon as any media path exists * await call.sip.waitForEarly(); * * // ASR is already receiving remote audio (IVR prompts, ringback, etc.) * const asr = await call.createAsr(); * asr.result$.subscribe((text) => console.log('Heard:', text)); * * // Play DTMF to navigate an IVR — works even before 200 OK * call.sip.sendDtmf('1'); * ``` * * #### Inbound usage * * For inbound calls you must call `sendProgress()` **first** to trigger early media, * then `waitForEarly()` resolves: * * ```ts * channel.sip.sendProgress(); * await channel.sip.waitForEarly(); // resolves immediately after sendProgress() * const asr = await channel.createAsr(); * ``` * * > **Tip:** some carriers answer outbound calls without early media (180 without SDP). * > In that case `waitForEarly()` resolves only when the 200 OK arrives. */ waitForEarly(): Promise; /** * Send a DTMF digit to the remote party. * * SIP channels delegate to the telephony stack. WS channels validate one digit * and emit `dtmf-send` to the connected client. Headless channels ignore it. * * @param digit - One of `0`–`9`, `*`, `#`. * @param duration - Tone duration in milliseconds (default **250 ms**). * * ```ts * // Navigate an IVR: press 1, wait, press 3 * sip.sendDtmf('1'); * await new Promise((r) => setTimeout(r, 2000)); * sip.sendDtmf('3'); * ``` */ sendDtmf(digit: string, duration?: number): void; /** * Send a SIP INFO request with an arbitrary content type and body on this call leg. * * No-op on WS and headless channels. * * @param contentType - MIME type, e.g. `"application/dtmf-relay"`. * @param body - Raw text body of the INFO request. */ sendInfo(contentType: string, body: string): void; /** * Put the SIP call on hold (sends re-INVITE with `a=sendonly`). * * The remote party hears silence (or hold music if your carrier supports it). * Call {@link unhold} to resume. */ hold(): void; /** * Resume a held SIP call (sends re-INVITE with `a=sendrecv`). */ unhold(): void; /** * Suppress outgoing SIP audio locally — the remote party hears silence, * but you still receive their audio. * * This does **not** send a re-INVITE; it simply stops feeding PCM * to the RTP encoder. Call {@link unmute} to resume. */ mute(): void; /** Resume sending audio after {@link mute}. No-op on WS/headless channels. */ unmute(): void; /** * Hang up the call. * * SIP channels send BYE. WS channels disconnect the socket. Headless channels destroy * the synthetic channel. After this call `events.terminated$` will emit and the session ends. */ hangup(): void; /** * Answer an **inbound** call (sends 200 OK with SDP). * * No-op if the call is already answered or if this is an outbound call. Also no-op * on WS/headless channels. After answering, the state transitions to `"active"` and * the API records the call answer time. * * If the call is in `"early"` state (after `sendProgress()`), `answer()` promotes it * to `"active"` — audio was already flowing and the final answer is now sent. * * ```ts * // Simple: answer immediately * channel.sip.answer(); * await channel.audio.say('Hello, how can I help?'); * ``` * * ```ts * // Advanced: pre-answer → answer * channel.sip.sendProgress(); // early media before final 200 OK * await channel.audio.say('One moment…'); * channel.sip.answer(); // final 200 OK * await channel.audio.say('How can I help?'); * ``` */ answer(): void; /** * Send **183 Session Progress** with SDP on an **inbound** call, enabling * early media (full-duplex audio) **before** the 200 OK answer. * * After calling `sendProgress()`: * - The call state transitions to `"early"` and `early$` emits. * - `audio.say()`, `audio.play()`, `createAsr()`, `dtmf$` all work — * exactly the same as in the `"active"` state. * - The remote party hears your audio before the final 200 OK. The API still treats * the call as not answered; external billing depends on carrier policy. * * Call `sip.answer()` later to send the final 200 OK and transition to `"active"`. * * No-op if the call is already in `"early"` or `"active"` state, or if this is * an outbound / WS / headless channel. * * ### Typical use case: play a greeting before answering * * ```ts * // Inbound call arrives — state is 'ringing' * channel.sip.sendProgress(); * // State is now 'early' — audio flows before final 200 OK * * await channel.audio.say('Please hold while we connect you…'); * * // Now answer with final 200 OK * channel.sip.answer(); * await channel.audio.say('Hello! How can I help?'); * ``` * * ### Typical use case: start ASR before answering * * ```ts * channel.sip.sendProgress(); * const asr = await channel.createAsr(); * await channel.audio.say('Hi! What is your account number?'); * const result = await firstValueFrom(asr.result$); * // Collected account number before sending final 200 OK * channel.sip.answer(); * ``` */ sendProgress(): void; /** * Initiate an **outbound** SIP call and return the new B-leg. * * Returns a new {@link MediaChannel} representing the B-leg. The returned channel * has its own `sip`, `audio`, `events`, etc. — use `waitForAnswer()` or * `waitForEarly()` on the B-leg before sending audio. * * Supported by SIP channels. In worker isolation mode the host proxies the returned * B-leg, including media operations, ASR, SIP control, LLM, and `bridge()`. WS channels * dial the B-leg via the host SIP stack and bridge client PCM to RTP. Headless channels throw. * * @param opts.sipUri - Full SIP URI, e.g. `"sip:+12025551234@trunk.carrier.com"`. * @param opts.msisdn - Legacy-mode phone number. The host resolves the SIP URI from the selected trunk. * @param opts.channel - Legacy-mode trunk name override, matching `nv.bridge(..., channel=...)`. * @param opts.protoAdditional - Legacy-mode extra outbound INVITE headers. * @param opts.fromUri - Optional caller-ID override SIP URI. * @returns A new `MediaChannel` for the outbound leg. * * ```ts * const bLeg = await channel.sip.makeCall({ * msisdn: '+12025551234', * channel: 'carrier-main', * }); * await bLeg.sip.waitForAnswer(); * // Bridge both legs so callers hear each other * const teardown = channel.sip.bridge(bLeg); * ``` */ makeCall(opts: { sipUri?: string; msisdn?: string; channel?: string; fromUri?: string; protoAdditional?: Record; legacyInviteHeaders?: { xViaTrunk: string; protoAdditional?: Record; }; }): Promise; /** * Cross-connect audio between this SIP call and another SIP call (conference bridge). * * Both parties hear each other in real time. Returns a teardown function * that disconnects the bridge when called. ScriptEngine only bridges two * `SipMediaChannel` instances directly; non-SIP channels return a no-op teardown * or throw depending on the host. * * @param other - The other `MediaChannel` (e.g. an outbound B-leg). * @returns A function to tear down the bridge. * * ```ts * const teardown = channel.sip.bridge(bLeg); * // ... later * teardown(); // disconnect the bridge * ``` */ bridge(other: MediaChannel): () => void; } //# sourceMappingURL=sip.d.ts.map