xphone

package module
v0.5.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2026 License: MIT Imports: 24 Imported by: 0

README

xphone

Go Reference Go Report Card CI Go Version

A Go library for SIP calling and RTP media. Register with a SIP trunk or PBX, or accept calls directly as a SIP server — and get decoded PCM audio frames through Go channels.

Also available in Rust.

Table of Contents


Status — Beta

xphone is in active development and used in internal production workloads. APIs may change between minor versions. If you're evaluating, start with the Quick Start below or the demos repo.


Scope and limitations

xphone is a voice data-plane library — SIP signaling and RTP media. It is not a telephony platform.

You are responsible for:

  • Billing, number provisioning, and call routing rules
  • Recording storage and playback infrastructure
  • High availability, persistence, and failover
  • Rate limiting, authentication, and abuse prevention at the application level

Security boundaries:

  • SRTP uses SDES key exchange only. DTLS-SRTP is not supported — xphone cannot interop with WebRTC endpoints that require it.
  • TLS is supported for SIP transport. See Configuration for transport options.
  • There is no built-in authentication layer for your application — xphone authenticates to SIP servers, not your end users.

Codec constraints:

  • Opus and G.729 require CGO and external C libraries. The default build is CGO-free (G.711, G.722 only).
  • PCM sample rate is fixed at 8 kHz (narrowband) or 16 kHz (G.722 wideband). There is no configurable sample rate.

Tested against

Category Tested with
SIP trunks Telnyx, Twilio SIP, VoIP.ms, Vonage
PBXes Asterisk, FreeSWITCH, 3CX
Integration tests fakepbx (in-process, no Docker) + xpbx (Dockerized Asterisk) in CI
Unit tests MockPhone & MockCall — full Phone/Call interface mocks

This is not a comprehensive compatibility matrix. If you hit issues with a provider or PBX not listed here, please open an issue.


Use cases

  • AI voice agents — pipe call audio directly into your STT/LLM/TTS pipeline without a telephony platform
  • Softphones and click-to-call — embed SIP calling into any Go application against a trunk or PBX
  • Call recording and monitoring — tap the PCM audio stream for transcription, analysis, or storage
  • Outbound dialers — programmatic dialing with DTMF detection for IVR automation
  • Unit-testable call flows — MockPhone and MockCall let you test every call branch without a SIP server

Quick Start

Install
go get github.com/x-phone/xphone-go

Requires Go 1.23+.

Receive calls
package main

import (
    "context"
    "fmt"
    "log"

    xphone "github.com/x-phone/xphone-go"
)

func main() {
    phone := xphone.New(
        xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
        xphone.WithRTPPorts(10000, 20000),
    )

    phone.OnRegistered(func() {
        fmt.Println("Registered -- ready to receive calls")
    })

    phone.OnIncoming(func(call xphone.Call) {
        fmt.Printf("Incoming call from %s\n", call.From())
        call.Accept()

        go func() {
            for frame := range call.PCMReader() {
                // frame is []int16, mono, 8000 Hz, 160 samples (20ms)
                transcribe(frame)
            }
        }()
    })

    if err := phone.Connect(context.Background()); err != nil {
        log.Fatal(err)
    }

    select {}
}

PCM format: []int16, mono, 8000 Hz, 160 samples per frame (20ms) — the standard input format for most speech-to-text APIs.

Make an outbound call
call, err := phone.Dial(ctx, "+15551234567",
    xphone.WithEarlyMedia(),
    xphone.WithDialTimeout(30 * time.Second),
)
if err != nil {
    log.Fatal(err)
}

go func() {
    for frame := range call.PCMReader() {
        processAudio(frame)
    }
}()

Dial accepts a full SIP URI ("sip:1002@pbx.example.com") or just the user part ("1002"), in which case your configured SIP server is used.


Connection Modes

xphone supports two ways to connect to the SIP world. Both produce the same Call interface — accept, end, DTMF, PCMReader/Writer are identical.

Phone mode (SIP client)

Registers with a SIP server like a normal endpoint. Use this with SIP trunks (Telnyx, Vonage), PBXes (Asterisk, FreeSWITCH), or any SIP registrar. No PBX is required — you can register directly with a SIP trunk provider:

phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
)
phone.OnIncoming(func(call xphone.Call) { call.Accept() })
phone.Connect(ctx)
Server mode (SIP trunk)

Accepts and places calls directly with trusted SIP peers — no registration required. Use this when trunk providers send INVITEs to your public IP, or when a PBX routes calls to your application:

server := xphone.NewServer(xphone.ServerConfig{
    Listen:     "0.0.0.0:5080",
    RTPPortMin: 10000,
    RTPPortMax: 20000,
    RTPAddress: "203.0.113.1",  // your public IP
    Peers: []xphone.PeerConfig{
        {Name: "twilio", Hosts: []string{"54.172.60.0/30", "54.244.51.0/30"}},
        {Name: "office-pbx", Host: "192.168.1.10"},
    },
})
server.OnIncoming(func(call xphone.Call) { call.Accept() })
server.Listen(ctx)

Peers are authenticated by IP/CIDR or SIP digest auth. Per-peer codec and RTP address overrides are supported.

For zero-downtime deploys, use ServerConfig.Listener with a pre-bound socket (e.g., with SO_REUSEPORT):

conn, _ := net.ListenPacket("udp4", "0.0.0.0:5080")
// socket2 can set SO_REUSEPORT before binding
server := xphone.NewServer(xphone.ServerConfig{
    Listener: conn,
    // ...
})
server.Listen(ctx)

Which mode? Use Phone when you register to a SIP server (most setups). Use Server when SIP peers send INVITEs directly to your application (Twilio SIP Trunk, direct PBX routing, peer-to-peer).


Working with Audio

xphone exposes audio as a stream of PCM frames through Go channels.

Frame format
Property Value
Encoding 16-bit signed PCM
Channels Mono
Sample rate 8000 Hz
Samples per frame 160
Frame duration 20ms
Reading inbound audio

call.PCMReader() returns a <-chan []int16. Each receive gives you one 20ms frame of decoded audio from the remote party:

go func() {
    for frame := range call.PCMReader() {
        sendToSTT(frame)
    }
    // channel closes when the call ends
}()

Important: Read frames promptly. The inbound buffer holds 256 frames (~5 seconds). If you fall behind, the oldest frames are silently dropped.

Writing outbound audio

call.PCMWriter() returns a chan<- []int16. Send one 20ms frame at a time:

go func() {
    ticker := time.NewTicker(20 * time.Millisecond)
    defer ticker.Stop()
    for range ticker.C {
        frame := getNextTTSFrame() // []int16, 160 samples
        select {
        case call.PCMWriter() <- frame:
        default:
            // outbound buffer full -- frame dropped, keep going
        }
    }
}()

Important: PCMWriter() sends each buffer as an RTP packet immediately — the caller must provide frames at real-time rate (one 160-sample frame every 20ms). For TTS or file playback, use PacedPCMWriter() instead.

Paced writer (for TTS / pre-generated audio)

call.PacedPCMWriter() accepts arbitrary-length PCM buffers and handles framing + pacing internally:

ttsAudio := elevenLabs.Synthesize("Hello, how can I help you?")
call.PacedPCMWriter() <- ttsAudio
Raw RTP access

