Real-Time Text (RFC 4103)

Tip

PJSUA-LIB readers — symbol equivalents are listed at the bottom of this page.

Overview

Real-time text (RTT) is a media stream that carries text characters character-by-character as the sender types, so the recipient sees each keystroke as it is entered rather than waiting for a full message. The transport is RTP, negotiated in SDP alongside the call’s audio and (optionally) video; the wire format is RFC 4103 (which uses the T.140 character set wrapped in RFC 2198 redundancy).

Why it matters

RTT is the standard accessibility-compliant text channel for SIP — the modern equivalent of a TTY for hearing- or speech-impaired users. The US FCC and EU accessibility directives mandate RTT support in “advanced communication services”, so applications that target those markets need it.

RTT vs SIP MESSAGE / instant messaging

PJSIP supports two distinct mechanisms for text in a call. They solve different problems and use entirely different transports — pick the one that matches your use case.

Real-time text (this guide)

SIP MESSAGE / instant messaging

RTP media stream — m=text line in SDP

SIP MESSAGE method — no media stream

Live, character-by-character

Discrete, complete messages

Always tied to a call

Inside a call (in-dialog) or outside a call (out-of-dialog)

Accessibility — TTY equivalent

“Chat in the call” or presence-style messaging

Call::sendText() / Call::onCallRxText()

Call::sendInstantMessage() (in-dialog) Buddy::sendInstantMessage() (out-of-dialog)

For instant messaging (SIP MESSAGE), see the Instant Messaging (IM) sections of Calls and Presence and Instant Messaging.

Build prerequisites

Real-time text is built unconditionally with the rest of PJMEDIA — no per-feature build flag. Available in PJSIP 2.16 and later (#4344).

The maximum RFC 2198 redundancy level used at runtime is capped at PJMEDIA_TXT_STREAM_MAX_RED_LEVELS (default 2), set at compile time in config_site.h. The default of 2 covers RFC 4103’s recommendation; raise it only if your deployment has truly unusual loss characteristics.

Negotiating a text stream

A call carries a text stream when the call setting includes one. Set CallSetting::textCount to 1 (or higher for multiple text streams; rare) before issuing the offer or answer:

try {
    CallOpParam prm(true);  // true = use default CallSetting
    prm.opt.audioCount = 1;
    prm.opt.videoCount = 0;
    prm.opt.textCount  = 1;

    call.makeCall("sip:peer@example.com", prm);
} catch(Error& err) {
}

The result is an additional m=text line in the offered SDP with two rtpmap entries — red/1000 for the RFC 2198 redundancy codec and t140/1000 for the underlying T.140 payload, as specified in RFC 4103. If the peer accepts, the negotiated text stream is created and the application can immediately send and receive on it.

To accept incoming calls that offer a text stream, leave CallSetting::textCount at its default of 1 — the incoming text stream is negotiated automatically and reported in onCallMediaState. Set it to 0 to refuse text-stream offers.

Sending text

Text is sent through pj::Call::sendText() taking a pj::CallSendTextParam. The typical pattern is to call it from the application’s keystroke handler:

void onKeyPressed(const std::string &ch)
{
    try {
        CallSendTextParam param;
        param.text = ch;   // typically a single character or short run
        call.sendText(param);
    } catch(Error& err) {
    }
}

The library handles RFC 4103 packetisation, the inter-keystroke buffering window, and RFC 2198 redundancy transparently — pass each character (or short run of characters) to sendText() as the user types.

You can also call sendText() with a longer string when text arrives in larger chunks (paste, autocomplete acceptance, voice-to-text output). The library will fragment it into RFC 4103 packets.

The CallSendTextParam::medIdx field selects which text stream to send to when the call has more than one. Default -1 selects the first text stream; in single-text-stream calls (the common case) you can leave it alone.

Receiving text

Incoming text is delivered through pj::Call::onCallRxText() on the application’s Call subclass:

class MyCall : public Call
{
public:
    using Call::Call;

    virtual void onCallRxText(OnCallRxTextParam &prm) override
    {
        // prm.seq — RTP sequence number for this text block
        // prm.ts  — RTP timestamp
        // prm.text — the decoded text (UTF-8); may be empty
        if (!prm.text.empty())
            appendToTranscript(prm.text);
    }
};

The callback fires once per received text block, after the receive buffer / jitter window has been drained. The library handles the RFC 2198 redundancy decoding and discards duplicates by RTP sequence number — the application sees each character at most once. Text may legitimately be empty (the header explicitly notes “the text can be empty”); guard for that.

T.140 control characters in the received bytes are passed through verbatim — the application is responsible for any UI-level handling (e.g. interpreting backspace).

Redundancy level (RFC 4103 / RFC 2198)

The RFC 2198 redundancy mechanism prepends each outgoing text packet with copies of the previous N text blocks, so a packet loss of up to N consecutive packets can be recovered by the receiver from a later packet. Trade-off: each redundancy level adds packet bytes for every text block sent.

The level is configured per account on pj::AccountTextConfig::redundancyLevel, accessed as AccountConfig::textConfig.redundancyLevel:

AccountConfig acc_cfg;
// ... other configuration ...
acc_cfg.textConfig.redundancyLevel = 2;   // default; 0 disables

MyAccount *acc = new MyAccount;
acc->create(acc_cfg);

Practical guidance:

  • 0 — no redundancy. Lowest bandwidth; lowest tolerance to loss.

  • 1 — adequate against an average packet loss of up to ~50 %.

  • 2 — default; tolerates ~66.7 % loss. Recommended by RFC 4103.

The level actually used on the wire is the lower of the local and remote levels after SDP negotiation, so configuring 2 locally is safe even if the peer caps at 1.

The cap is PJMEDIA_TXT_STREAM_MAX_RED_LEVELS (default 2); raise it in config_site.h and rebuild if you genuinely need higher.

Sample applications

The pjsua console sample at pjsip-apps/src/pjsua exposes text-call commands; use it to verify a build supports RTT end-to-end and as a reference for how the API is wired. The PJMEDIA-level implementation lives in pjmedia/src/pjmedia/txt_stream.c and the PJSUA-LIB integration in pjsip/src/pjsua-lib/pjsua_txt.c.

PJSUA-LIB equivalents

PJSUA2

PJSUA-LIB

CallSetting::textCount

pjsua_call_setting::txt_cnt

pj::Call::sendText() / CallSendTextParam{medIdx, text}

pjsua_call_send_text() / pjsua_call_send_text_param

pj::Call::onCallRxText() / OnCallRxTextParam{seq, ts, text}

pjsua_callback::on_call_rx_text / pjsua_txt_stream_data

AccountConfig::textConfig.redundancyLevel

pjsua_acc_config::txt_red_level