Skip to content

Latest commit

 

History

History
556 lines (404 loc) · 17.3 KB

File metadata and controls

556 lines (404 loc) · 17.3 KB

sip-lab Documentation

Overview

A nodejs module that helps to write functional/integration tests for SIP systems (including media operations). It uses pjproject for SIP and media processing.

It permits to:

  • make audio calls using UDP, TCP and TLS transports
  • send/receive DTMF inband/RFC2833/INFO.
  • send/receive BFSK bits.
  • play/record audio on a call from/to a wav file
  • send/receive fax (T.30 only)
  • send/receive MRCPv2 messages (TCP only, no TLS)
  • send/receive audio using SRTP
  • do speech synth using flite
  • do speech recog using pocketsphinx (but only works well with sampling rate of 16000)
  • do speech synth/recog using ws_speech_server (this permits to use google/amazon/azure/etc speech services)

TODO:

  • add support for video playing/recording from/to file
  • add support for T.38 fax
  • add support for SIP over WebSocket
  • add support for WebRTC
  • add support for MSRP

Installation

The npm package is built for Debian 11 and this is the recommended distro.

You can use other debian/ubuntu version but they will require a build of dependencies that will take time (something like 7 minutes but this was measured on my slow PC).

First install apt packages:

apt install build-essential automake autoconf libtool libspeex-dev libopus-dev libsdl2-dev libavdevice-dev libswscale-dev libv4l-dev libopencore-amrnb-dev libopencore-amrwb-dev libvo-amrwbenc-dev libvo-amrwbenc-dev libboost-dev libtiff-dev libpcap-dev libssl-dev uuid-dev flite-dev cmake git wget

Then switch to node v19, switch to your node project folder and install sip-lab:

nvm install 19
nvm use 19
cd YOUR_NODE_PROJECT_FOLDER
npm i sip-lab

Obs: once you install sip-lab, you can switch to other node versions like v20, v21.