For lower-level control — pre-encoded audio, custom codecs, or RTP header inspection:

go func() {
    for pkt := range call.RTPReader() {
        processRTP(pkt) // *rtp.Packet (pion/rtp)
    }
}()

call.RTPWriter() <- myRTPPacket

RTPWriter and PCMWriter are mutually exclusive — if you write to RTPWriter, PCMWriter is ignored for that call.

Converting to float32
func pcmToFloat32(frame []int16) []float32 {
    out := make([]float32, len(frame))
    for i, s := range frame {
        out[i] = float32(s) / 32768.0
    }
    return out
}

Features

Calling — stable
  • SIP registration with auto-reconnect and keepalive
  • Inbound and outbound calls
  • Hold / resume (re-INVITE)
  • Blind transfer (REFER) and attended transfer (REFER with Replaces, RFC 3891)
  • Call waiting (Phone.Calls() API)
  • Session timers (RFC 4028)
  • Mute / unmute
  • 302 redirect following
  • Early media (183 Session Progress)
  • ReplaceAudioWriter — atomic audio source swap (e.g., music on hold)
  • Outbound proxy routing (WithOutboundProxy)
  • Separate outbound credentials (WithOutboundCredentials)
  • Custom headers on outbound INVITEs (WithHeader, DialOptions.CustomHeaders)
  • Server.DialURI — dial arbitrary SIP URIs without pre-configured peers
  • Transfer failure surfaced via EndedByTransferFailed end reason
DTMF — stable
  • RFC 4733 (RTP telephone-events)
  • SIP INFO (RFC 2976)
Audio codecs — stable
  • G.711 u-law (PCMU), G.711 A-law (PCMA) — built-in
  • G.722 wideband — built-in
  • Opus — optional, requires CGO + libopus (-tags opus)
  • G.729 — optional, requires CGO + bcg729 (-tags g729)
  • Jitter buffer
Video — newer, less production mileage
  • H.264 (RFC 6184) and VP8 (RFC 7741)
  • Depacketizer/packetizer pipeline
  • Mid-call video upgrade/downgrade (re-INVITE)
  • VideoReader / VideoWriter / VideoRTPReader / VideoRTPWriter
  • RTCP PLI/FIR for keyframe requests
Security — stable
  • SRTP (AES_CM_128_HMAC_SHA1_80) with SDES key exchange
  • SRTP replay protection (RFC 3711)
  • SRTCP encryption (RFC 3711 §3.4)
  • Key material zeroization
  • Separate SRTP contexts for audio and video
Network — stable
  • TCP and TLS SIP transport
  • STUN NAT traversal (RFC 5389)
  • TURN relay for symmetric NAT (RFC 5766)
  • ICE-Lite (RFC 8445 §2.2)
  • RTCP Sender/Receiver Reports (RFC 3550)
Messaging — newer, less production mileage
  • SIP MESSAGE (RFC 3428)
  • SIP SUBSCRIBE/NOTIFY (RFC 6665)
  • MWI / voicemail notification (RFC 3842)
  • BLF / Busy Lamp Field monitoring
  • SIP Presence (RFC 3856)
Testing — stable
  • MockPhone and MockCall — full interface mocks for unit testing

Call States

Idle -> Ringing (inbound) or Dialing (outbound)
     -> RemoteRinging -> Active <-> OnHold -> Ended
call.OnState(func(state xphone.CallState) {
    fmt.Printf("State: %v\n", state)
})

call.OnEnded(func(reason xphone.EndReason) {
    fmt.Printf("Ended: %v\n", reason)
})

Call Control

call.Hold()
call.Resume()

call.BlindTransfer("sip:1003@pbx.example.com")
callA.AttendedTransfer(callB)

call.Mute()
call.Unmute()

call.SendDTMF("5")
call.OnDTMF(func(digit string) {
    fmt.Printf("Received: %s\n", digit)
})

// Mid-call video upgrade
call.AddVideo(xphone.VideoCodecH264, xphone.VideoCodecVP8)
call.OnVideoRequest(func(req *xphone.VideoUpgradeRequest) {
    req.Accept()
})
call.OnVideo(func() {
    // read frames from call.VideoReader()
})

phone.SendMessage(ctx, "sip:1002@pbx", "Hello!")

Media Pipeline

Audio
Inbound:
  SIP Trunk -> RTP/UDP -> RTPRawReader (pre-jitter)
                        -> Jitter Buffer -> RTPReader (post-jitter)
                                          -> Codec Decode -> PCMReader ([]int16)

Outbound (mutually exclusive):
  PCMWriter      -> Codec Encode -> RTP/UDP -> SIP Trunk   (caller paces at 20ms)
  PacedPCMWriter -> Auto-frame + 20ms ticker -> Codec Encode -> RTP/UDP -> SIP Trunk
  RTPWriter      -> RTP/UDP -> SIP Trunk   (raw mode — PCMWriter ignored)
Video
Inbound:
  SIP Trunk -> RTP/UDP -> Depacketizer (H.264/VP8) -> VideoReader (NAL units / frames)
                        -> VideoRTPReader (raw video RTP packets)

Outbound (mutually exclusive):
  VideoWriter -> Packetizer (H.264/VP8) -> RTP/UDP -> SIP Trunk
  VideoRTPWriter -> RTP/UDP -> SIP Trunk   (raw mode)

Video uses a separate RTP port and independent SRTP contexts. RTCP PLI/FIR requests trigger keyframe generation on the sender side.

All channels are buffered (256 entries). Inbound taps drop oldest on overflow; outbound writers drop newest. Audio frames are 160 samples at 8000 Hz = 20ms. Video frames carry codec-specific NAL units (H.264) or encoded frames (VP8).


Configuration

phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "pbx.example.com"),
    xphone.WithTransport("udp", nil),                      // "udp" | "tcp" | "tls"
    xphone.WithRTPPorts(10000, 20000),                      // RTP port range
    xphone.WithCodecs(xphone.CodecOpus, xphone.CodecPCMU),  // codec preference
    xphone.WithJitterBuffer(50 * time.Millisecond),
    xphone.WithMediaTimeout(30 * time.Second),
    xphone.WithNATKeepalive(25 * time.Second),
    xphone.WithStunServer("stun.l.google.com:19302"),
    xphone.WithSRTP(),
    xphone.WithDtmfMode(xphone.DtmfModeRFC4733),           // or DtmfModeSIPInfo
    xphone.WithICE(true),                                   // ICE-Lite
    xphone.WithTurnServer("turn.example.com:3478"),
    xphone.WithTurnCredentials("user", "pass"),
    xphone.WithOutboundProxy("sip:proxy.example.com:5060"),   // route INVITEs via proxy
    xphone.WithOutboundCredentials("trunk-user", "trunk-pass"), // separate INVITE auth
    xphone.WithLogger(slog.Default()),
)

See pkg.go.dev for all options.


RTP Port Range

Each active call requires an even-numbered UDP port for RTP audio. Configure an explicit range for production deployments behind firewalls:

phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
    xphone.WithRTPPorts(10000, 20000),
)

Only even ports are used (per RTP spec). Maximum concurrent audio-only calls = (max - min) / 2.

Range Even ports Max concurrent calls
10000–10100 50 ~50
10000–12000 1000 ~1000
10000–20000 5000 ~5000

