npm package: @observertc/observer-js
Description: Server Side NodeJS Library for processing ObserveRTC Samples
Author: Balazs Kreith
Latest version: 1.0.0-beta.3

# ObserverTC - Observer JS

[![NPM version](https://img.shields.io/npm/v/@observertc/observer-js.svg)](https://www.npmjs.com/package/@observertc/observer-js)
[![License](https://img.shields.io/npm/l/@observertc/observer-js.svg)](https://github.com/observertc/observer-js/blob/main/LICENSE)

`observer-js` is a Node.js library for monitoring WebRTC client data. It processes statistical samples from clients, organizes them into calls and participants, tracks a wide range of metrics, detects common issues, and calculates quality scores. This enables real-time insights into WebRTC session performance.

This library is a core component of the ObserverTC ecosystem, designed to provide robust server-side monitoring capabilities for WebRTC applications.

## Features

- **Hierarchical Data Model**: Organizes data into `Observer` -> `ObservedCall` -> `ObservedClient` -> `ObservedPeerConnection` and further into streams and data channels.
- **Comprehensive Metrics**: Tracks a wide array of WebRTC statistics including RTT, jitter, packet loss, codecs, ICE states, TURN usage, bandwidth, and more.
- **Automatic Entity Management**: Can automatically create and manage call and client entities based on incoming data samples.
- **Issue Detection**: Built-in detectors for common WebRTC problems.
- **Quality Scoring**: Calculates quality scores for calls and clients.
- **Event-Driven**: Emits events for significant state changes, new entities, and detected issues.
- **Configurable Update Policies**: Flexible control over how and when metrics are processed and updated.
- **TypeScript Support**: Written in TypeScript, providing strong typing and intellisense.
- **Extensible**: Supports custom application data (`appData`) and integration with external schema definitions (e.g., `observertc/schemas`).

## Installation

```bash
npm install @observertc/observer-js
# or
yarn add @observertc/observer-js
```

## Quick Start

```typescript
import { Observer, ObserverConfig } from '@observertc/observer-js';
import { ClientSample } from '@observertc/schemas'; // Assuming you use the official schemas

// 1. Configure the Observer
const observerConfig: ObserverConfig = {
	updatePolicy: 'update-on-interval',
	updateIntervalInMs: 5000, // Update observer every 5 seconds
	defaultCallUpdatePolicy: 'update-on-any-client-updated', // Calls update when any client sends data
};
const observer = new Observer(observerConfig);

// 2. Listen to events
observer.on('newcall', (call) => {
	console.log(`[Observer] New call detected: ${call.callId}`);

	call.on('newclient', (client) => {
		console.log(`[Call: ${call.callId}] New client joined: ${client.clientId}`);

		client.on('issue', (issue) => {
			console.warn(`[Client: ${client.clientId}] Issue: ${issue.type} - ${issue.severity} - ${issue.description}`);
		});
	});

	call.on('update', () => {
		console.log(
			`[Call: ${call.callId}] Metrics updated. Score: ${call.score?.toFixed(1)}, Clients: ${call.numberOfClients}`
		);
	});
});

// 3. Process Client Samples
// (ClientSample typically comes from your application after processing getStats() output)
function processClientStats(rawStats: any, callId: string, clientId: string) {
	// Transform rawStats into the ClientSample format
	// This is a placeholder for your actual transformation logic
	const sample: ClientSample = {
		callId,
		clientId,
		timestamp: Date.now(),
		// ...populate with transformed stats from rawStats, adhering to the ClientSample schema
		// from github.com/observertc/schemas
	};
	observer.accept(sample);
}

// Example usage:
// const myRawClientStats = getStatsFromClient();
// processClientStats(myRawClientStats, 'myMeeting123', 'userABC');

// 4. Cleanup when done
// process.on('SIGINT', () => observer.close());
```

---

## Detailed Documentation

The following sections provide a comprehensive guide to `observer-js`.

### 1. General Description

(This section is identical to the introductory paragraph at the top of this README)

### 2. Core Concepts

#### 2.1. Data Flow

1.  **Client-Side**: Your application collects WebRTC statistics (e.g., via `RTCPeerConnection.getStats()`).
2.  **Transformation**: These raw stats are transformed into the `ClientSample` schema (ideally from [observertc/schemas](https://github.com/observertc/schemas)).
3.  **Ingestion**: The `ClientSample` is passed to the `observer.accept()` method.
4.  **Processing**: `observer-js` processes the sample, updating or creating relevant entities (`ObservedCall`, `ObservedClient`, `ObservedPeerConnection`, etc.) and their metrics.
5.  **Analysis**: Metrics are analyzed for issue detection and quality scoring.
6.  **Events**: Events are emitted for significant state changes, new issues, or updates.

#### 2.2. Entity Hierarchy

- **`Observer`**: The root object, managing multiple calls and global settings.
  - **`ObservedCall`**: Represents a distinct call session.
    - **`ObservedClient`**: Represents an individual participant within a call.
      - **`ObservedPeerConnection`**: Represents a WebRTC RTCPeerConnection of a client.
        - **`ObservedInboundRtpStream` / `ObservedOutboundRtpStream`**: Tracks individual media streams.
        - **`ObservedDataChannel`**: Tracks data channels.
- **`ObservedTURN`**: Tracks global TURN server usage metrics across the observer.

#### 2.3. Automatic Entity Creation

When `observer.accept(sample)` is called:

- If an `ObservedCall` for `sample.callId` doesn't exist, it's typically created.
- If an `ObservedClient` for `sample.clientId` within that call doesn't exist, it's typically created.
- Peer connections, streams, and data channels are similarly managed based on IDs in the sample.

#### 2.4. Metrics Aggregation

The library aggregates a wide array of metrics at each level of the hierarchy, including (but not limited to):

- RTT, jitter, packet loss
- Bytes sent/received (audio, video, data)
- Codec information
- ICE connection details, TURN usage
- Stream/track states (muted, enabled)
- Frame rates, resolutions
- Bandwidth estimations

#### 2.5. Issue Detection

A `Detectors` system analyzes metrics to identify common WebRTC issues (e.g., high packet loss, low audio levels, frozen video, connection setup problems). Issues are reported via events.

#### 2.6. Quality Scoring

`ScoreCalculator` components assess the quality of calls and clients based on metrics and detected issues, typically resulting in a numerical score (e.g., 0.0 to 5.0).

#### 2.7. Event-Driven Architecture

The library uses Node.js `EventEmitter` to signal various occurrences, allowing applications to react to changes in real-time.

### 3. API Reference

#### 3.1. `Observer`

Manages all monitored calls and global observer state.

**Configuration (`ObserverConfig`)**

```typescript
export type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = {
	updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated' | 'update-on-interval';
	updateIntervalInMs?: number; // Used if updatePolicy is 'update-on-interval'
	defaultCallUpdatePolicy?: ObservedCallSettings['updatePolicy'];
	defaultCallUpdateIntervalInMs?: number;
	appData?: AppData; // Custom data for this observer instance
};
```

**Constructor**

```typescript
new Observer<AppData>(config?: ObserverConfig<AppData>)
```

- `config`: Optional. Defaults: `updatePolicy: 'update-when-all-call-updated'`.

**Key Properties**

- `observedCalls: Map<string, ObservedCall>`: Active calls.
- `observedTURN: ObservedTURN`: Aggregated TURN metrics.
- `appData: AppData | undefined`: Custom application data.
- `closed: boolean`: True if `close()` has been called.
- Counters: `totalAddedCall`, `totalRemovedCall`, RTT buckets, `totalClientIssues`, `numberOfClientsUsingTurn`, `numberOfClients`, `numberOfPeerConnections`, etc.

**Key Methods**

- `createObservedCall<T>(settings: ObservedCallSettings<T>): ObservedCall<T>`
- `getObservedCall<T>(callId: string): ObservedCall<T> | undefined`
- `accept(sample: ClientSample): void`: A convenience method to feed WebRTC stats. If `sample.callId` and `sample.clientId` are provided, it will route the sample to the appropriate `ObservedCall` and `ObservedClient`, creating them if they don't exist. The core sample processing for an existing client happens within the `ObservedClient`'s own `accept` or update mechanism.
- `update(): void`: Manually trigger an update cycle (behavior depends on `updatePolicy`).
- `close(): void`: Cleans up resources for the observer and all its calls.
- `createEventMonitor<CTX>(ctx?: CTX): ObserverEventMonitor<CTX>`: For contextual event listening.

**Events (`ObserverEvents`)**

- `'newcall' (call: ObservedCall)`
- `'call-updated' (call: ObservedCall)`
- `'client-event' (client: ObservedClient, event: ClientEvent)`
- `'client-issue' (client: ObservedClient, issue: ClientIssue)`
- `'client-metadata' (client: ObservedClient, metadata: ClientMetaData)`
- `'client-extension-stats' (client: ObservedClient, stats: ExtensionStat)`
- `'update' ()`
- `'close' ()`

#### 3.2. `ObservedCall`

Represents a single call session.

**Configuration (`ObservedCallSettings`)**

```typescript
export type ObservedCallSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
	callId: string;
	appData?: AppData;
	updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated' | 'update-on-interval';
	updateIntervalInMs?: number; // Used if updatePolicy is 'update-on-interval'
	remoteTrackResolvePolicy?: 'mediasoup-sfu'; // For specific SFU integration
};
```

**Key Properties**

- `callId: string`
- `appData: AppData | undefined`
- `numberOfClients: number`
- `score: number | undefined`: Overall call quality score.
- `observedClients: Map<string, ObservedClient>`
- Counters: `totalAddedClients`, `totalRemovedClients`, `numberOfIssues`, RTT buckets, total bytes sent/received (audio/video/data), etc.

**Key Methods**

- `createObservedClient<T>(settings: ObservedClientSettings<T>): ObservedClient<T>`
- `getObservedClient<T>(clientId: string): ObservedClient<T> | undefined`
- `update(): void`
- `close(): void`
- `createEventMonitor<CTX>(ctx?: CTX): ObservedCallEventMonitor<CTX>`

**Events (Emitted via `ObservedCall` instance)**

- `'newclient' (client: ObservedClient)`
- `'empty' ()`: When the last client leaves.
- `'not-empty' ()`: When the first client joins an empty call.
- `'update' ()`
- `'close' ()`

#### 3.3. `ObservedClient`

Represents a participant in a call.

**Configuration (`ObservedClientSettings`)**

```typescript
export type ObservedClientSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
	clientId: string;
	appData?: AppData;
	// Potentially other client-specific settings
};
```

**Key Properties**

- `clientId: string`
- `call: ObservedCall`: Reference to the parent call.
- `appData: AppData | undefined`
- `score: number | undefined`: Client quality score.
- `numberOfPeerConnections: number`
- `usingTURN: boolean`
- `observedPeerConnections: Map<string, ObservedPeerConnection>`
- Counters: `numberOfIssues`, RTT buckets, total bytes sent/received, `availableIncomingBitrate`, `availableOutgoingBitrate`, etc.

**Key Methods**

- `accept(sample: ClientSample): void`: (Or a similar internal update method called by `Observer.accept` or `ObservedCall`) Processes a `ClientSample` specific to this client, updating its metrics, peer connections, streams, etc. This is the primary point where a client's detailed WebRTC statistics are processed.
- `createObservedPeerConnection<T>(settings: ObservedPeerConnectionSettings<T>): ObservedPeerConnection<T>`
- `getObservedPeerConnection<T>(peerConnectionId: string): ObservedPeerConnection<T> | undefined`
- `update(): void`
- `close(): void`
- `createEventMonitor<CTX>(ctx?: CTX): ObservedClientEventMonitor<CTX>`

**Events (Emitted via `ObservedClient` instance)**

- `'joined' ()`
- `'left' ()`
- `'update' ()`
- `'close' ()`
- `'newpeerconnection' (pc: ObservedPeerConnection)`
- `'issue' (issue: ClientIssue)` (and other specific issue events)

#### 3.4. `ObservedPeerConnection`

Represents an `RTCPeerConnection`.

- Tracks ICE connection state, data channel stats, stream stats.
- Holds `ObservedInboundRtpStream`, `ObservedOutboundRtpStream`, and `ObservedDataChannel` instances.

#### 3.5. `ObservedInboundRtpStream` / `ObservedOutboundRtpStream`

- Track metrics for individual media streams (audio/video) like codec, packets lost/received, jitter, bytes, etc.

#### 3.6. `ObservedDataChannel`

- Tracks metrics for data channels like state, messages sent/received, bytes.

#### 3.7. `ClientSample` (Schema)

This is the primary input data structure passed to `observer.accept()`. It's a comprehensive object that should mirror the information obtainable from WebRTC `getStats()` and other client-side states. Key fields include:

- `callId`, `clientId`, `timestamp`
- `peerConnections: RTCPeerConnectionStats[]`
- `inboundRtpStreams: RTCInboundRtpStreamStats[]`
- `outboundRtpStreams: RTCOutboundRtpStreamStats[]`
- `remoteInboundRtpStreams: RTCRemoteInboundRtpStreamStats[]`
- `remoteOutboundRtpStreams: RTCRemoteOutboundRtpStreamStats[]`
- `dataChannels: RTCDataChannelStats[]`
- `iceLocalCandidates: RTCIceCandidateStats[]`, `iceRemoteCandidates: RTCIceCandidateStats[]`, `iceCandidatePairs: RTCIceCandidatePairStats[]`
- `mediaSources: RTCAudioSourceStats[] / RTCVideoSourceStats[]`
- `tracks: RTCMediaStreamTrackStats[]`
- `certificates: RTCCertificateStats[]`
- `codecs: RTCCodecStats[]`
- `transports: RTCIceTransportStats[]` (or similar depending on spec version)
- `browser`, `engine`, `platform`, `os` (client environment metadata)
- `userMediaErrors`, `iceConnectionStates`, `connectionStates` (client-reported events/states)
- `extensionStats` (for custom data)

_(Refer to the [observertc/schemas](https://github.com/observertc/schemas) repository, particularly the `ClientSample.ts` definition, for the exact and complete structure.)_

### 4. Configuration Possibilities

#### 4.1. Update Policies

Control how frequently entities re-calculate metrics and emit `update` events.

**Observer Level (`ObserverConfig.updatePolicy`)**

- `'update-on-any-call-updated'`: Observer updates if any of its calls update.
- `'update-when-all-call-updated'`: Observer updates after all its calls update. (Default)
- `'update-on-interval'`: Observer updates at `ObserverConfig.updateIntervalInMs`.

**Call Level (`ObservedCallSettings.updatePolicy` or `ObserverConfig.defaultCallUpdatePolicy`)**

- `'update-on-any-client-updated'`: Call updates if any of its clients update.
- `'update-when-all-client-updated'`: Call updates after all its clients update.
- `'update-on-interval'`: Call updates at its `updateIntervalInMs`.

#### 4.2. Intervals

- `ObserverConfig.updateIntervalInMs`
- `ObserverConfig.defaultCallUpdateIntervalInMs`
- `ObservedCallSettings.updateIntervalInMs`

#### 4.3. Application Data (`appData`)

Associate custom context with `Observer`, `ObservedCall`, and `ObservedClient` instances using generics.

```typescript
interface MyCallAppData {
	meetingTitle: string;
	scheduledAt: Date;
}
const call = observer.createObservedCall<MyCallAppData>({
	callId: 'call1',
	appData: { meetingTitle: 'Team Sync', scheduledAt: new Date() },
});
console.log(call.appData?.meetingTitle);
```

#### 4.4. `appData` vs. Attachments

The `observer-js` library provides two primary ways to associate custom information with its entities: `appData` and `attachments`. Understanding their distinct purposes is key for effective use.

**`appData` (Application Data)**

- **Purpose**: `appData` is designed to hold structured, typed, and relatively static metadata about an entity (`Observer`, `ObservedCall`, `ObservedClient`). This data is typically set at the time of entity creation and is directly accessible as a property of the entity instance.
- **Typing**: It is strongly typed using generics (e.g., `Observer<MyObserverAppData>`). This provides type safety and autocompletion in TypeScript environments.
- **Mutability**: While technically mutable (if the object assigned is mutable), it's generally intended for information that defines or describes the entity and doesn't change frequently during its lifecycle.
- **Accessibility**: Directly accessible via `entity.appData`.
- **Use Cases**:
  - Storing application-specific identifiers (e.g., `userId`, `roomId`, `meetingType`).
  - Configuration flags relevant to how your application interprets this entity.
  - Descriptive information (e.g., `clientDeviceType`, `callRegion`).

**`attachments` (Arbitrary Attachments)**

- **Purpose**: `attachments` (if implemented as a `Map<string, unknown>` or similar mechanism on entities) are meant for associating arbitrary, often dynamic, or less structured data with an entity. This can be useful for temporary state, inter-plugin communication, or data that doesn't fit neatly into a predefined `appData` schema.
- **Typing**: Typically less strictly typed (e.g., `unknown` or `any` values in a Map). Consumers of attachments need to perform their own type checks or assertions.
- **Mutability**: Designed to be more dynamic. Attachments can be added, updated, or removed throughout the entity's lifecycle.
- **Accessibility**: Accessed via methods like `entity.setAttachment(key, value)`, `entity.getAttachment(key)`, `entity.removeAttachment(key)`.
- **Use Cases**:
  - Storing temporary state calculated by one part of your application to be read by another (e.g., a custom issue detector plugin might attach intermediate findings).
  - Caching results of expensive computations related to the entity.
  - Allowing different modules or plugins to associate their own private data with an observer entity without needing to modify its core `appData` type.
  - Storing large binary data or complex objects that are not part of the core descriptive metadata.

**When to Use Which:**

- Use **`appData`** for:
  - Core, descriptive metadata that is known at creation time or changes infrequently.
  - Data that benefits from strong typing and is integral to your application's understanding of the entity.
- Use **`attachments`** for:
  - Dynamic, temporary, or loosely structured data.
  - Data added by different, potentially independent, parts of your system or plugins.
  - Information that doesn't need to be part of the primary, typed `appData` schema.

If `attachments` are not yet a formal feature, this section can serve as a design consideration or be adapted if you introduce such a mechanism. If `attachments` are already present, ensure the description matches their actual implementation.

#### 4.5. Remote Track Resolution

For SFU scenarios, especially with MediaSoup:
`ObservedCallSettings.remoteTrackResolvePolicy: 'mediasoup-sfu'`

### 5. Examples

#### 5.1. Basic Observer Setup & Sample Ingestion

```typescript
// filepath: /path/to/your/app.ts
import { Observer, ObserverConfig } from '@observertc/observer-js'; // Adjust path
import { ClientSample } from '@observertc/schemas'; // Adjust path if using official schemas

const observerConfig: ObserverConfig = {
	updatePolicy: 'update-on-interval',
	updateIntervalInMs: 5000,
	defaultCallUpdatePolicy: 'update-on-any-client-updated',
};
const observer = new Observer(observerConfig);

observer.on('newcall', (call) => {
	console.log(`[Observer] New call: ${call.callId}`);
	call.on('update', () => {
		console.log(`[Call: ${call.callId}] Updated. Clients: ${call.numberOfClients}, Score: ${call.score?.toFixed(1)}`);
	});
	call.on('newclient', (client) => {
		console.log(`[Call: ${call.callId}] New client: ${client.clientId}`);
		client.on('update', () => {
			// console.log(`[Client: ${client.clientId}] Updated. Score: ${client.score?.toFixed(1)}`);
		});
		client.on('issue', (issue) => {
			console.warn(`[Client: ${client.clientId}] Issue: ${issue.type} - ${issue.severity} - ${issue.description}`);
		});
	});
});

// Function to transform your app's WebRTC stats to ClientSample
function mapStatsToClientSample(appStats: any, callId: string, clientId: string): ClientSample {
	// Detailed mapping logic here based on ClientSample.ts schema
	// from github.com/observertc/schemas
	return {
		callId,
		clientId,
		timestamp: Date.now(),
		// ... map all relevant stats fields ...
	} as ClientSample; // Ensure all required fields are present
}

// Example: Receiving stats and processing
const rawStatsFromClient = {
	/* ... your client's getStats() output ... */
};
const callId = 'meeting-alpha-123';
const clientId = 'user-xyz-789';
const sample = mapStatsToClientSample(rawStatsFromClient, callId, clientId);
observer.accept(sample);

// Later, on application shutdown:
// observer.close();
```

#### 5.2. Manual Call and Client Creation

```typescript
// ... observer setup ...

const call = observer.createObservedCall({
	callId: 'scheduled-webinar-456',
	updatePolicy: 'update-on-interval',
	updateIntervalInMs: 10000,
});

const client1 = call.createObservedClient({ clientId: 'presenter-01' });
// Samples for 'presenter-01' in call 'scheduled-webinar-456' will update this client.
```

#### 5.3. Using Event Monitors for Contextual Logging

```typescript
const call = observer.getObservedCall('meeting-alpha-123');
if (call) {
	const callMonitor = call.createEventMonitor({ callId: call.callId, started: new Date() });
	callMonitor.on('client-joined', (client, context) => {
		console.log(`EVENT_MONITOR (${context.callId}): Client ${client.clientId} joined at ${new Date()}`);
	});
	callMonitor.on('issue-detected', (client, issue, context) => {
		console.error(`EVENT_MONITOR (${context.callId}): Issue on ${client.clientId} - ${issue.description}`);
	});
}
```

### 6. Best Practices

- **Resource Management**: Always call `observer.close()`, `call.close()`, and `client.close()` when entities are no longer needed to free resources and stop timers.
- **Error Handling**: Wrap calls to library methods in `try...catch` blocks where appropriate, especially for operations that might throw errors based on state (e.g., creating an entity that already exists if not using `getOrCreate` patterns).
- **Event Listener Cleanup**: If dynamically adding/removing listeners, ensure they are properly removed (e.g., using `emitter.off()` or `emitter.removeListener()`) to prevent memory leaks, especially for short-lived monitored entities.
- **`ClientSample` Accuracy**: The quality of monitoring heavily depends on the completeness and correctness of the `ClientSample` data provided. Ensure thorough mapping from `getStats()`.
- **Update Policies**: Choose update policies carefully based on the desired granularity of updates and performance considerations.

### 7. Troubleshooting

- **Memory Leaks**: Ensure `close()` is called on all entities. Check for unremoved event listeners.
- **No Events / Missing Updates**:
  - Verify `observer.accept()` is being called with correctly formatted `ClientSample` data.
  - Ensure `callId` and `clientId` in samples match expectations.
  - Check if `updatePolicy` and `updateIntervalInMs` are configured as intended.
- **Debugging**: Utilize `console.log` within event handlers at different levels (Observer, Call, Client) to trace data flow and state changes. Use `appData` to add correlation IDs for easier debugging.

### 8. TypeScript Support

The library is written in TypeScript and provides type definitions.
Use generics with `Observer`, `ObservedCall`, and `ObservedClient` to type your custom `appData`.

```typescript
interface MyClientAppData {
	userId: string;
	role: 'admin' | 'user';
}
const client = call.createObservedClient<MyClientAppData>({
	clientId: 'user1',
	appData: { userId: 'u-123', role: 'admin' },
});
// client.appData will be typed as MyClientAppData | undefined
```

### 9. Contributing

(Placeholder for contribution guidelines - e.g., link to CONTRIBUTING.md, coding standards, pull request process)

### 10. License

This project is licensed under the [MIT License](LICENSE).