npm package: @knmholdings/phone
Description: A TypeScript SDK for integrating SIP (Session Initiation Protocol) calling features into web applications, built on top of [SIP.js](https://sipjs.com/). This SDK provides a simplified interface for making and receiving calls, managing call states, and han
Latest version: 1.6.1

# Phone SDK

A TypeScript SDK for integrating SIP (Session Initiation Protocol) calling features into web applications, built on top of [SIP.js](https://sipjs.com/). This SDK provides a simplified interface for making and receiving calls, managing call states, and handling common telephony operations.

## Features

- **SIP Registration:** Connect to your SIP server.
- **Outbound Calls:** Initiate calls to SIP URIs or extensions.
- **Inbound Calls:** Receive and manage incoming calls.
- **Call Controls:**
  - Accept / Reject incoming calls.
  - Hangup active calls.
  - Mute / Unmute local audio.
  - Transfer calls (REFER) or Invite (API).
- **Event-Driven:** Attach listeners to various call and SDK events.
- **Stream Management:** Exposes methods to get local and remote `MediaStream` objects for attaching to HTML `<audio>` elements.
- **Agent Profile Registration:** Includes a method to interact with a backend API for agent profile setup before SIP registration (customizable).
- **Custom Call ID:** Uses `KNM-Call-ID` header for incoming calls and generates UUID for outgoing calls, ensuring consistent call tracking across the system.

## Installation

```bash
npm install sip.js @knmholdings/phone
# or
yarn add sip.js @knmholdings/phone
```

If you are using the SDK directly from its source or a local build, you'll need to ensure `sip.js` and `tslib` are also available.

## Getting Started

### 1. Initialization

First, import and instantiate the `PhoneSdk`:

```javascript
import { PhoneSdk } from '@knmholdings/phone'
const sdk = new PhoneSdk()
```

Or for UMD (if loaded via a `<script>` tag, assuming UMD name `KnM`):

```html
<script src="https://github.com/onsip/SIP.js/releases/download/0.21.2/sip-0.21.2.min.js"></script>
<script src="path/to/your/bundle.umd.min.js"></script>
<script>
	const PhoneSdk = window.KnM.PhoneSdk
	const sdk = new PhoneSdk()
</script>
```

### 2. Agent Profile Registration (Optional, Specific to KNM Holdings)

If your system requires registering an agent profile with a backend API before using SIP:

```javascript
async function registerProfile() {
	try {
		const agentDisplayName = 'Your Agent Name'
		const result = await sdk.register(agentDisplayName)
		if (result.success && result.data) {
			console.log('Agent profile registered successfully!')
			console.log('SIP Extension:', result.data.username)
			console.log('SIP Password:', result.data.password)
			console.log('SIP Display Name:', result.data.displayName)
			// Use these credentials to initialize SIP in the next step
			return result.data
		} else {
			console.error('Agent profile registration failed:', result.message)
		}
	} catch (error) {
		console.error('Error during agent profile registration:', error)
	}
	return null
}
```

### 3. SIP Initialization

Connect to your SIP server (typically after successful agent profile registration):

```javascript
async function initSIP(sipCredentials) {
	const config = {
		extension: sipCredentials.username, // e.g., "1007"
		password: sipCredentials.password, // e.g., "yourSecretPassword"
		displayName: sipCredentials.displayName, // e.g., "Agent 1007"
		sipServerUrl: 'your.sipserver.com', // Your SIP domain or IP
		apiServerUrl: 'https://api.yourserver.com', // Your API server URL
		apiKey: 'your-api-key' // Your API key for authentication
	}
	try {
		await sdk.init(config)
		console.log('SIP SDK Initialized and attempting to register.')
	} catch (error) {
		console.error('SIP SDK Initialization failed:', error)
	}
}

// Example flow:
registerProfile().then((creds) => {
	if (creds) {
		initSIP(creds)
	}
})
```

### 4. Attaching Event Listeners

The SDK is event-driven. Listen to events to handle call states and SDK status:

```javascript
sdk.on('onRegister', (event) => {
	console.log('Registration State:', event.state, 'Registered:', event.registered)
	if (event.registered) {
		// Ready to make/receive calls
	}
})

sdk.on('onCall', (event) => {
	if (event.type === 'incomingCall') {
		console.log('Incoming call from:', event.caller, 'Call ID:', event.callId)
		// Prompt user to accept or reject
		// Example: sdk.acceptCall(event.callId);
	} else if (event.type === 'callEstablished') {
		console.log('Call established with:', event.callee, 'Call ID:', event.callId)
		// Attach remote audio stream
		const remoteStream = sdk.getRemoteStream(event.callId)
		if (remoteStream) {
			const remoteAudioElement = document.getElementById('remoteAudio')
			remoteAudioElement.srcObject = remoteStream
			remoteAudioElement.play().catch((e) => console.warn('Autoplay failed:', e))
		}
	} else if (event.type === 'endCall') {
		console.log('Call ended:', event.callId, 'Reason:', event.reason)
		const remoteAudioElement = document.getElementById('remoteAudio')
		if (remoteAudioElement) remoteAudioElement.srcObject = null
	} else if (event.type === 'localMute' || event.type === 'localUnmute') {
		console.log('Local audio mute/unmute:', event.type, 'Call ID:', event.callId)
	}
})

sdk.on('onHangup', (event) => {
	if (event.type === 'remoteHangupCall') {
		console.log('Remote party hung up:', event.caller, 'Call ID:', event.callId)
	} else if (event.type === 'localHangupCall') {
		console.log('Local user hung up:', event.caller, 'Call ID:', event.callId)
	}
})

sdk.on('onError', (event) => {
	console.error('SDK Error:', event.message, event)
})
```

### 5. Making a Call

```javascript
async function makeCallTo(targetExtension) {
	if (!sdk.isRegistered()) {
		console.error('Not registered. Cannot make call.')
		return
	}
	try {
		const callId = await sdk.makeCall(targetExtension) // e.g., "1008"
		if (callId) {
			console.log('Call initiated with ID:', callId)
		} else {
			console.log('Failed to initiate call.')
		}
	} catch (error) {
		console.error('Error making call:', error)
	}
}
```

### 6. Call Controls

```javascript
// Assume 'currentCallId' is the ID of the active or ringing call

// Accept an incoming call
sdk.acceptCall(currentCallId)

// Reject an incoming call
sdk.rejectCall(currentCallId, 'Busy')

// Hangup an active call
sdk.hangupCall(currentCallId)

// Mute local audio
sdk.mute(currentCallId) // Mutes
sdk.mute(currentCallId, true) // Explicitly mutes

// Unmute local audio
sdk.unmute(currentCallId) // Unmutes (alias for sdk.mute(currentCallId, false))

// Transfer or invite a call (API-based)
sdk.redirect({
	type: 'transfer', // or 'invite'
	callId: currentCallId,
	targetUri: 'target_extension_or_uri'
})

// Transfer using SIP REFER (if supported by server)
sdk.transfer(currentCallId, 'target_extension_or_uri')
```

## API Reference

### `PhoneSdk` Class

#### Constructor

- `new PhoneSdk()`

#### Methods

- `async init(config: SDKConfig): Promise<void>`
  - Initializes the UserAgent and attempts SIP registration.
  - `config`:
    - `extension: string` - SIP extension
    - `password?: string` - SIP password
    - `displayName?: string` - Display name for the user
    - `sipServerUrl: string` - SIP server domain/IP
    - `apiServerUrl: string` - API server URL for additional services
    - `apiKey: string` - API key for authentication
    - `ices?: ICE[]` - Optional ICE servers
- `async disconnect(): Promise<void>`
  - Unregisters and stops the UserAgent.
- `on<K extends SDKEventsName>(eventName: K, handler: (event: SDKEventsMap[K]) => void): void`
  - Attaches an event listener. See Event Reference below for event names.
- `off<K extends SDKEventsName>(eventName: K, handler: (event: SDKEventsMap[K]) => void): void`
  - Removes an event listener.
- `isRegistered(): boolean`
  - Returns `true` if currently registered with the SIP server.
- `async makeCall(to: string): Promise<string | null>`
  - Initiates an outbound call to the target extension or SIP URI.
  - Returns the `callId` on success, `null` on failure.
- `async acceptCall(callId: string): Promise<void>`
  - Accepts an incoming call.
- `async rejectCall(callId: string, reason?: string): Promise<void>`
  - Rejects an incoming call.
- `async hangupCall(callId: string, reason?: string): Promise<void>`
  - Hangs up an active call.
- `async mute(callId: string, muteAudio?: boolean): Promise<void>`
  - Mutes (`true`) or unmutes (`false`) the local audio for the call. Defaults to `true`.
- `unmute(callId: string): Promise<void>`
  - Alias for `mute(callId, false)`.
- `async redirect(config: RedirectOptions): Promise<boolean>`
  - Redirects an active call via API (transfer/invite). Returns `true` if successful.
- `async transfer(callId: string, targetUri: string): Promise<boolean>`
  - Transfers an active call using SIP REFER (if supported by server). Returns `true` if successful.
- `getLocalStream(callId: string): MediaStream | null`
  - Gets the local `MediaStream` (microphone).
- `getRemoteStream(callId: string): MediaStream | null`
  - Gets the remote `MediaStream` (audio from the other party).
- `async register(initialDisplayName: string): Promise<AgentRegistrationResult>`
  - (Specific to KNM Holdings integration) Registers agent profile with an external API.
  - Returns `AgentRegistrationResult`:
    - `success: boolean`
    - `data: { username: string, password: string, displayName: string } | null`
    - `message: string`
    - `code?: string`

## Event Reference

You can listen to these events via `sdk.on(eventName, handler)`:

| Event Name   | Payload Type      | Description                                                                                                              |
| ------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `onError`    | `OnErrorEvent`    | SDK or SIP error                                                                                                         |
| `onCall`     | `OnCallEvent`     | Call states (outgoing, incoming, established, mute, unmute, ended, ...). See the `type` field in the payload for details |
| `onHangup`   | `OnHangupEvent`   | Call ended (local or remote)                                                                                             |
| `onInvite`   | `OnInviteEvent`   | Events related to INVITE/transfer via API                                                                                |
| `onTransfer` | `OnTransferEvent` | Events related to transfer via API                                                                                       |
| `onRegister` | `OnRegisterEvent` | SIP registration state                                                                                                   |

**Possible `type` values for each event:**

**OnCallEvent `type` values:**

| Type                     | Description                            |
| ------------------------ | -------------------------------------- |
| `incomingCall`           | Incoming call received                 |
| `localAcceptCall`        | Local user accepted the call           |
| `localNotAcceptCall`     | Local user did not accept the call     |
| `localSucceedCreateCall` | Outgoing call created successfully     |
| `remoteRingingCall`      | Remote party is ringing                |
| `remoteAcceptCall`       | Remote party accepted the call         |
| `remoteNotAcceptCall`    | Remote party did not accept the call   |
| `callEstablished`        | Call is established and media can flow |
| `localMute`              | Local audio muted                      |
| `localUnmute`            | Local audio unmuted                    |
| `endCall`                | Call has ended                         |

**OnHangupEvent `type` values:**

| Type               | Description                   |
| ------------------ | ----------------------------- |
| `localHangupCall`  | Local user hung up the call   |
| `remoteHangupCall` | Remote party hung up the call |

**OnInviteEvent `type` values:**

| Type                    | Description                           |
| ----------------------- | ------------------------------------- |
| `localStartInvite`      | Local user started an invite          |
| `localAcceptInvite`     | Local user accepted an invite         |
| `remoteAcceptInvite`    | Remote party accepted an invite       |
| `localNotAcceptInvite`  | Local user did not accept an invite   |
| `remoteNotAcceptInvite` | Remote party did not accept an invite |

**OnTransferEvent `type` values:**

| Type                 | Description                   |
| -------------------- | ----------------------------- |
| `localStartTransfer` | Local user started a transfer |

**Example payloads for some events:**

```js
// onCall event
{
	callId: '...',
	type: 'incomingCall' | 'localAcceptCall' | 'remoteAcceptCall' | 'callEstablished' | 'localMute' | 'localUnmute' | 'endCall',
	caller: '...',
	callee: '...',
	code: '...',
	reason: '...'
}

// onHangup event
{
	callId: '...',
	type: 'localHangupCall' | 'remoteHangupCall',
	caller: '...',
	callee: '...',
	code: '...',
	reason: '...'
}

// onError event
{
	message: '...',
	code: '...',
	error: {},
	callId: '...',
	targetUri: '...'
}
```

## Custom Headers

The SDK uses the custom header `KNM-Call-ID` to identify calls. This ID is generated for outgoing calls and read from incoming INVITE requests. Make sure your SIP infrastructure (PBX, SBC, ...) allows this header to pass through if you need backend synchronization.

## Logging

The SDK uses an internal logger (`utils/logger.ts`). By default, `sip.js` logging is disabled (`logBuiltinEnabled: false`). SDK logs will appear in the browser console.

## Development

### Prerequisites / Peer Dependencies

- Node.js and npm/yarn
- A SIP server for testing

### Build

```bash
npm install
npm run build
```

This will build the SDK into the `dist/` directory.

### Testing

You can use the `test.html` file to test the UMD bundle (`dist/bundle.umd.js`).

1. Configure `test.html` with your SIP credentials.
2. Serve the project directory (e.g., `npx serve .`).
3. Open `test.html` in your browser.

### Commit Message Convention

Follow Conventional Commits (e.g., `feat: ...`, `fix: ...`, `BREAKING CHANGE: ...`).