When ports run out: inbound calls receive a 500 Internal Server Error and outbound dials fail with an error. Widen the range before investigating SIP server configuration.

If WithRTPPorts is not called, the OS assigns ephemeral ports. This works for development but is impractical in production where firewall rules need a known range.


NAT Traversal

STUN (most deployments)

Discovers your public IP via a STUN Binding Request:

phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
    xphone.WithStunServer("stun.l.google.com:19302"),
)
TURN (symmetric NAT)

For environments where STUN alone fails (cloud VMs, corporate firewalls):

phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
    xphone.WithTurnServer("turn.example.com:3478"),
    xphone.WithTurnCredentials("user", "pass"),
)
ICE-Lite

SDP-level candidate negotiation (RFC 8445 §2.2):

phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
    xphone.WithICE(true),
    xphone.WithStunServer("stun.l.google.com:19302"),
)

Only enable STUN/TURN/ICE when the SIP server is on the public internet. Do not enable it when connecting via VPN or private network.


Opus Codec

Opus is optional and requires CGO + libopus. The default build is CGO-free.

Install libopus
# Debian / Ubuntu
sudo apt-get install libopus-dev libopusfile-dev

# macOS
brew install opus opusfile
Build with Opus
go build -tags opus ./...
go test -tags opus ./...
Usage
phone := xphone.New(
    xphone.WithCredentials("1001", "secret", "sip.telnyx.com"),
    xphone.WithCodecs(xphone.CodecOpus, xphone.CodecPCMU),
)

Opus runs at 8kHz natively — no resampling needed. PCM frames remain []int16, mono, 160 samples (20ms). RTP timestamps use 48kHz clock per RFC 7587.

Without the opus build tag, CodecOpus is accepted in configuration but will not be negotiated.


Testing

Unit tests with mocks

MockPhone and MockCall implement the Phone and Call interfaces:

phone := xphone.NewMockPhone()
phone.Connect(context.Background())

phone.OnIncoming(func(c xphone.Call) {
    c.Accept()
})
phone.SimulateIncoming("sip:1001@pbx")

assert.Equal(t, xphone.StateActive, phone.LastCall().State())
call := xphone.NewMockCall()
call.Accept()
call.SendDTMF("5")
assert.Equal(t, []string{"5"}, call.SentDTMF())

call.SimulateDTMF("9")
call.InjectRTP(pkt)
Integration tests with FakePBX (no Docker)
go test -v -run TestFakePBX ./...
go test -v -run TestServerFakePBX ./...
End-to-end tests with Asterisk
cd testutil/docker && docker compose up -d
go test -tags=integration -v -count=1 ./...
cd testutil/docker && docker compose down

Example App

examples/sipcli is a terminal SIP client with registration, calls, hold, resume, DTMF, mute, transfer, echo mode, and speaker output:

cd examples/sipcli
go run . -profile myserver       # from ~/.sipcli.yaml
go run . -server pbx.example.com -user 1001 -pass secret

Logging

xphone uses Go's log/slog:

phone := xphone.New(
    xphone.WithLogger(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }))),
)

// Silence all library logs
phone := xphone.New(
    xphone.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
)

If no logger is provided, slog.Default() is used.


Stack

Layer Implementation
SIP Signaling sipgo
RTP / SRTP pion/rtp + built-in SRTP (AES_CM_128_HMAC_SHA1_80)
G.711 / G.722 Built-in (PCMU, PCMA) + gotranspile/g722
G.729 AoiEnoki/bcg729 (optional, -tags g729)
Opus hraban/opus (optional, -tags opus)
H.264 / VP8 Built-in packetizer/depacketizer (RFC 6184, RFC 7741)
RTCP Built-in (RFC 3550 SR/RR + PLI/FIR)
Jitter Buffer Built-in
STUN Built-in (RFC 5389)
TURN Built-in (RFC 5766)
ICE-Lite Built-in (RFC 8445 §2.2)
TUI (sipcli) bubbletea + lipgloss

Roadmap

  • DTLS-SRTP key exchange (WebRTC interop)
  • Full ICE (RFC 5245)

License

MIT

Documentation

Overview

Package xphone provides an event-driven SIP user agent for embedding VoIP telephony into Go applications. It manages SIP registration, call state machines, RTP media pipelines, codec encode/decode, and DTMF — exposing a concurrency-safe API for handling multiple concurrent calls.

Index

Constants

View Source
const DTMFPayloadType = 101

DTMFPayloadType is the RTP payload type for DTMF events (RFC 4733).

Variables

View Source
var (
	// ErrNotRegistered is returned when an operation requires an active SIP registration.
	ErrNotRegistered = errors.New("xphone: not registered")
	// ErrCallNotFound is returned when a call ID does not match any active call.
	ErrCallNotFound = errors.New("xphone: call not found")
	// ErrInvalidState is returned when a call operation is invalid for the current state.
	ErrInvalidState = errors.New("xphone: invalid state for operation")
	// ErrMediaTimeout is returned when no RTP packets are received within the configured timeout.
	ErrMediaTimeout = errors.New("xphone: RTP media timeout")
	// ErrDialTimeout is returned when an outbound call is not answered before the dial timeout.
	ErrDialTimeout = errors.New("xphone: dial timeout exceeded before answer")
	// ErrNoRTPPortAvailable is returned when the RTP port range is exhausted.
	ErrNoRTPPortAvailable = errors.New("xphone: RTP port range exhausted")
	// ErrRegistrationFailed is returned when SIP registration is rejected by the server.
	ErrRegistrationFailed = errors.New("xphone: registration failed")
	// ErrTransferFailed is returned when a blind transfer (REFER) is rejected.
	ErrTransferFailed = errors.New("xphone: transfer failed")
	// ErrTLSConfigRequired is returned when TLS transport is selected without providing a TLS config.
	ErrTLSConfigRequired = errors.New("xphone: TLS transport requires TLSConfig")
	// ErrInvalidDTMFDigit is returned when SendDTMF receives a digit outside 0-9, *, #, A-D.
	ErrInvalidDTMFDigit = errors.New("xphone: invalid DTMF digit")
	// ErrAlreadyMuted is returned when Mute is called on an already-muted call.
	ErrAlreadyMuted = errors.New("xphone: already muted")
	// ErrNotMuted is returned when Unmute is called on a call that is not muted.
	ErrNotMuted = errors.New("xphone: not muted")
	// ErrNoVideo is returned when a video operation is called on an audio-only call.
	ErrNoVideo = errors.New("xphone: no video stream")
	// ErrAlreadyConnected is returned when Connect is called on a phone that is already connected.
	ErrAlreadyConnected = errors.New("xphone: already connected")
	// ErrNotConnected is returned when Disconnect is called on a phone that is not connected.
	ErrNotConnected = errors.New("xphone: not connected")
	// ErrHostRequired is returned when Connect is called without a Host configured.
	ErrHostRequired = errors.New("xphone: Host is required")
	// ErrSubscriptionRejected is returned when the server permanently rejects a SUBSCRIBE.
	ErrSubscriptionRejected = errors.New("xphone: subscription rejected")
	// ErrSubscriptionNotFound is returned when UnsubscribeEvent is called with an unknown ID.
	ErrSubscriptionNotFound = errors.New("xphone: subscription not found")
	// ErrVideoAlreadyActive is returned when AddVideo is called on a call that already has video.
	ErrVideoAlreadyActive = errors.New("xphone: video already active")
	// ErrAlreadyListening is returned when Listen is called on a server that is already listening.
	ErrAlreadyListening = errors.New("xphone: server already listening")
	// ErrNotListening is returned when an operation requires the server to be listening.
	ErrNotListening = errors.New("xphone: server not listening")
	// ErrPeerNotFound is returned when a dial target references an unknown peer name.
	ErrPeerNotFound = errors.New("xphone: peer not found")
	// ErrPeerRejected is returned when an incoming request fails peer authentication.
	ErrPeerRejected = errors.New("xphone: peer authentication failed")
)