Notes:

  • Build will not work on Debian 10 as cmake version is older than required.
  • It will work on Debian 12 but a build process will be required. But you need to build using node v19 or older. But building with node v20 or v21 will fail (#107). But once you have it built, you can switch to a newer version of node.

So basically, if you stick with Debian 11 and uses node v19, istallation should be smooth.

Basic Usage

The following example demonstrates how to create two SIP endpoints, make a call between them, and then terminate the call.

// This test creates 2 UDP SIP endpoints, makes a call between them and disconeects.

const sip = require ('sip-lab')
const Zeq = require('@mayama/zeq')
const m = require('data-matching')
const sip_msg = require('sip-matching')

// here we create our Zeq instance
var z = new Zeq()


async function test() {
    // here we set our Zeq instance to trap events generated by sip-lab event_source
    z.trap_events(sip.event_source, 'event', (evt) => {
        var e = evt.args[0]
        return e
    })

    sip.set_codecs("pcmu/8000/1:128,pcma/8000/1:128,gsm/8000/1:128")

    // here we start sip-lab
    console.log(sip.start((data) => { console.log(data)} ))

    // Here we create the SIP endpoints (transports).
    // Since we don't specify the port, an available port will be allocated.
    // Since we don't specify the type ('udp' or 'tcp' or 'tls'), 'udp' will be used by default.
    const t1 = sip.transport.create({address: "127.0.0.1"})
    const t2 = sip.transport.create({address: "127.0.0.1"})

    // here we just print the transports
    console.log("t1", t1)
    console.log("t2", t2)

    // make the call from t1 to t2 with some custom heaaders
    const oc = sip.call.create(t1.id, {
        from_uri: 'sip:alice@test.com',
        to_uri: `sip:bob@${t2.address}:${t2.port}`,
        headers: {
            'X-MyHeader1': 'abc',
            'X-MyHeader2': 'def',
        },
    })

    // Here we will wait for the call to arrive at t2 with the custom headers
    // We will also get a '100 Trying' that is sent by sip-lab automatically
    // We will wait for at most 1000ms. If all events don't arrive within 1000ms, an exception will be thrown and the test will fail due to timeout.
    await z.wait([
        {
            event: "incoming_call",
            call_id: m.collect("call_id"),
            transport_id: t2.id,
            msg: sip_msg({
                $rU: 'bob',
                $fU: 'alice',
                $tU: 'bob',
                $fd: 'test.com',
                '$hdr(X-MyHeader1)': 'abc',
                hdr_x_myheader2: 'def',
            })
        },
        {
            event: 'response',
            call_id: oc.id,
            method: 'INVITE',
            msg: sip_msg({
                $rs: '100',
                $rr: 'Trying',
                '$(hdrcnt(via))': 1,
                '$hdr(call-id)': m.collect('sip_call_id'),
                $fU: 'alice',
                $fd: 'test.com',
                $tU: 'bob',
                '$hdr(l)': '0',
            }),
        },
    ], 1000)
    // Details about zeq wait(list_of_events_to_wait_for, timeout_in_ms):
    // The order of events in the list is irrelevant.
    // What matters is that all events arrive within the specified timeout.
    // When specifying events, you can be as detailed or succinct as you need.
    // For example, the above event 'response' is waiting for a SIP '100 Trying' to arrive,
    // but we are specifying several things to match just to show that we can be very detailed when performing a match.
    // But it could have been just like this:
    //
    //  {
    //      event: 'response',
    //      call_id: oc.id,
    //      method: 'INVITE',
    //      msg: sip_msg({
    //          $rs: '100',
    //      }),
    //  }
    // Regarding the function sip_msg(), this is a special matching function provided by https://github.com/MayamaTakeshi/sip-matching that makes it 
    // easy to match a SIP message using openser/kamailio/opensips pseudo-variables syntax.
 

    // Here we store data for the incoming call
    // just to organize our code (not really needed)
    const ic = {
        id: z.store.call_id,
        sip_call_id: z.store.sip_call_id,
    }

    // Now we answer the call at t2 side sending custom headers.
    sip.call.respond(ic.id, {
        code: 200,
        reason: 'OK',
        headers: {
            'X-MyHeader3': 'ABC',
            'X-MyHeader4': 'DEF',
        },
    })

    // Then we wait for the '200 OK' at the t1 side with the custom headers.
    // We will also get event 'media_update' for both sides indicating media streams (RTP) were set up successfully
    await z.wait([
        {
            event: 'response',
            call_id: oc.id,
            method: 'INVITE',
            msg: sip_msg({
                $rs: '200',
                $rr: 'OK',
                '$(hdrcnt(VIA))': 1,
                $fU: 'alice',
                $fd: 'test.com',
                $tU: 'bob',
                '$hdr(content-type)': 'application/sdp',
                $rb: '!{_}a=sendrecv',
                '$hdr(X-MyHeader3)': 'ABC',
                hdr_x_myheader4: 'DEF',
            }),
        },
        {
            event: 'media_update',
            call_id: oc.id,
            status: 'ok',
        },
        {
            event: 'media_update',
            call_id: ic.id,
            status: 'ok',
        },
    ], 1000)

    // now we terminate the call from t1 side
    sip.call.terminate(oc.id)

    // and wait for termination events
    await z.wait([
        {
            event: 'response',
            call_id: oc.id,
            method: 'BYE',
            msg: sip_msg({
                $rs: '200',
                $rr: 'OK',
            }),
        },
        {
            event: 'call_ended',
            call_id: oc.id,
        },
        {
            event: 'call_ended',
            call_id: ic.id,
        },
    ], 1000)

    console.log("Success")

    sip.stop()
    process.exit(0)
}


test()
.catch(e => {
    console.error(e)
    process.exit(1)
})

Notice that you can specify matching of SIP headers and other elements using opensips/kamailio/openser pseudo-variables syntax (courtesy of sip-matching )

For more examples look into folder samples.

API Reference

sip Module

start(callback)

Starts the sip-lab module.

  • callback (optional): A function to be called with log data.

stop(cleanup)

Stops the sip-lab module.

  • cleanup (optional): A boolean indicating whether to terminate all active calls, registrations, and subscriptions.

set_log_level(level)

Sets the log level for the module.

  • level: An integer representing the log level.

set_codecs(codecs)

Sets the enabled codecs for media streams.

  • codecs: A string containing a comma-separated list of codec definitions.

dtmf_aggregation_on(timeout)

Enables DTMF aggregation with a specified timeout. This means instead of firing a 'digits' event every time they are received, we will aggregated them and fire a single event upon timeout.

  • timeout: The timeout in milliseconds.

disable_telephone_event()

Disables the telephone-event codec.

sip.transport

create(options)

Creates a new SIP transport.

  • options: An object with the following properties:
    • address: The IP address to bind to.
    • port (optional): The port to bind to. If not specified, an available port will be used.
    • type (optional): The transport type ('udp', 'tcp', or 'tls'). Defaults to 'udp'.
    • cert_file (optional): The path to the TLS certificate file.
    • key_file (optional): The path to the TLS key file.

sip.call

create(transport_id, options)

Creates a new outgoing call.

  • transport_id: The ID of the transport to use for the call.
  • options: An object with the following properties:
    • from_uri: The From URI for the call.
    • to_uri: The To URI for the call.
    • headers (optional): An object containing custom SIP headers.
    • media (optional): A string or array specifying the media streams to offer.
    • delayed_media (optional): A boolean indicating whether to use delayed media negotiation.

respond(call_id, options)

Responds to an incoming call.

  • call_id: The ID of the call to respond to.
  • options: An object with the following properties:
    • code: The SIP response code.
    • reason: The SIP reason phrase.
    • headers (optional): An object containing custom SIP headers.
    • media (optional): A string or array specifying the media streams to answer with.

terminate(call_id)

Terminates a call.

  • call_id: The ID of the call to terminate.

reinvite(call_id, options)

Sends a re-INVITE request.

  • call_id: The ID of the call to re-invite.
  • options (optional): An object with the following properties:
    • media (optional): A string or array specifying the new media streams.

update(call_id, options)

Sends an UPDATE request.

  • call_id: The ID of the call to update.
  • options: An object with the following properties:
    • headers (optional): An object containing custom SIP headers.

send_dtmf(call_id, options)

Sends DTMF tones.

  • call_id: The ID of the call to send DTMF to.
  • options: An object with the following properties:
    • digits: The DTMF digits to send.
    • mode: The DTMF mode (0 for RFC2833, 1 for in-band).

start_inband_dtmf_detection(call_id, options)

Starts in-band DTMF detection.

  • call_id: The ID of the call to start detection on.
  • options (optional): An object with the following properties:
    • media_id: The ID of the media stream to start detection on.

start_play_wav(call_id, options)

Starts playing a WAV file.

  • call_id: The ID of the call to play the WAV file on.
  • options: An object with the following properties:
    • file: The path to the WAV file.
    • end_of_file_event (optional): A boolean indicating whether to emit an end_of_file event.
    • no_loop (optional): A boolean indicating whether to loop the WAV file.

stop_play_wav(call_id)

Stops playing a WAV file.

  • call_id: The ID of the call to stop playing on.

start_record_wav(call_id, options)

Starts recording audio to a WAV file.

  • call_id: The ID of the call to record.
  • options: An object with the following properties:
    • file: The path to the WAV file to record to.

stop_record_wav(call_id)

Stops recording audio.

  • call_id: The ID of the call to stop recording on.

start_speech_recog(call_id)

Starts speech recognition.

  • call_id: The ID of the call to start recognition on.

stop_speech_recog(call_id)

Stops speech recognition.

  • call_id: The ID of the call to stop recognition on.

start_speech_synth(call_id, options)

Starts speech synthesis.

  • call_id: The ID of the call to start synthesis on.
  • options: An object with the following properties:
    • voice: The voice to use for synthesis.
    • text: The text to synthesize.

stop_speech_synth(call_id)

Stops speech synthesis.

  • call_id: The ID of the call to stop synthesis on.

start_fax(call_id, options)

Starts a fax session.

  • call_id: The ID of the call to start the fax session on.
  • options: An object with the following properties:
    • is_sender: A boolean indicating whether this endpoint is the sender.
    • file: The path to the TIFF file to send or receive.
    • transmit_on_idle: A boolean indicating whether to transmit on idle.

send_bfsk(call_id, options)

Sends BFSK bits.

  • call_id: The ID of the call to send BFSK on.
  • options: An object with the following properties:
    • bits: The bits to send.
    • freq_zero: The frequency for the '0' bit.
    • freq_one: The frequency for the '1' bit.

start_bfsk_detection(call_id, options)

Starts BFSK detection.

  • call_id: The ID of the call to start detection on.
  • options: An object with the following properties:
    • freq_zero: The frequency for the '0' bit.
    • freq_one: The frequency for the '1' bit.

send_mrcp_msg(call_id, options)

Sends an MRCP message.

  • call_id: The ID of the call to send the message on.
  • options: An object with the following properties:
    • msg: The MRCP message to send.

sip.account

create(transport_id, options)

Creates a new SIP account.

  • transport_id: The ID of the transport to use for the account.
  • options: An object with the following properties:
    • domain: The SIP domain.
    • server: The SIP server address.
    • username: The username for authentication.
    • password: The password for authentication.
    • headers (optional): An object containing custom SIP headers.
    • expires (optional): The registration expiration time in seconds.

register(account, options)

Registers a SIP account.

  • account: The account object to register.
  • options (optional): An object with the following properties:
    • auto_refresh: A boolean indicating whether to automatically refresh the registration.

unregister(account)

Unregisters a SIP account.

  • account: The account object to unregister.

sip.subscription

create(transport_id, options)

Creates a new SIP subscription.

  • transport_id: The ID of the transport to use for the subscription.
  • options: An object with the following properties:
    • event: The event to subscribe to.
    • accept: The accepted content type.
    • from_uri: The From URI for the subscription.
    • to_uri: The To URI for the subscription.
    • request_uri: The request URI for the subscription.
    • proxy_uri: The proxy URI for the subscription.
    • auth (optional): An object containing authentication details.

subscribe(subscription, options)

Subscribes to an event.

  • subscription: The subscription object.
  • options (optional): An object with the following properties:
    • expires: The subscription expiration time in seconds.

sip.request

create(transport_id, options)

Creates a new non-dialog SIP request.

  • transport_id: The ID of the transport to use for the request.
  • options: An object with the following properties:
    • method: The SIP method for the request.
    • from_uri: The From URI for the request.
    • to_uri: The To URI for the request.
    • request_uri: The request URI for the request.
    • headers (optional): An object containing custom SIP headers.

respond(request_id, options)

Responds to a non-dialog SIP request.

  • request_id: The ID of the request to respond to.
  • options: An object with the following properties:
    • code: The SIP response code.
    • reason: The SIP reason phrase.
    • headers (optional): An object containing custom SIP headers.

Events

sip-lab emits various events that can be trapped and used for test automation.

  • incoming_call: Fired when a new call is received.
  • response: Fired when a SIP response is received.
  • call_ended: Fired when a call is terminated.
  • media_update: Fired when the media status of a call is updated.
  • dtmf: Fired when DTMF tones are received.
  • bfsk: Fired when BFSK bits are received.
  • fax_result: Fired when a fax transmission is complete.
  • mrcp_msg: Fired when an MRCP message is received.
  • speech: Fired when speech is recognized.
  • speech_synth_complete: Fired when speech synthesis is complete.
  • end_of_file: Fired when a WAV file has finished playing.
  • registration_status: Fired when the status of a registration changes.
  • non_dialog_request: Fired when a non-dialog SIP request is received.
  • request: Fired when a dialog-related SIP request is received.
  • reinvite: Fired when a re-INVITE request is received.