Sentinel errors returned by Phone and Call methods.

Functions

func DTMFCodeDigit

func DTMFCodeDigit(code int) string

DTMFCodeDigit returns the digit string for an RFC 4733 event code. Returns "" if the code is unknown.

func DTMFDigitCode

func DTMFDigitCode(digit string) int

DTMFDigitCode returns the RFC 4733 event code for a digit string. Returns -1 if the digit is invalid.

func EncodeDTMF

func EncodeDTMF(digit string, ts uint32, seq uint16, ssrc uint32) ([]*rtp.Packet, error)

EncodeDTMF encodes a DTMF digit into a sequence of RTP packets (RFC 4733).

func EncodeInfoDTMF added in v0.3.0

func EncodeInfoDTMF(digit string, durationMs int) (string, error)

EncodeInfoDTMF creates a SIP INFO application/dtmf-relay body. Format: "Signal=<digit>\r\nDuration=<ms>\r\n"

func ParseInfoDTMF added in v0.3.0

func ParseInfoDTMF(body string) string

ParseInfoDTMF parses a SIP INFO application/dtmf-relay body and returns the digit. Parsing is case-insensitive for the "Signal" key and tolerates whitespace. Returns "" if no valid digit is found.

Types

type AcceptOption

type AcceptOption func(*acceptOptions)

AcceptOption is a functional option for Accept().

type Call

type Call interface {
	ID() string
	CallID() string
	Direction() Direction
	From() string
	To() string
	FromName() string
	RemoteURI() string
	RemoteDID() string
	RemoteIP() string
	RemotePort() int
	State() CallState
	Codec() Codec
	LocalSDP() string
	RemoteSDP() string
	StartTime() time.Time
	Duration() time.Duration
	Header(name string) []string
	Headers() map[string][]string
	Accept(opts ...AcceptOption) error
	Reject(code int, reason string) error
	End() error
	MediaSessionActive() bool
	Hold() error
	Resume() error
	Mute() error
	Unmute() error
	MuteAudio() error
	UnmuteAudio() error
	SendDTMF(digit string) error
	BlindTransfer(target string) error
	AttendedTransfer(other Call) error
	RTPRawReader() <-chan *rtp.Packet
	RTPReader() <-chan *rtp.Packet
	RTPWriter() chan<- *rtp.Packet
	PCMReader() <-chan []int16
	PCMWriter() chan<- []int16
	PacedPCMWriter() chan<- []int16
	ReplaceAudioWriter(newSrc <-chan []int16) error
	OnDTMF(func(digit string))
	OnHold(func())
	OnResume(func())
	OnMute(func())
	OnUnmute(func())
	HasVideo() bool
	VideoCodec() VideoCodec
	VideoReader() <-chan VideoFrame
	VideoWriter() chan<- VideoFrame
	VideoRTPReader() <-chan *rtp.Packet
	VideoRTPWriter() chan<- *rtp.Packet
	MuteVideo() error
	UnmuteVideo() error
	RequestKeyframe() error
	AddVideo(codecs ...VideoCodec) error
	OnVideoRequest(func(*VideoUpgradeRequest))
	OnVideo(func())
	OnMedia(func())
	OnState(func(state CallState))
	OnEnded(func(reason EndReason))
}

Call is the public interface for an active call.

type CallState

type CallState int

CallState represents the current state of a call.

const (
	StateIdle          CallState = iota
	StateRinging                 // inbound: INVITE received, not yet accepted
	StateDialing                 // outbound: INVITE sent, no response yet
	StateRemoteRinging           // outbound: 180 received
	StateEarlyMedia              // outbound: 183 received + WithEarlyMedia set
	StateActive                  // call established, RTP flowing
	StateOnHold                  // re-INVITE with a=sendonly/inactive
	StateEnded                   // terminal
)

type Codec

type Codec int

Codec represents an audio codec.

const (
	CodecPCMU Codec = 0
	CodecPCMA Codec = 8
	CodecG722 Codec = 9
	CodecG729 Codec = 18
	CodecOpus Codec = 111
)

type Config

type Config struct {
	Username  string
	Password  string
	Host      string
	Port      int
	Transport string
	TLSConfig *tls.Config

	RegisterExpiry   time.Duration
	RegisterRetry    time.Duration
	RegisterMaxRetry int

	// OutboundProxy is the SIP URI to route outbound INVITEs through (e.g. "sip:proxy.example.com:5060").
	// When set, the initial INVITE is sent to this address instead of Host.
	// Registration still goes to Host. In-dialog requests use the route set from Record-Route.
	OutboundProxy string
	// OutboundUsername is the SIP digest username for outbound INVITE auth (401/407).
	// Falls back to Username if empty.
	OutboundUsername string
	// OutboundPassword is the SIP digest password for outbound INVITE auth (401/407).
	// Falls back to Password if empty.
	OutboundPassword string

	NATKeepaliveInterval time.Duration

	StunServer string
	SRTP       bool

	// TurnServer is the TURN server address (host:port) for relay allocation.
	// When set with TurnUsername/TurnPassword, the phone allocates a TURN relay
	// during call setup for symmetric NAT traversal.
	TurnServer   string
	TurnUsername string
	TurnPassword string

	// ICE enables ICE-Lite candidate gathering and STUN responder on the media socket.
	ICE bool

	RTPPortMin   int
	RTPPortMax   int
	CodecPrefs   []Codec
	JitterBuffer time.Duration
	MediaTimeout time.Duration
	PCMFrameSize int
	PCMRate      int
	DtmfMode     DtmfMode

	// VoicemailURI is the SIP URI to subscribe for MWI (RFC 3842).
	// Example: "sip:*97@pbx.local". When set, the phone auto-subscribes on connect.
	VoicemailURI string

	// DisplayName is included in SIP Contact headers.
	// Helps PBXes identify the device (e.g., "VoiceApp/1.0").
	DisplayName string

	Logger *slog.Logger
}

Config holds all configuration for a Phone instance.

type DTMFEvent

type DTMFEvent struct {
	Digit    string
	Duration uint16
	End      bool
	Volume   uint8
}

DTMFEvent represents a decoded DTMF event from an RTP packet.

func DecodeDTMF

func DecodeDTMF(payload []byte) *DTMFEvent

DecodeDTMF decodes a DTMF event from an RTP payload. Returns nil if the payload is less than 4 bytes.

type DialOption

type DialOption func(*DialOptions)

DialOption is a functional option for Dial().

func WithAuth added in v0.5.11

func WithAuth(username, password string) DialOption

WithAuth sets per-call SIP digest credentials for 401/407 proxy authentication challenges. Overrides phone-level WithOutboundCredentials and WithCredentials for this single dial attempt.

func WithCallerID

func WithCallerID(id string) DialOption

WithCallerID sets the caller ID for an outbound call.

func WithCodecOverride

func WithCodecOverride(codecs ...Codec) DialOption

WithCodecOverride overrides the codec preference for an outbound call.

func WithDialTimeout

func WithDialTimeout(d time.Duration) DialOption

WithDialTimeout sets the dial timeout for an outbound call.

func WithEarlyMedia

func WithEarlyMedia() DialOption

WithEarlyMedia enables early media (183 Session Progress).

func WithHeader

func WithHeader(name, value string) DialOption

WithHeader adds a custom SIP header to an outbound call.

func WithVideo added in v0.3.0

func WithVideo(codecs ...VideoCodec) DialOption

WithVideo enables video in the SDP offer. If no codecs are specified, defaults to [H264, VP8] preference order.

type DialOptions

type DialOptions struct {
	CallerID      string
	CustomHeaders map[string]string
	EarlyMedia    bool
	Timeout       time.Duration
	CodecOverride []Codec
	Video         bool         // enable video in SDP offer
	VideoCodecs   []VideoCodec // video codec preferences (default: [H264, VP8])
	AuthUsername  string       // per-call SIP digest username (overrides config)
	AuthPassword  string       // per-call SIP digest password (overrides config)
}

DialOptions holds configuration for an outbound call.

type Direction

type Direction int

Direction indicates whether a call is inbound or outbound.

const (
	DirectionInbound Direction = iota
	DirectionOutbound
)

type DtmfMode added in v0.3.0

type DtmfMode int

DtmfMode controls how DTMF digits are sent and received.

const (
	// DtmfRfc4733 sends and receives DTMF via RFC 4733 RTP telephone-event packets (default).
	DtmfRfc4733 DtmfMode = iota
	// DtmfSipInfo sends and receives DTMF via SIP INFO with application/dtmf-relay body (RFC 2976).
	DtmfSipInfo
	// DtmfBoth sends DTMF via RFC 4733 RTP; also accepts incoming SIP INFO DTMF.
	DtmfBoth
)

type EndReason

type EndReason int

EndReason describes why a call ended.

const (
	EndedByLocal          EndReason = iota // End() while Active/OnHold
	EndedByRemote                          // BYE received
	EndedByTimeout                         // MediaTimeout exceeded
	EndedByError                           // internal or transport error
	EndedByTransfer                        // REFER completed (200 OK)
	EndedByRejected                        // Reject() called
	EndedByCancelled                       // End() before 200 OK (outbound)
	EndedByTransferFailed                  // REFER rejected by remote party
)

type ExtensionState added in v0.3.0

type ExtensionState int

ExtensionState represents the monitoring state of a remote extension (BLF).

const (
	ExtensionAvailable  ExtensionState = iota // idle, no active dialogs
	ExtensionRinging                          // early/proceeding dialog
	ExtensionOnThePhone                       // confirmed dialog
	ExtensionOffline                          // not registered or subscription failed
	ExtensionUnknown                          // subscription pending or parse error
)

type MockCall

type MockCall struct {
	// contains filtered or unexported fields
}

MockCall is a mock implementation of the Call interface for testing. It provides setters for all state and simulation methods for callbacks.

func NewMockCall

func NewMockCall() *MockCall

NewMockCall creates a new MockCall with default state (Ringing, Inbound).

func (*MockCall) Accept

func (c *MockCall) Accept(opts ...AcceptOption) error

func (*MockCall) AddVideo added in v0.3.0

func (c *MockCall) AddVideo(codecs ...VideoCodec) error

func (*MockCall) AttendedTransfer added in v0.5.3

func (c *MockCall) AttendedTransfer(other Call) error

func (*MockCall) BlindTransfer

func (c *MockCall) BlindTransfer(target string) error

func (*MockCall) CallID

func (c *MockCall) CallID() string

func (*MockCall) Codec

func (c *MockCall) Codec() Codec

func (*MockCall) Direction

func (c *MockCall) Direction() Direction

func (*MockCall) Duration

func (c *MockCall) Duration() time.Duration

func (*MockCall) End

func (c *MockCall) End() error

func (*MockCall) From

func (c *MockCall) From() string

func (*MockCall) FromName

func (c *MockCall) FromName() string

func (*MockCall) HasVideo added in v0.3.0

func (c *MockCall) HasVideo() bool

func (*MockCall) Header

func (c *MockCall) Header(name string) []string

func (*MockCall) Headers

func (c *MockCall) Headers() map[string][]string

func (*MockCall) Hold

func (c *MockCall) Hold() error

func (*MockCall) ID

func (c *MockCall) ID() string

func (*MockCall) InjectRTP

func (c *MockCall) InjectRTP(pkt *rtp.Packet)

InjectRTP pushes an RTP packet onto the RTPReader channel.

func (*MockCall) LastTransferTarget

func (c *MockCall) LastTransferTarget() string

func (*MockCall) LocalSDP

func (c *MockCall) LocalSDP() string

func (*MockCall) MediaSessionActive added in v0.2.0

func (c *MockCall) MediaSessionActive() bool

func (*MockCall) Mute

func (c *MockCall) Mute() error

func (*MockCall) MuteAudio added in v0.3.0

func (c *MockCall) MuteAudio() error

func (*MockCall) MuteVideo added in v0.3.0

func (c *MockCall) MuteVideo() error

func (*MockCall) Muted

func (c *MockCall) Muted() bool

func (*MockCall) OnDTMF

func (c *MockCall) OnDTMF(fn func(string))

func (*MockCall) OnEnded

func (c *MockCall) OnEnded(fn func(EndReason))

func (*MockCall) OnHold

func (c *MockCall) OnHold(fn func())

func (*MockCall) OnMedia

func (c *MockCall) OnMedia(fn func())

func (*MockCall) OnMute

func (c *MockCall) OnMute(fn func())

func (*MockCall) OnResume

func (c *MockCall) OnResume(fn func())

func (*MockCall) OnState

func (c *MockCall) OnState(fn func(CallState))

func (*MockCall) OnUnmute

func (c *MockCall) OnUnmute(fn func())

func (*MockCall) OnVideo added in v0.3.0

func (c *MockCall) OnVideo(fn func())

func (*MockCall) OnVideoRequest added in v0.3.0

func (c *MockCall) OnVideoRequest(fn func(*VideoUpgradeRequest))

func (*MockCall) PCMReader

func (c *MockCall) PCMReader() <-chan []int16

func (*MockCall) PCMWriter

func (c *MockCall) PCMWriter() chan<- []int16

func (*MockCall) PacedPCMWriter added in v0.3.2

func (c *MockCall) PacedPCMWriter() chan<- []int16

func (*MockCall) RTPRawReader

func (c *MockCall) RTPRawReader() <-chan *rtp.Packet

func (*MockCall) RTPReader

func (c *MockCall) RTPReader() <-chan *rtp.Packet

func (*MockCall) RTPWriter

func (c *MockCall) RTPWriter() chan<- *rtp.Packet

func (*MockCall) Reject

func (c *MockCall) Reject(code int, reason string) error

func (*MockCall) RemoteDID added in v0.2.0

func (c *MockCall) RemoteDID() string

func (*MockCall) RemoteIP

func (c *MockCall) RemoteIP() string

func (*MockCall) RemotePort

func (c *MockCall) RemotePort() int

func (*MockCall) RemoteSDP

func (c *MockCall) RemoteSDP() string

func (*MockCall) RemoteURI

func (c *MockCall) RemoteURI() string

func (*MockCall) ReplaceAudioWriter added in v0.5.0

func (c *MockCall) ReplaceAudioWriter(newSrc <-chan []int16) error

func (*MockCall) RequestKeyframe added in v0.3.0

func (c *MockCall) RequestKeyframe() error

func (*MockCall) Resume

func (c *MockCall) Resume() error

func (*MockCall) SendDTMF

func (c *MockCall) SendDTMF(digit string) error

func (*MockCall) SentDTMF

func (c *MockCall) SentDTMF() []string

func (*MockCall) SetCodec

func (c *MockCall) SetCodec(codec Codec)

func (*MockCall) SetDirection

func (c *MockCall) SetDirection(d Direction)

func (*MockCall) SetHasVideo added in v0.3.0

func (c *MockCall) SetHasVideo(v bool)

SetHasVideo enables or disables video on the mock call.

func (*MockCall) SetHeader

func (c *MockCall) SetHeader(name, value string)

func (*MockCall) SetLocalSDP

func (c *MockCall) SetLocalSDP(sdp string)

func (*MockCall) SetMediaSessionActive added in v0.2.0

func (c *MockCall) SetMediaSessionActive(active bool)

func (*MockCall) SetRemoteIP

func (c *MockCall) SetRemoteIP(ip string)

func (*MockCall) SetRemotePort

func (c *MockCall) SetRemotePort(port int)

func (*MockCall) SetRemoteSDP

func (c *MockCall) SetRemoteSDP(sdp string)

func (*MockCall) SetRemoteURI

func (c *MockCall) SetRemoteURI(uri string)

func (*MockCall) SetStartTime

func (c *MockCall) SetStartTime(t time.Time)

func (*MockCall) SetState

func (c *MockCall) SetState(s CallState)

func (*MockCall) SetVideoCodec added in v0.3.0

func (c *MockCall) SetVideoCodec(vc VideoCodec)

SetVideoCodec sets the negotiated video codec.

func (*MockCall) SimulateDTMF

func (c *MockCall) SimulateDTMF(digit string)

SimulateDTMF fires both the phone-level and per-call OnDTMF callbacks.

func (*MockCall) StartTime

func (c *MockCall) StartTime() time.Time

func (*MockCall) State

func (c *MockCall) State() CallState

func (*MockCall) To

func (c *MockCall) To() string

func (*MockCall) Unmute

func (c *MockCall) Unmute() error

func (*MockCall) UnmuteAudio added in v0.3.0

func (c *MockCall) UnmuteAudio() error

func (*MockCall) UnmuteVideo added in v0.3.0

func (c *MockCall) UnmuteVideo() error

func (*MockCall) VideoCodec added in v0.3.0

func (c *MockCall) VideoCodec() VideoCodec

func (*MockCall) VideoRTPReader added in v0.3.0

func (c *MockCall) VideoRTPReader() <-chan *rtp.Packet

func (*MockCall) VideoRTPWriter added in v0.3.0

func (c *MockCall) VideoRTPWriter() chan<- *rtp.Packet

func (*MockCall) VideoReader added in v0.3.0

func (c *MockCall) VideoReader() <-chan VideoFrame

func (*MockCall) VideoWriter added in v0.3.0

func (c *MockCall) VideoWriter() chan<- VideoFrame

type MockPhone

type MockPhone struct {
	// contains filtered or unexported fields
}

MockPhone is a mock implementation of the Phone interface for testing. It simulates registration, dialing, and incoming calls without SIP transport.

func NewMockPhone

func NewMockPhone() *MockPhone

NewMockPhone creates a new MockPhone in the Disconnected state.

func (*MockPhone) AttendedTransfer added in v0.3.0

func (p *MockPhone) AttendedTransfer(callA Call, callB Call) error

func (*MockPhone) Calls added in v0.3.0

func (p *MockPhone) Calls() []Call

func (*MockPhone) Connect

func (p *MockPhone) Connect(_ context.Context) error

func (*MockPhone) Dial

func (p *MockPhone) Dial(_ context.Context, target string, opts ...DialOption) (Call, error)

func (*MockPhone) Disconnect

func (p *MockPhone) Disconnect() error

func (*MockPhone) FindCall added in v0.2.0

func (p *MockPhone) FindCall(callID string) Call

func (*MockPhone) LastCall

func (p *MockPhone) LastCall() Call

LastCall returns the most recent call (dialed or incoming).

func (*MockPhone) OnCallDTMF added in v0.2.0

func (p *MockPhone) OnCallDTMF(fn func(Call, string))

func (*MockPhone) OnCallEnded added in v0.2.0

func (p *MockPhone) OnCallEnded(fn func(Call, EndReason))

func (*MockPhone) OnCallState added in v0.2.0

func (p *MockPhone) OnCallState(fn func(Call, CallState))

func (*MockPhone) OnError

func (p *MockPhone) OnError(fn func(error))

func (*MockPhone) OnIncoming

func (p *MockPhone) OnIncoming(fn func(Call))

func (*MockPhone) OnMessage added in v0.3.0

func (p *MockPhone) OnMessage(fn func(SipMessage))

func (*MockPhone) OnRegistered

func (p *MockPhone) OnRegistered(fn func())

func (*MockPhone) OnUnregistered

func (p *MockPhone) OnUnregistered(fn func())

func (*MockPhone) OnVoicemail added in v0.3.0

func (p *MockPhone) OnVoicemail(fn func(VoicemailStatus))

func (*MockPhone) SendMessage added in v0.3.0

func (p *MockPhone) SendMessage(_ context.Context, _ string, _ string) error

func (*MockPhone) SendMessageWithType added in v0.3.0

func (p *MockPhone) SendMessageWithType(_ context.Context, _ string, _ string, _ string) error

func (*MockPhone) SimulateError

func (p *MockPhone) SimulateError(err error)

SimulateError fires the OnError callbacks with the given error.

func (*MockPhone) SimulateIncoming

func (p *MockPhone) SimulateIncoming(from string)

SimulateIncoming creates an incoming MockCall and fires the OnIncoming callback.

func (*MockPhone) SimulateMWI added in v0.3.0

func (p *MockPhone) SimulateMWI(status VoicemailStatus)

SimulateMWI fires the OnVoicemail callbacks with the given status.

func (*MockPhone) SimulateMessage added in v0.3.0

func (p *MockPhone) SimulateMessage(msg SipMessage)

SimulateMessage fires the OnMessage callbacks with the given message.

func (*MockPhone) State

func (p *MockPhone) State() PhoneState

func (*MockPhone) SubscribeEvent added in v0.3.0

func (p *MockPhone) SubscribeEvent(_ context.Context, _ string, _ string, _ int, _ func(NotifyEvent)) (string, error)

func (*MockPhone) UnsubscribeEvent added in v0.3.0

func (p *MockPhone) UnsubscribeEvent(_ string) error

func (*MockPhone) Unwatch added in v0.3.0

func (p *MockPhone) Unwatch(_ string) error

func (*MockPhone) Watch added in v0.3.0

type NotifyEvent added in v0.3.0

type NotifyEvent struct {
	Event             string   // Event header (e.g. "dialog", "presence")
	ContentType       string   // Content-Type of the NOTIFY body
	Body              string   // raw NOTIFY body
	SubscriptionState SubState // parsed Subscription-State
	Expires           int      // expires parameter from Subscription-State header
	Reason            string   // reason parameter (e.g. "deactivated", "rejected")
}

NotifyEvent is delivered by SubscribeEvent callbacks for each incoming NOTIFY.

type PeerAuthConfig added in v0.4.0

type PeerAuthConfig struct {
	Username string
	Password string
}

PeerAuthConfig holds SIP digest authentication credentials for a peer.

type PeerConfig added in v0.4.0

type PeerConfig struct {
	// Name is a human-readable identifier for this peer (e.g. "office-pbx", "twilio").
	Name string
	// Host is a single IP address for this peer. Checked during IP-based auth.
	Host string
	// Hosts is a list of IP addresses and/or CIDR ranges (e.g. "54.172.60.0/30").
	Hosts []string
	// Port is the SIP port for this peer. Default: 5060.
	Port int
	// Auth enables SIP digest authentication for this peer.
	Auth *PeerAuthConfig
	// RTPAddress overrides the server-level RTPAddress for this peer.
	// Use when this peer needs to see a different IP in SDP (e.g. different
	// network interface). Empty means use the server-level setting.
	RTPAddress string
	// Codecs restricts the codecs offered/accepted for this peer.
	// Empty means accept any codec from the server-level CodecPrefs.
	Codecs []Codec
}

PeerConfig defines a trusted SIP peer for Server mode.

type Phone

type Phone interface {
	Connect(ctx context.Context) error
	Disconnect() error
	Dial(ctx context.Context, target string, opts ...DialOption) (Call, error)
	OnIncoming(func(call Call))
	OnRegistered(func())
	OnUnregistered(func())
	OnError(func(err error))
	OnCallState(func(call Call, state CallState))
	OnCallEnded(func(call Call, reason EndReason))
	OnCallDTMF(func(call Call, digit string))
	OnVoicemail(func(status VoicemailStatus))
	OnMessage(func(msg SipMessage))
	SendMessage(ctx context.Context, target string, body string) error
	SendMessageWithType(ctx context.Context, target string, contentType string, body string) error
	Watch(ctx context.Context, extension string, fn func(ext string, state ExtensionState, prev ExtensionState)) (string, error)
	Unwatch(subscriptionID string) error
	SubscribeEvent(ctx context.Context, uri string, event string, expires int, fn func(NotifyEvent)) (string, error)
	UnsubscribeEvent(subscriptionID string) error
	AttendedTransfer(callA Call, callB Call) error
	FindCall(callID string) Call
	Calls() []Call
	State() PhoneState
}

Phone is the public interface for the xphone library.

func New

func New(opts ...PhoneOption) Phone

New creates a new Phone with the given options. Unset fields receive sensible defaults.

type PhoneOption

type PhoneOption func(*Config)

PhoneOption is a functional option for New().

func WithCodecs

func WithCodecs(codecs ...Codec) PhoneOption

WithCodecs sets the codec preference order.

func WithCredentials

func WithCredentials(username, password, host string) PhoneOption

WithCredentials sets the SIP username, password, and host.

func WithDisplayName added in v0.5.6

func WithDisplayName(name string) PhoneOption

WithDisplayName sets the display name for SIP Contact headers. Helps PBXes identify the device (e.g., "VoiceApp/1.0").

func WithDtmfMode added in v0.3.0

func WithDtmfMode(mode DtmfMode) PhoneOption

WithDtmfMode sets the DTMF signaling mode. Default is DtmfRfc4733 (RTP telephone-event packets).

func WithICE added in v0.3.0

func WithICE(enabled bool) PhoneOption

WithICE enables ICE-Lite candidate gathering and STUN connectivity check responder on the media socket (RFC 8445 section 2.2).

func WithJitterBuffer

func WithJitterBuffer(d time.Duration) PhoneOption

WithJitterBuffer sets the jitter buffer depth.

func WithLogger

func WithLogger(l *slog.Logger) PhoneOption

WithLogger sets the structured logger for the phone. If nil or not called, slog.Default() is used.

func WithMediaTimeout

func WithMediaTimeout(d time.Duration) PhoneOption

WithMediaTimeout sets the RTP media timeout.

func WithNATKeepalive

func WithNATKeepalive(d time.Duration) PhoneOption

WithNATKeepalive sets the NAT keepalive interval (CRLF ping over UDP).

func WithOutboundCredentials added in v0.4.1

func WithOutboundCredentials(username, password string) PhoneOption

WithOutboundCredentials sets separate SIP digest credentials for outbound INVITE authentication (401/407 challenges). When unset, the registration credentials (Username/Password) are used for all requests.

func WithOutboundProxy added in v0.4.1

func WithOutboundProxy(uri string) PhoneOption

WithOutboundProxy sets the SIP URI to route outbound INVITEs through. Registration still goes to the configured Host. In-dialog requests (re-INVITE, BYE) use the route set established via Record-Route. Example: "sip:proxy.example.com:5060"

func WithPCMRate

func WithPCMRate(rate int) PhoneOption

WithPCMRate sets the output sample rate for PCMReader.

func WithRTPPorts

func WithRTPPorts(min, max int) PhoneOption

WithRTPPorts sets the RTP port range.

func WithSRTP added in v0.2.0

func WithSRTP() PhoneOption

WithSRTP enables SRTP (Secure RTP) for media encryption. When enabled, SDP offers use RTP/SAVP with AES_CM_128_HMAC_SHA1_80.

func WithStunServer added in v0.2.0

func WithStunServer(server string) PhoneOption

WithStunServer sets the STUN server for NAT-mapped address discovery. Format: "host:port" (e.g. "stun.l.google.com:19302"). When set, the phone queries the STUN server during Connect() to discover its public IP, which is then used in SIP Contact headers and SDP.

func WithTransport

func WithTransport(protocol string, tlsCfg *tls.Config) PhoneOption

WithTransport sets the SIP transport protocol and optional TLS config.

func WithTurnCredentials added in v0.3.0

func WithTurnCredentials(username, password string) PhoneOption

WithTurnCredentials sets the TURN long-term credentials.

func WithTurnServer added in v0.3.0

func WithTurnServer(server string) PhoneOption

WithTurnServer sets the TURN server for relay allocation (RFC 5766). Format: "host:port" (e.g. "turn.example.com:3478"). Must be used with WithTurnCredentials.

func WithVoicemailURI added in v0.3.0

func WithVoicemailURI(uri string) PhoneOption

WithVoicemailURI sets the voicemail server URI for MWI SUBSCRIBE (RFC 3842). When set, the phone subscribes to voicemail notifications on Connect() and fires the OnVoicemail callback when the mailbox status changes. Example: "sip:*97@pbx.local"

type PhoneState

type PhoneState int

PhoneState represents the registration state of the phone.

const (
	PhoneStateDisconnected PhoneState = iota
	PhoneStateRegistering
	PhoneStateRegistered
	PhoneStateUnregistering
	PhoneStateRegistrationFailed
)

type Server added in v0.4.0

type Server interface {
	Listen(ctx context.Context) error
	Shutdown() error
	Dial(ctx context.Context, peer string, to string, from string, opts ...DialOption) (Call, error)
	DialURI(ctx context.Context, uri string, from string, opts ...DialOption) (Call, error)
	OnIncoming(func(call Call))
	OnCallState(func(call Call, state CallState))
	OnCallEnded(func(call Call, reason EndReason))
	OnCallDTMF(func(call Call, digit string))
	OnError(func(err error))
	OnOptions(func() int)
	FindCall(callID string) Call
	Calls() []Call
	State() ServerState
}

Server is the public interface for SIP trunk/server mode. It accepts and places calls directly with trusted SIP peers (PBXes, trunk providers) without SIP registration. Both Server and Phone produce the same Call interface — the downstream API is identical.

func NewServer added in v0.4.0

func NewServer(cfg ServerConfig) Server

NewServer creates a new Server with the given configuration.

type ServerConfig added in v0.4.0

type ServerConfig struct {
	// Listen is the address to bind the SIP listener on (e.g. "0.0.0.0:5080").
	Listen string
	// Listener is an optional pre-created UDP socket for the SIP listener.
	// When set, the server uses this connection instead of creating its own,
	// giving the caller full control over socket creation (e.g. SO_REUSEPORT,
	// buffer sizes, fd passing for zero-downtime deploys). The server takes
	// ownership: it will close the connection when Listen returns. Listen is
	// ignored when Listener is set.
	Listener net.PacketConn
	// RTPPortMin is the minimum RTP port to allocate. 0 means OS-assigned.
	RTPPortMin int
	// RTPPortMax is the maximum RTP port to allocate. 0 means OS-assigned.
	RTPPortMax int
	// RTPAddress is the public IP to advertise in SDP when listening on 0.0.0.0.
	// If empty, the listen address or auto-detected local IP is used.
	RTPAddress string
	// SRTP enables SRTP media encryption (RTP/SAVP with AES_CM_128_HMAC_SHA1_80).
	SRTP bool
	// CodecPrefs sets the codec preference order. Default: [PCMU].
	CodecPrefs []Codec
	// JitterBuffer sets the jitter buffer depth. Default: 50ms.
	JitterBuffer time.Duration
	// MediaTimeout sets the RTP inactivity timeout. Default: 30s.
	MediaTimeout time.Duration
	// PCMRate sets the output sample rate for PCMReader. Default: 8000.
	PCMRate int
	// DtmfMode controls DTMF signaling mode. Default: DtmfRfc4733.
	DtmfMode DtmfMode
	// Peers is the list of trusted SIP peers that can send/receive calls.
	Peers []PeerConfig
	// Logger sets the structured logger. Default: slog.Default().
	Logger *slog.Logger
}

ServerConfig holds all configuration for a Server instance.

type ServerState added in v0.4.0

type ServerState int

ServerState represents the state of a Server instance.

const (
	ServerStateStopped   ServerState = iota
	ServerStateListening             // Listen() active, accepting SIP
)

type SipMessage added in v0.3.0

type SipMessage struct {
	From        string
	To          string
	ContentType string
	Body        string
}

SipMessage represents an incoming or outgoing SIP MESSAGE (RFC 3428).

type SubState added in v0.3.0

type SubState int

SubState represents the state of a SIP event subscription (RFC 6665).

const (
	SubStatePending    SubState = iota // awaiting authorization
	SubStateActive                     // subscription is active
	SubStateTerminated                 // subscription ended
)

type VideoCodec added in v0.3.0

type VideoCodec int

VideoCodec represents a video codec.

const (
	VideoCodecH264 VideoCodec = 96
	VideoCodecVP8  VideoCodec = 97
)

type VideoFrame added in v0.3.0

type VideoFrame struct {
	Codec      VideoCodec
	Timestamp  uint32
	IsKeyframe bool
	Data       []byte // H.264: Annex-B NAL units; VP8: raw frame
}

VideoFrame represents a single assembled video frame from the RTP pipeline. The library does NOT encode/decode video — consumers use platform APIs (VideoToolbox, VA-API, etc.) and feed raw encoded frames in/out.

type VideoUpgradeRequest added in v0.3.0

type VideoUpgradeRequest struct {
	// contains filtered or unexported fields
}

VideoUpgradeRequest is created when the remote sends a re-INVITE adding video. The app must call Accept() or Reject(). If neither is called within 30 seconds, the request is automatically rejected. Calling Accept or Reject more than once is a no-op.

func (*VideoUpgradeRequest) Accept added in v0.3.0

func (r *VideoUpgradeRequest) Accept()

Accept accepts the video upgrade, starts the video pipeline, and responds 200 OK with a video SDP answer to the remote party.

func (*VideoUpgradeRequest) Reject added in v0.3.0

func (r *VideoUpgradeRequest) Reject()

Reject rejects the video upgrade and responds 200 OK with m=video 0 (RFC 3264 media rejection). Audio is unchanged.

func (*VideoUpgradeRequest) RemoteCodec added in v0.3.0

func (r *VideoUpgradeRequest) RemoteCodec() VideoCodec

RemoteCodec returns the video codec offered by the remote party.

type VoicemailStatus added in v0.3.0

type VoicemailStatus struct {
	// MessagesWaiting indicates whether new messages are waiting.
	MessagesWaiting bool
	// Account is the optional message account URI (e.g. "sip:*97@pbx.local").
	Account string
	// NewMessages is the count of new (unheard) voice messages.
	NewMessages int
	// OldMessages is the count of old (heard) voice messages.
	OldMessages int
}

VoicemailStatus represents the state of a voicemail mailbox (RFC 3842 MWI).

Directories

Path Synopsis
Package ice implements ICE-Lite (RFC 8445 §2.2) for SIP media NAT traversal.
Package ice implements ICE-Lite (RFC 8445 §2.2) for SIP media NAT traversal.
internal
rtcp
Package rtcp implements basic RTCP (RFC 3550) Sender/Receiver Reports for trunk compatibility.
Package rtcp implements basic RTCP (RFC 3550) Sender/Receiver Reports for trunk compatibility.
sdp
sip
Package sip implements a minimal SIP stack for xphone: message parsing/building, digest authentication, UDP transport, and client transactions.
Package sip implements a minimal SIP stack for xphone: message parsing/building, digest authentication, UDP transport, and client transactions.
srtp
Package srtp implements SRTP/SRTCP (RFC 3711) with AES_CM_128_HMAC_SHA1_80.
Package srtp implements SRTP/SRTCP (RFC 3711) with AES_CM_128_HMAC_SHA1_80.
stun
Package stun implements a STUN Binding client (RFC 5389) and provides shared STUN message primitives used by the TURN and ICE packages.
Package stun implements a STUN Binding client (RFC 5389) and provides shared STUN message primitives used by the TURN and ICE packages.
Package turn implements a TURN client (RFC 5766) for NAT relay allocation.
Package turn implements a TURN client (RFC 5766) for NAT relay allocation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL