# TikTok-Live-Connector

A Node.js library to receive live stream events such as comments and gifts in realtime
from [TikTok LIVE](https://www.tiktok.com/live) by connecting to TikTok's internal Webcast push service.
This package includes a wrapper that connects to the Webcast service using just the username (`@uniqueId`).
This allows you to connect to your own live chat as well as the live chat of other streamers. No credentials are
required. Besides [Chat Comments](#chat), other events such
as [Members Joining](#member), [Gifts](#gift), [Subscriptions](#subscribe), [Viewers](#roomuser), [Follows](#social), [Shares](#social), [Questions](#questionnew), [Likes](#like)
and [Battles](#linkmicbattle) can be tracked.

[![Discord](https://img.shields.io/discord/977648006063091742?logo=discord&label=TikTokLive%20Discord&labelColor=%23171717&color=%231877af)](https://discord.gg/N3KSxzvDX8)
![Connections](https://api.eulerstream.com/analytics/pips?client=ttlive-node)
![Downloads](https://img.shields.io/npm/dw/tiktok-live-connector?style=flat&color=0274b5&alt=1)
![Stars](https://img.shields.io/github/stars/zerodytrash/tiktok-live-connector?style=flat&color=0274b5&alt=1)
![Issues](https://img.shields.io/github/issues/zerodytrash/tiktok-live-connector?style=flat&color=0274b5&alt=1)
![Forks](https://img.shields.io/github/forks/zerodytrash/tiktok-live-connector?style=flat&color=0274b5&alt=1)

> **Note:** 
> This is <strong>not</strong> a production-ready API. It is a reverse engineering project. Use the [WebSocket API](https://www.eulerstream.com/websockets) for production.

> **Tip:** 
> An example project is available
> at https://tiktok-chat-reader.zerody.one/ - [View Source](https://github.com/zerodytrash/TikTok-Chat-Reader)

### Do you prefer other programming languages?

- **Python** rewrite: [TikTokLive](https://github.com/isaackogan/TikTokLive)
  by [@isaackogan](https://github.com/isaackogan) and [@eulerstream](https://github.com/eulerstream/)
- **Java** rewrite: [TikTokLiveJava](https://github.com/jwdeveloper/TikTokLiveJava)
  by [@jwdeveloper](https://github.com/jwdeveloper) and [@kohlerpop1](https://github.com/kohlerpop1)
- **Go** rewrite: [GoTikTokLive](https://github.com/steampoweredtaco/gotiktoklive)
  by [@steampoweredtaco](https://github.com/steampoweredtaco) and [@davincible](https://github.com/davincible)
- **C#** rewrite: [TikTokLiveSharp](https://github.com/frankvHoof93/TikTokLiveSharp)
  by [@frankvHoof93](https://github.com/frankvHoof93)

### Table of Contents

- [Getting Started](#getting-started)
- [Params and Options](#params-and-options)
- [Methods](#methods)
- [Properties](#properties)
- [Events](#events)
- [Examples](#examples)

## Getting Started

1. Install the package via NPM

```bash
npm i tiktok-live-connector
```

2. Create your first TikTok LIVE chat connection

```ts
// Username of someone who is currently live
import { TikTokLiveConnection, WebcastEvent } from 'tiktok-live-connector';

const tiktokUsername = 'officialgeilegisela';

// Create a new wrapper object and pass the username
const connection = new TikTokLiveConnection(tiktokUsername);

// Connect to the chat (await can be used as well)
connection.connect().then(state => {
    console.info(`Connected to roomId ${state.roomId}`);
}).catch(err => {
    console.error('Failed to connect', err);
});

// Define the events that you want to handle
// In this case we listen to chat messages (comments)
connection.on(WebcastEvent.CHAT, data => {
    console.log(`${data.user.uniqueId} (userId:${data.user.uniqueId}) writes: ${data.comment}`);
});

// And here we receive gifts sent to the streamer
connection.on(WebcastEvent.GIFT, data => {
    console.log(`${data.user.uniqueId} (userId:${data.user.userId}) sends ${data.giftId}`);
});

// ...and more events described in the documentation below
```

## Params and Options

To create a new `TikTokLiveConnection` object the following parameters can be specified.

`TikTokLiveConnection(uniqueId, [options])`

| Param Name | Required | Description |
|------------|----------|-------------|
| `uniqueId` | Yes | The unique username of the broadcaster. You can find this name in the URL.<br>Example: `https://www.tiktok.com/@officialgeilegisela/live` becomes `officialgeilegisela`. The leading `@` and the full URL form are also accepted. |
| `options`  | No | Optional connection properties. Defaults are applied when a value is not specified.<br><br>`signApiKey` (default: `undefined`)<br>Euler Stream API key. When provided, it is written to the global `SignConfig.apiKey` before the underlying Euler client is created. Ignored when `eulerApiInstance` is passed.<br><br>`eulerApiInstance` (default: `undefined`)<br>Pre-built `EulerStreamApiClient` to use for all sign-server traffic. Takes precedence over `signApiKey`.<br><br>`session` (default: `undefined`)<br>Authenticated session bundle. Pass `session.cookie` to seed the cookie jar with `sessionid` and `tt-target-idc`, and/or `session.oAuthToken` to send an OAuth token to the sign server. Required when `authenticateWs` or `useMobile` is true. See [Authenticated Connection](#authenticated-connection).<br><br>`authenticateWs` (default: `false`)<br>Forward the session cookies or OAuth token to the sign server so the WebSocket is authenticated. Disabled by default since signing is done by a third-party service; enabling it sends your session credentials to that service.<br><br>`useMobile` (default: `false`)<br>Use the mobile WebSocket flow. Implies `authenticateWs: true` and requires `session.cookie`.<br><br>`processInitialData` (default: `true`)<br>Decode and emit the message batch returned in the initial sign response (recent chat history etc.).<br><br>`fetchRoomInfoOnConnect` (default: `true`)<br>Fetch room info during connect. If the streamer is not currently live the connect rejects with `UserOfflineError`. The fetched info is stored on `connection.roomInfo` and is also accessible via [`fetchRoomInfo()`](#methods).<br><br>`enableExtendedGiftInfo` (default: `false`)<br>Fetch the room gift list during connect so `WebcastGiftMessage` events carry an `extendedGiftInfo` field with name, cost, and image data.<br><br>`clientPresets` (default: randomized)<br>Pre-built `{ device, screen, location }` presets. Defaults to a freshly randomized set from `getRandomPresets()`.<br><br>`webClientOptions` (default: `{}`)<br>Extra options forwarded to the underlying [`got`](https://github.com/sindresorhus/got) HTTP client (proxy agent, timeout, etc.). Headers and search params from this object are merged with the defaults; transport-only fields are passed through. See [Proxied Connection](#proxied-connection).<br><br>`wsClientOptions` (default: `{}`)<br>Extra options forwarded to the underlying [`ws`](https://github.com/websockets/ws) WebSocket client. See [Proxied Connection](#proxied-connection).<br><br>`webConfigOverrides` (default: `{}`)<br>Partial overrides for the resolved `WebcastWebConfigDefaults` used by the HTTP client. Use this to customize `DEFAULT_HTTP_CLIENT_PARAMS`, `DEFAULT_HTTP_CLIENT_HEADERS`, etc.<br><br>`wsConfigOverrides` (default: `{}`)<br>Partial overrides for the resolved `WebcastWebSocketConfigDefaults` used by the WebSocket client. Use this to customize `DEFAULT_WS_CLIENT_PARAMS` or `DEFAULT_WS_CLIENT_HEADERS`. |

#### Example Options

```ts
const tikTokLiveConnection = new TikTokLiveConnection(tiktokUsername, {
    signApiKey: 'your-api-key',
    processInitialData: false,
    enableExtendedGiftInfo: true,
    webConfigOverrides: {
        DEFAULT_HTTP_CLIENT_PARAMS: {
            app_language: 'en-US',
            device_platform: 'web_pc'
        },
        DEFAULT_HTTP_CLIENT_HEADERS: {
            'X-Custom-Header': 'value'
        }
    },
    wsConfigOverrides: {
        DEFAULT_WS_CLIENT_PARAMS: {
            app_language: 'en-US'
        },
        DEFAULT_WS_CLIENT_HEADERS: {
            'X-Custom-Header': 'value'
        }
    },
    webClientOptions: {
        timeout: { request: 10000 }
    },
    wsClientOptions: {
        handshakeTimeout: 10000
    }
});
```

## Methods

| Method Name | Description |
|-------------|-------------|
| `connect([roomId])` | Connects to the live stream chat. Returns a `Promise<TikTokLiveConnectionState>` that resolves when the WebSocket is open and the room has been entered. Pass an explicit `roomId` to skip room-id resolution. |
| `disconnect()` | Closes the WebSocket and waits for the `close` event before resolving. Safe to call even when not connected. |
| `sendMessage(content, [roomId])` | Sends a chat message to the connected room (or to `roomId` if provided). Requires `signApiKey` and an authenticated session. See [Send Messages](#send-messages). |
| `fetchRoomId([uniqueId])` | Resolves the room id for the configured username (or for `uniqueId` if provided). Tries HTML scrape, then the TikTok API, then Euler Stream as a fallback. |
| `fetchIsLive([uniqueId])` | Resolves whether the user is currently live. Same fallback chain as `fetchRoomId`. |
| `waitUntilLive([seconds], [abortSignal])` | Polls `fetchIsLive` every `seconds` seconds (minimum 30) until the streamer goes live. Reject via `abortSignal` to cancel. |
| `fetchRoomInfo([roomId])` | Fetches room info from TikTok's webcast API. Caches the result on `connection.roomInfo`. Callable without an active connection. See [Retrieve Room Info](#retrieve-room-info). |
| `fetchAvailableGifts()` | Fetches the room gift list. Caches on `connection.availableGifts`. Callable without an active connection. See [Retrieve Available Gifts](#retrieve-available-gifts). |

## Properties

| Property Name | Description |
|---------------|-------------|
| `webClient: WebcastHttpClient` | The HTTP client used to talk to TikTok's web and webcast APIs. Holds the cookie jar, the merged client params, and the merged client headers. |
| `apiClient: EulerStreamApiClient` | The Euler Stream API client used for sign-server traffic and other premium endpoints. Equivalent to `webClient.apiClient`. |
| `wsClient: WebcastWebSocketClient \| null` | The active WebSocket client. `null` until the connection is established and after disconnect. |
| `options: TikTokLiveConnectionMutableOptions` | The subset of options retained on the instance (`processInitialData`, `fetchRoomInfoOnConnect`, `enableExtendedGiftInfo`, `authenticateWs`, `useMobile`). |
| `roomInfo: RoomInfo \| null` | The room info object cached from the most recent `fetchRoomInfo()` call (or from connect, when `fetchRoomInfoOnConnect` is true). |
| `availableGifts: RoomGiftInfo \| null` | The cached gift list. Populated when `enableExtendedGiftInfo` is `true` or after manually calling `fetchAvailableGifts()`. |
| `isConnecting: boolean` | True while `connect()` is in flight. |
| `isConnected: boolean` | True after a successful connect, until the WebSocket closes. |
| `clientParams: Record<string, string>` | Live URI parameters sent with every `webClient` request. Mutating this changes outgoing query strings. |
| `roomId: string` | The currently bound room id. Empty string until connect resolves or `fetchRoomId()` is called. |
| `state: TikTokLiveConnectionState` | Snapshot containing `isConnected`, `isConnecting`, `roomId`, `roomInfo`, and `availableGifts`. |

## Signing Configuration

WebSocket URL signing is delegated to the [Euler Stream sign server](https://www.eulerstream.com) via the
[`@eulerstream/euler-api-sdk`](https://github.com/EulerStream/EulerApiSdk) package. Configuration is held on the
mutable `SignConfig` singleton.

For most users, passing `signApiKey` to the `TikTokLiveConnection` constructor is enough. It writes the key to
`SignConfig.apiKey` before the underlying Euler client is built.

For advanced cases (custom sign-server URL, extra headers, JWT auth), mutate `SignConfig` directly before
constructing your first connection:

```ts
import { SignConfig } from 'tiktok-live-connector';

SignConfig.apiKey = 'your api key'; // An API key created at https://www.eulerstream.com
SignConfig.basePath = 'https://your-custom-sign-server.com';
SignConfig.baseOptions = SignConfig.baseOptions || {};
SignConfig.baseOptions.headers = {
    ...(SignConfig.baseOptions.headers || {}),
    'X-Custom-Header': 'Custom-Header-Value'
};
```

> **Important:** 
> `SignConfig` is read once when the underlying Euler client is first instantiated and the result is cached.
> Mutate it before constructing the first `TikTokLiveConnection`, or pass an explicit `eulerApiInstance` to
> bypass the cache.

## Accessing TikTok LIVE Routes

All HTTP route handlers live in the global `RouteConfig` registry and are also exported individually.
Each route is a function that accepts a single options object and returns a `Promise` resolving to the route's
response. The currently registered routes are:

- `fetchRoomInfo` (TikTok webcast API)
- `fetchRoomGifts` (TikTok webcast API)
- `fetchRoomInfoFromApiLive` (TikTok web API)
- `fetchRoomInfoFromHtml` (HTML scrape with SIGI_STATE extraction)
- `fetchRoomGiftsFromProvider` (Euler Stream)
- `fetchRoomGiftGalleryFromProvider` (Euler Stream)
- `fetchRoomIdFromProvider` (Euler Stream)
- `fetchRoomInfoFromProvider` (Euler Stream)
- `fetchSignedWebSocketFromProvider` (Euler Stream)
- `fetchWebcastSignatureFromProvider` (Euler Stream)
- `sendRoomChatFromProvider` (Euler Stream)
- `fetchRoomIdComposite` (HTML, then API, then Euler fallback)
- `fetchIsLiveComposite` (HTML, then API, then Euler fallback)

### Example Usage

Routes need a `webClient` (and an `apiClient` for Euler routes), both of which are exposed on a
`TikTokLiveConnection` instance:

```ts
import { TikTokLiveConnection, RouteConfig } from 'tiktok-live-connector';

const connection = new TikTokLiveConnection('officialgeilegisela');

const sigiState = await RouteConfig.fetchRoomInfoFromHtml({
    webClient: connection.webClient,
    uniqueId: 'officialgeilegisela'
});

console.log('SIGI_STATE liveRoom:', sigiState.liveRoom);
```

### Swapping a Route Implementation

You can replace any handler in `RouteConfig` to inject custom behavior (caching, alternate sign provider,
proxying through your own backend, etc.):

```ts
import { RouteConfig } from 'tiktok-live-connector';

const original = RouteConfig.fetchSignedWebSocketFromProvider;
RouteConfig.fetchSignedWebSocketFromProvider = async (args) => {
    console.log('signing for room', args.roomId);
    return original(args);
};
```

## Accessing 3rd-Party (Euler Stream) Routes

The Euler Stream API client is exposed as `connection.apiClient`. It wraps every endpoint published by the
[`@eulerstream/euler-api-sdk`](https://github.com/eulerstream/EulerApiSdk) package, including premium and
analytics endpoints not bundled into `RouteConfig`.

### Example 1: Fetching Rate Limits

```ts
import { TikTokLiveConnection } from 'tiktok-live-connector';

const connection = new TikTokLiveConnection('officialgeilegisela', {
    signApiKey: 'your-api-key'
});

const response = await connection.apiClient.webcast.getRateLimits();
console.log('Rate Limits:', response.data);
```

### Example 2: Using JWT Authentication

When running the connector in a client-facing environment (bundled desktop app, browser worker, etc.) you should
not ship your raw API key. Mint a short-lived JWT on your server and hand that to the client instead.

#### Server-Side

```ts
import { SignConfig, createEulerClient } from 'tiktok-live-connector';

SignConfig.apiKey = 'your_server_side_api_key';
const apiClient = createEulerClient();

const res = await apiClient.authentication.createJWT(
    122, // Your account ID
    {
        limits: { minute: 5, hour: 5, day: 5 },
        expireAfter: 60 * 60 * 2 // 2 hours, the maximum accepted value
    }
);

console.log('Generated JWT:', res.data.token);
```

#### Client-Side

```ts
import { SignConfig, TikTokLiveConnection } from 'tiktok-live-connector';

SignConfig.baseOptions = SignConfig.baseOptions || {};
SignConfig.baseOptions.headers = {
    ...(SignConfig.baseOptions.headers || {}),
    'x-jwt-key': 'generated-jwt-key'
};

const connection = new TikTokLiveConnection('tv_asahi_news');
```

## Events

A `TikTokLiveConnection` object has the following events which can be handled with an event listener.

The simplest event handler is a connect event:

```ts
const connection = new TikTokLiveConnection('officialgeilegisela');
connection.on(ControlEvent.CONNECTED, () => console.log("Connected!"));
```

### Control Events:

- [`ControlEvent.CONNECTED`](#connected) or `"connected"`
- [`ControlEvent.DISCONNECTED`](#disconnected) or `"disconnected"`
- [`ControlEvent.RAW_DATA`](#rawdata) or `"rawData"`
- [`ControlEvent.DECODED_DATA`](#decodeddata) or `"decodedData"`
- [`ControlEvent.WEBSOCKET_CONNECTED`](#websocketconnected) or `"websocketConnected"`
- [`ControlEvent.ERROR`](#error) or `"error"`
- [`ControlEvent.WEBSOCKET_DATA`](#websocketdata) or `"websocketData"`

### Message Events:

- [`WebcastEvent.CHAT`](#chat) or `"chat"`
- [`WebcastEvent.GIFT`](#gift) or `"gift"`
- [`WebcastEvent.MEMBER`](#member) or `"member"`
- [`WebcastEvent.LIKE`](#like) or `"like"`
- [`WebcastEvent.SOCIAL`](#social) or `"social"`
- [`WebcastEvent.ENVELOPE`](#envelope) or `"envelope"`
- [`WebcastEvent.QUESTION_NEW`](#questionnew) or `"questionNew"`
- [`WebcastEvent.LINK_MIC_BATTLE`](#linkmicbattle) or `"linkMicBattle"`
- [`WebcastEvent.LINK_MIC_ARMIES`](#linkmicarmies) or `"linkMicArmies"`
- [`WebcastEvent.LIVE_INTRO`](#liveintro) or `"liveIntro"`
- [`WebcastEvent.SUPER_FAN`](#superfan) or `"superFan"`
- [`WebcastEvent.SUPER_FAN_JOIN`](#superfanjoin) or `"superFanJoin"`
- [`WebcastEvent.FOLLOW`](#follow) or `"follow"`
- [`WebcastEvent.SHARE`](#share) or `"share"`
- [`WebcastEvent.STREAM_END`](#streamend) or `"streamEnd"`
- [`WebcastEvent.ROOM_USER`](#roomuser) or `"roomUser"`
- [`WebcastEvent.EMOTE`](#emote) or `"emote"`
- [`WebcastEvent.GOAL_UPDATE`](#goalupdate) or `"goalUpdate"`
- [`WebcastEvent.ROOM_MESSAGE`](#roommessage) or `"roomMessage"`
- [`WebcastEvent.CAPTION_MESSAGE`](#captionmessage) or `"captionMessage"`
- [`WebcastEvent.IM_DELETE`](#imdelete) or `"imDelete"`
- [`WebcastEvent.IN_ROOM_BANNER`](#inroombanner) or `"inRoomBanner"`
- [`WebcastEvent.RANK_UPDATE`](#rankupdate) or `"rankUpdate"`
- [`WebcastEvent.POLL_MESSAGE`](#pollmessage) or `"pollMessage"`
- [`WebcastEvent.RANK_TEXT`](#ranktext) or `"rankText"`
- [`WebcastEvent.LINK_MIC_BATTLE_PUNISH_FINISH`](#linkmicbattlepunishfinish) or `"linkMicBattlePunishFinish"`
- [`WebcastEvent.LINK_MIC_BATTLE_TASK`](#linkmicbattletask) or `"linkMicBattleTask"`
- [`WebcastEvent.LINK_MIC_FAN_TICKET_METHOD`](#linkmicfanticketmethod) or `"linkMicFanTicketMethod"`
- [`WebcastEvent.LINK_MIC_METHOD`](#linkmicmethod) or `"linkMicMethod"`
- [`WebcastEvent.UNAUTHORIZED_MEMBER`](#unauthorizedmember) or `"unauthorizedMember"`
- [`WebcastEvent.OEC_LIVE_SHOPPING`](#oecliveshopping) or `"oecLiveShopping"`
- [`WebcastEvent.MSG_DETECT`](#msgdetect) or `"msgDetect"`
- [`WebcastEvent.LINK_MESSAGE`](#linkmessage) or `"linkMessage"`
- [`WebcastEvent.ROOM_VERIFY`](#roomverify) or `"roomVerify"`
- [`WebcastEvent.LINK_LAYER`](#linklayer) or `"linkLayer"`
- [`WebcastEvent.ROOM_PIN`](#roompin) or `"roomPin"`
- [`WebcastEvent.SUPER_FAN_BOX`](#superfanbox) or `"superFanBox"`
- [`WebcastEvent.ACCESS_CONTROL`](#accesscontrol) or `"accessControl"`
- [`WebcastEvent.ACCESS_RECALL`](#accessrecall) or `"accessRecall"`
- [`WebcastEvent.BOOST_CARD`](#boostcard) or `"boostCard"`
- [`WebcastEvent.BOTTOM_MESSAGE`](#bottommessage) or `"bottomMessage"`
- [`WebcastEvent.CAPSULE`](#capsule) or `"capsule"`
- [`WebcastEvent.GAME_RANK_NOTIFY`](#gameranknotify) or `"gameRankNotify"`
- [`WebcastEvent.GIFT_BROADCAST`](#giftbroadcast) or `"giftBroadcast"`
- [`WebcastEvent.GIFT_DYNAMIC_RESTRICTION`](#giftdynamicrestriction) or `"giftDynamicRestriction"`
- [`WebcastEvent.GIFT_PANEL_UPDATE`](#giftpanelupdate) or `"giftPanelUpdate"`
- [`WebcastEvent.GIFT_PROMPT`](#giftprompt) or `"giftPrompt"`
- [`WebcastEvent.GUIDE`](#guide) or `"guide"`
- [`WebcastEvent.HOURLY_RANK`](#hourlyrank) or `"hourlyRank"`
- [`WebcastEvent.LINK_MIC_LAYOUT_STATE`](#linkmiclayoutstate) or `"linkMicLayoutState"`
- [`WebcastEvent.LINK_STATE`](#linkstate) or `"linkState"`
- [`WebcastEvent.LIVE_GAME_INTRO`](#livegameintro) or `"liveGameIntro"`
- [`WebcastEvent.MARQUEE_ANNOUNCEMENT`](#marqueeannouncement) or `"marqueeAnnouncement"`
- [`WebcastEvent.NOTICE`](#notice) or `"notice"`
- [`WebcastEvent.PARTNERSHIP_DROPS_UPDATE`](#partnershipdropsupdate) or `"partnershipDropsUpdate"`
- [`WebcastEvent.PARTNERSHIP_GAME_OFFLINE`](#partnershipgameoffline) or `"partnershipGameOffline"`
- [`WebcastEvent.PARTNERSHIP_PUNISH`](#partnershippunish) or `"partnershipPunish"`
- [`WebcastEvent.PERCEPTION`](#perception) or `"perception"`
- [`WebcastEvent.ROOM_NOTIFY`](#roomnotify) or `"roomNotify"`
- [`WebcastEvent.SPEAKER`](#speaker) or `"speaker"`
- [`WebcastEvent.SUB_NOTIFY`](#subnotify) or `"subNotify"`
- [`WebcastEvent.SUB_PIN_EVENT`](#subpinevent) or `"subPinEvent"`
- [`WebcastEvent.TOAST`](#toast) or `"toast"`
- [`WebcastEvent.VIEWER_PICKS_UPDATE`](#viewerpicksupdate) or `"viewerPicksUpdate"`

## Control Events

### `connected`

Triggered when the connection is successfully established.

```ts
connection.on(
    ControlEvent.CONNECTED,
    (state: TikTokLiveConnectionState) => {
        console.log(`Hurray! Connected to roomId ${state.roomId}`);
        console.log(`Room info loaded: ${Boolean(state.roomInfo)}`);
    }
);
```

### `disconnected`

Triggered when the connection gets disconnected. In that case you can call `connect()` again to have a reconnect logic.
Note that you should wait a little bit before attempting a reconnect to to avoid being rate-limited.

```ts
connection.on(ControlEvent.DISCONNECTED, ({ code, reason }) => {
    console.log(`Disconnected :(${code})`);
    if (reason) {
        console.log(`Reason: ${reason}`);
    }
});
```

### `rawData`

Triggered every time a protobuf encoded webcast message arrives. You can deserialize the binary object depending on the
use case with <a href="https://www.npmjs.com/package/protobufjs">protobufjs</a>.

```ts
connection.on(ControlEvent.RAW_DATA, (messageTypeName, binary) => {
    console.log(messageTypeName, binary);
});
```

### `decodedData`

Triggered every time a decoded message arrives. This is the same as the `rawData` event but the binary object is already
decoded. You can use this event to handle the protobuf messages without the need of deserializing them yourself.

```ts
connection.on(ControlEvent.DECODED_DATA, (event: string, decodedData: any, binary: Uint8Array) => {
    console.log(event, decodedData);
});
```

### `websocketConnected`

Will be triggered as soon as a WebSocket connection is established. The WebSocket client object is passed.

```ts
connection.on(ControlEvent.WEBSOCKET_CONNECTED, (client: TikTokWsClient) => {
    console.log('WebSocket open:', client.open);
});
```

### `websocketData`

Triggered every time a WebSocket message arrives. This is the same as the `rawData` event but the binary object is

```ts
connection.on(ControlEvent.WEBSOCKET_DATA, (data: Uint8Array) => {
    console.log(`WebSocket bytes: ${data.length}`);
});
```

### `error`

General error event. You should handle this.

```ts
connection.on(ControlEvent.ERROR, ({ info, exception }) => {
    console.error('Error!', info, exception);
});
```

## Message Events

### `member`

Triggered every time a new viewer joins the live stream.

```ts
connection.on(WebcastEvent.MEMBER, (data: WebcastMemberMessage) => {
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`User uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`User nickname: ${nickname}`);
    }
    if (uniqueId || nickname) {
        console.log('User joined the stream!');
    }
    console.log(`Viewers: ${data.memberCount}`);
});
```

### `chat`

Triggered every time a new chat comment arrives.

```ts
connection.on(WebcastEvent.CHAT, (data: WebcastChatMessage) => {
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`User uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`User nickname: ${nickname}`);
    }
    if (data.comment) {
        console.log(`Comment: ${data.comment}`);
    }
});
```

### `gift`

Triggered every time a gift arrives. You will receive additional information via the `extendedGiftInfo` attribute when
you enable the [`enableExtendedGiftInfo`](#params-and-options) option.

> **NOTE:** Users have the capability to send gifts in a streak. This increases the `repeatCount` value until the user
> terminates the streak. During this time new gift events are triggered again and again with an increased `repeatCount`
> value. It should be noted that after the end of the streak, another gift event is triggered, which signals the end of
> the streak via `repeatEnd`:`true`. This applies only to gifts with `giftType`:`1`. This means that even if the user
> sends a `giftType`:`1` gift only once, you will receive the event twice. Once with `repeatEnd`:`false` and once
> with `repeatEnd`:`true`. Therefore, the event should be handled as follows:

```ts
connection.on(WebcastEvent.GIFT, (data: WebcastGiftMessage) => {
    const giftType = data.giftDetails?.giftType;
    const giftName = data.giftDetails?.giftName;
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;

    if (giftType === 1 && !data.repeatEnd) {
        // Streak in progress => show only temporary
        console.log('Gift streak in progress');
    } else {
        // Streak ended or non-streakable gift => process the gift with final repeat_count
        console.log('Gift streak ended or non-streakable gift');
    }
    if (uniqueId) {
        console.log(`User uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`User nickname: ${nickname}`);
    }
    console.log(`GiftId: ${data.giftId}, repeatCount: ${data.repeatCount}`);
    if (giftName) {
        console.log(`Gift name: ${giftName}`);
    }
});
```

### `roomUser`

Triggered every time a statistic message arrives. This message currently contains the viewer count and a top gifter
list.

```ts
connection.on(WebcastEvent.ROOM_USER, data => {
    console.log(`Viewer Count: ${data.viewerCount}`);
    const topGifter = data.ranksList[0];
    if (topGifter?.user) {
        const uniqueId = topGifter.user.uniqueId;
        const nickname = topGifter.user.nickname;
        if (uniqueId) {
            console.log(`Top gifter uniqueId: ${uniqueId} (${topGifter.coinCount})`);
        }
        if (nickname) {
            console.log(`Top gifter nickname: ${nickname} (${topGifter.coinCount})`);
        }
    }
});
```

### `like`

Triggered when a viewer sends likes to the streamer. For streams with many viewers, this event is not always triggered
by TikTok.

```ts
connection.on(WebcastEvent.LIKE, data => {
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`User uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`User nickname: ${nickname}`);
    }
    console.log(`Likes: ${data.likeCount}, total likes: ${data.totalLikeCount}`);
});
```

### `social`

Triggered every time someone shares the stream or follows the host.

```ts
connection.on(WebcastEvent.SOCIAL, data => {
    if (data.action) {
        console.log(`Social action: ${data.action}`);
    }
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`User uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`User nickname: ${nickname}`);
    }
    if (data.shareType || data.shareTarget) {
        console.log(`Share type: ${data.shareType}, share target: ${data.shareTarget}`);
    }
});
```

### `emote`

Triggered every time a subscriber sends an emote (sticker).

```ts
connection.on(WebcastEvent.EMOTE, (data: WebcastEmoteChatMessage) => {
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`User uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`User nickname: ${nickname}`);
    }
    const emoteId = data.emoteList[0]?.emoteId;
    if (emoteId) {
        console.log(`Emote id: ${emoteId}`);
    }
});
```

### `envelope`

Triggered every time someone sends a treasure chest.

```ts
connection.on(WebcastEvent.ENVELOPE, data => {
    const envelope = data.envelopeInfo;
    if (envelope) {
        if (envelope.envelopeId) {
            console.log(`Envelope ${envelope.envelopeId}`);
        }
        if (envelope.sendUserName) {
            console.log(`From: ${envelope.sendUserName}`);
        }
        console.log(`Diamonds: ${envelope.diamondCount}, People: ${envelope.peopleCount}`);
    }
});
```

### `questionNew`

Triggered every time someone asks a new question via the question feature.

```ts
connection.on(WebcastEvent.QUESTION_NEW, data => {
    const question = data.details?.questionText;
    if (question) {
        console.log(`Question: ${question}`);
    }
    const uniqueId = data.details?.user?.uniqueId;
    const nickname = data.details?.user?.nickname;
    if (uniqueId) {
        console.log(`Asked by (uniqueId): ${uniqueId}`);
    }
    if (nickname) {
        console.log(`Asked by (nickname): ${nickname}`);
    }
});
```

### `linkMicBattle`

Triggered every time a battle starts.

```ts
connection.on(WebcastEvent.LINK_MIC_BATTLE, (data) => {
    const participants = Object.values(data.anchorInfo)
        .map(info => info.user?.displayId || info.user?.nickName)
        .filter(Boolean);
    console.log(`New Battle ${data.battleId}`);
    if (participants.length) {
        console.log(`Participants: ${participants.join(' VS ')}`);
    }
});
```

### `linkMicArmies`

Triggered every time a battle participant receives points. Contains the current status of the battle and the army that
suported the group.

```ts
connection.on(WebcastEvent.LINK_MIC_ARMIES, (data) => {
    console.log(`Battle ${data.battleId}: gift ${data.giftId} x${data.giftCount}`);
    console.log(`Total diamonds: ${data.totalDiamondCount}`);
});
```

### `liveIntro`

Triggered when a live intro message appears.

```ts
connection.on(WebcastEvent.LIVE_INTRO, (msg) => {
    const uniqueId = msg.host?.uniqueId;
    const nickname = msg.host?.nickname;
    if (uniqueId) {
        console.log(`Host uniqueId: ${uniqueId}`);
    }
    if (nickname) {
        console.log(`Host nickname: ${nickname}`);
    }
    if (msg.description) {
        console.log(`Description: ${msg.description}`);
    }
});
```

### `streamEnd`

Triggered when the live stream gets terminated by the host. Will also trigger the [`disconnected`](#disconnected) event.

```ts
connection.on(WebcastEvent.STREAM_END, ({ action }: { action: ControlAction }) => {
    if (action === ControlAction.CONTROL_ACTION_STREAM_ENDED) {
        console.log('Stream ended by user');
    }
    if (action === ControlAction.CONTROL_ACTION_STREAM_SUSPENDED) {
        console.log('Stream ended by platform moderator (ban)');
    }
});
```

### `superFan`

Triggers when a user becomes a Super Fan.

```ts
connection.on(WebcastEvent.SUPER_FAN, (data) => {
    if (data.content?.defaultPattern) {
        console.log(data.content.defaultPattern);
    }
    if (data.commonBarrageContent?.defaultPattern) {
        console.log(data.commonBarrageContent.defaultPattern);
    }
});
```

### `superFanJoin`

Triggers when an existing Super Fan joins the live.

```ts
connection.on(WebcastEvent.SUPER_FAN_JOIN, (data) => {
    if (data.content?.defaultPattern) {
        console.log(data.content.defaultPattern);
    }
    if (data.commonBarrageContent?.defaultPattern) {
        console.log(data.commonBarrageContent.defaultPattern);
    }
});
```

### `superFanBox`

Triggers when a user sends a Super Fan Box.

```ts
connection.on(WebcastEvent.SUPER_FAN_BOX, (data) => {
    console.log('A Super Fan Box was sent!', data.envelopeInfo);
});
```

### Custom Events

These events are based on message events.

### `follow`

Triggers when a user follows the streamer. Based on `social` event.

```ts
connection.on(WebcastEvent.FOLLOW, (data) => {
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`${uniqueId} followed!`);
    }
    if (nickname) {
        console.log(`${nickname} followed!`);
    }
});
```

### `share`

Triggers when a user shares the stream. Based on `social` event.

```ts
connection.on(WebcastEvent.SHARE, (data) => {
    const uniqueId = data.user?.uniqueId;
    const nickname = data.user?.nickname;
    if (uniqueId) {
        console.log(`${uniqueId} shared the stream!`);
    }
    if (nickname) {
        console.log(`${nickname} shared the stream!`);
    }
})
```

### `goalUpdate`

Triggered when a channel goal is updated.

```ts
connection.on(WebcastEvent.GOAL_UPDATE, (data: WebcastGoalUpdateMessage) => {
    const goalDescription = data.goal?.description;
    const contributor = data.contributorDisplayId || data.contributorIdStr || data.contributorId;
    if (goalDescription) {
        console.log(`Goal update: ${goalDescription}`);
    }
    if (contributor) {
        console.log(`Contributor: ${contributor}, count: ${data.contributeCount}, score: ${data.contributeScore}`);
    }
});
```

### `roomMessage`

No information available.

```ts
connection.on(WebcastEvent.ROOM_MESSAGE, (data: WebcastRoomMessage) => {
    if (data.content) {
        console.log(`Room message: ${data.content}`);
    }
    if (data.source) {
        console.log(`Source: ${data.source}`);
    }
    console.log(`Scene: ${data.scene}`);
});
```

### `captionMessage`

No information available.

```ts
connection.on(WebcastEvent.CAPTION_MESSAGE, (data: WebcastCaptionMessage) => {
    if (data.content.length) {
        const lines = data.content.map(c => `[${c.lang}] ${c.content}`).join(' ');
        console.log(`Caption (${data.timestampMs}): ${lines}`);
    }
});
```

### `imDelete`

Triggered when a message is deleted in the chat.

```ts
connection.on(WebcastEvent.IM_DELETE, (data: WebcastImDeleteMessage) => {
    if (data.deleteMsgIdsList.length) {
        console.log(`Deleted messages: ${data.deleteMsgIdsList.join(', ')}`);
    }
    if (data.deleteUserIdsList.length) {
        console.log(`Deleted users: ${data.deleteUserIdsList.join(', ')}`);
    }
});
```

### `inRoomBanner`

Triggered when a banner is shown in the room.

```ts
connection.on(WebcastEvent.IN_ROOM_BANNER, (data: WebcastInRoomBannerMessage) => {
    if (data.jsonData) {
        console.log('Banner payload:', data.jsonData);
    }
});
```

### `rankUpdate`

Triggered when a ranking update is received.

```ts
connection.on(WebcastEvent.RANK_UPDATE, (data: WebcastRankUpdateMessage) => {
    console.log(`Rank updates: ${data.updatesList.length}`);
    const firstUpdate = data.updatesList[0];
    if (firstUpdate) {
        console.log(`Rank type: ${firstUpdate.rankType}, owner rank: ${firstUpdate.ownerRank}`);
    }
});
```

### `pollMessage`

Triggered when a poll-related message is sent in the room.

```ts
connection.on(WebcastEvent.POLL_MESSAGE, (data: WebcastPollMessage) => {
    const title = data.pollBasicInfo?.title;
    if (title) {
        console.log(`Poll: ${title}`);
    }
    if (data.pollBasicInfo?.timeRemain) {
        console.log(`Time remaining: ${data.pollBasicInfo.timeRemain}`);
    }
});
```

### `rankText`

Triggered when text related to rankings is displayed.

```ts
connection.on(WebcastEvent.RANK_TEXT, (data: WebcastRankTextMessage) => {
    if (data.ownerIdxBeforeUpdate || data.ownerIdxAfterUpdate) {
        console.log(`Rank change: ${data.ownerIdxBeforeUpdate} -> ${data.ownerIdxAfterUpdate}`);
    }
    if (data.selfGetBadgeMsg?.defaultPattern) {
        console.log(`Self badge text: ${data.selfGetBadgeMsg.defaultPattern}`);
    }
});
```

### `linkMicBattlePunishFinish`

Triggered when a link mic battle punishment is finished.

```ts
connection.on(WebcastEvent.LINK_MIC_BATTLE_PUNISH_FINISH, (data: WebcastLinkMicBattlePunishFinish) => {
    console.log(`Battle punish finished: ${data.battleId}`);
    console.log(`Reason: ${data.reason}, channel: ${data.channelId}`);
});
```

### `linkMicBattleTask`

Triggered when a new task is issued during a link mic battle.

```ts
connection.on(WebcastEvent.LINK_MIC_BATTLE_TASK, (data: WebcastLinkmicBattleTaskMessage) => {
    console.log(`Battle task (${data.battleId}) type: ${data.battleTaskMessageType}`);
    if (data.taskUpdate) {
        console.log(`Progress: ${data.taskUpdate.taskProgress}`);
    }
    if (data.taskSettle) {
        console.log(`Result: ${data.taskSettle.taskResult}`);
    }
});
```

### `linkMicFanTicketMethod`

Triggered when a fan ticket-related method is invoked during a link mic session.

```ts
connection.on(WebcastEvent.LINK_MIC_FAN_TICKET_METHOD, (data: WebcastLinkMicFanTicketMethod) => {
    const notice = data.FanTicketRoomNotice;
    if (notice) {
        console.log(`Total fan tickets: ${notice.TotalLinkMicFanTicket}`);
        console.log(`Users in notice: ${notice.UserFanTicketList.length}`);
    }
});
```

### `linkMicMethod`

Triggered when a link mic method is used.

```ts
connection.on(WebcastEvent.LINK_MIC_METHOD, (data: WebcastLinkMicMethod) => {
    console.log(`LinkMic messageType: ${data.messageType}`);
    if (data.channelId || data.userId || data.toUserId) {
        console.log(`Channel: ${data.channelId}, user: ${data.userId}, toUser: ${data.toUserId}`);
    }
});
```

### `unauthorizedMember`

Triggered when an unauthorized member tries to perform a restricted action.

```ts
connection.on(WebcastEvent.UNAUTHORIZED_MEMBER, (data: WebcastUnauthorizedMemberMessage) => {
    console.log(`Unauthorized member action: ${data.action}`);
    if (data.nickName) {
        console.log(`Nickname: ${data.nickName}`);
    }
    if (data.enterText?.defaultPattern) {
        console.log(`Enter text: ${data.enterText.defaultPattern}`);
    }
});
```

### `oecLiveShopping`

Triggered when a live shopping event occurs.

```ts
connection.on(WebcastEvent.OEC_LIVE_SHOPPING, (data: WebcastOecLiveShoppingMessage) => {
    const shop = data.shopData;
    if (shop) {
        if (shop.title || shop.priceString) {
            console.log(`Product: ${shop.title} (${shop.priceString})`);
        }
        if (shop.shopName || shop.shopUrl) {
            console.log(`Shop: ${shop.shopName}, URL: ${shop.shopUrl}`);
        }
    }
});
```

### `msgDetect`

Triggered when the system detects a message for moderation or other purposes.

```ts
connection.on(WebcastEvent.MSG_DETECT, (data: WebcastMsgDetectMessage) => {
    console.log(`Detect type: ${data.detectType}, triggerBy: ${data.triggerBy}`);
    if (data.triggerCondition?.uplinkDetectWebSocket !== undefined) {
        console.log(`WebSocket detect: ${data.triggerCondition.uplinkDetectWebSocket}`);
    }
    if (data.timeInfo?.apiRecvTimeMs) {
        console.log(`API recv time: ${data.timeInfo.apiRecvTimeMs}`);
    }
});
```

### `linkMessage`

Triggered during a link mic session for various communication purposes.

```ts
connection.on(WebcastEvent.LINK_MESSAGE, (data: WebcastLinkMessage) => {
    console.log(`Link message type: ${data.MessageType}, scene: ${data.Scene}`);
    if (data.InviteContent) {
        console.log(`Invite from ${data.InviteContent.fromUserId}`);
        if (data.InviteContent.actionId) {
            console.log(`Action: ${data.InviteContent.actionId}`);
        }
    }
    if (data.ReplyContent) {
        console.log(`Reply status: ${data.ReplyContent.replyStatus}`);
        if (data.ReplyContent.fromUserId || data.ReplyContent.toUserId) {
            console.log(`From ${data.ReplyContent.fromUserId} to ${data.ReplyContent.toUserId}`);
        }
    }
});
```

### `roomVerify`

Triggered when the system performs room verification.

```ts
connection.on(WebcastEvent.ROOM_VERIFY, (data: RoomVerifyMessage) => {
    console.log(`Room verify: ${data.noticeType}`);
    console.log(`Action: ${data.action}, closeRoom: ${data.closeRoom}`);
    if (data.content) {
        console.log(`Content: ${data.content}`);
    }
});
```

### `linkLayer`

Triggered when a new link mic layer is added or updated.

```ts
connection.on(WebcastEvent.LINK_LAYER, (data: WebcastLinkLayerMessage) => {
    console.log(`LinkLayer type: ${data.messageType}, channel: ${data.channelId}, scene: ${data.scene}`);
    if (data.inviteContent) {
        if (data.inviteContent.inviteeLinkMicId) {
            console.log(`Invite linkMicId: ${data.inviteContent.inviteeLinkMicId}`);
        }
    }
    if (data.createChannelContent) {
        if (data.createChannelContent.ownerLinkMicId) {
            console.log(`Owner linkMicId: ${data.createChannelContent.ownerLinkMicId}`);
        }
    }
});
```

### `roomPin`

Triggered when a message is pinned in the chat room.

```ts
connection.on(WebcastEvent.ROOM_PIN, (data: WebcastRoomPinMessage) => {
    const operatorUniqueId = data.operator?.uniqueId;
    const operatorNickname = data.operator?.nickname;
    if (operatorUniqueId) {
        console.log(`Pinned by (uniqueId): ${operatorUniqueId}`);
    }
    if (operatorNickname) {
        console.log(`Pinned by (nickname): ${operatorNickname}`);
    }
    if (data.method) {
        console.log(`Pin method: ${data.method}`);
    }

    const chatText = data.chatMessage?.comment;
    const giftName = data.giftMessage?.giftDetails?.giftName;
    const socialAction = data.socialMessage?.action;
    const memberUniqueId = data.memberMessage?.user?.uniqueId;
    const memberNickname = data.memberMessage?.user?.nickname;
    const likeCount = data.likeMessage?.likeCount;

    if (chatText) {
        console.log(`Pinned chat: ${chatText}`);
    }
    if (giftName) {
        console.log(`Pinned gift: ${giftName}`);
    }
    if (socialAction) {
        console.log(`Pinned social action: ${socialAction}`);
    }
    if (memberUniqueId) {
        console.log(`Pinned member (uniqueId): ${memberUniqueId}`);
    }
    if (memberNickname) {
        console.log(`Pinned member (nickname): ${memberNickname}`);
    }
    if (typeof likeCount === 'number') {
        console.log(`Pinned like count: ${likeCount}`);
    }
});
```

## Examples

### Retrieve Room Info

````ts
const connection = new TikTokLiveConnection('@username');

connection.fetchRoomInfo().then((roomInfo: RoomInfoResponse) => {
    console.log(roomInfo);
    console.log(`Stream started timestamp: ${roomInfo.create_time}, Streamer bio: ${roomInfo.owner.bio_description}`);
    console.log(`HLS URL: ${roomInfo.stream_url.hls_pull_url}`); // Can be played or recorded with e.g. VLC
}).catch(err => {
    console.error(err);
});
````

### Retrieve Available Gifts

````ts
const connection = new TikTokLiveConnection('@username');

connection.fetchAvailableGifts().then((giftList: RoomGiftInfo) => {
    console.log(giftList);
    giftList.forEach(gift => {
        console.log(`id: ${gift.id}, name: ${gift.name}, cost: ${gift.diamond_count}`)
    });
}).catch(err => {
    console.error(err);
})
````

### Proxied Connection

The HTTP layer uses [`got`](https://github.com/sindresorhus/got) and the WebSocket layer uses
[`ws`](https://github.com/websockets/ws). Both accept a Node `http.Agent`, which makes
[`hpagent`](https://www.npmjs.com/package/hpagent) the recommended proxy integration. `hpagent` is already a
direct dependency of this package, so no extra install is required.

#### HTTP Client (got) via `webClientOptions`

`got` accepts an `agent` map keyed by protocol (`http`, `https`, `http2`). Provide proxy agents for each protocol
the connector talks to:

```ts
import { TikTokLiveConnection } from 'tiktok-live-connector';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';

const proxy = 'http://user:pwd@host:8080';

const connection = new TikTokLiveConnection('@username', {
    webClientOptions: {
        agent: {
            http: new HttpProxyAgent({ proxy, keepAlive: true }),
            https: new HttpsProxyAgent({ proxy, keepAlive: true })
        }
    }
});
```

This routes all TikTok web/webcast HTTP traffic (room id, room info, gift list, HTML scrape, sign requests)
through the proxy.

#### WebSocket Client (ws) via `wsClientOptions`

`ws` accepts a single `agent` (a Node `http.Agent`). Since the WebSocket upgrade goes over `wss://`, use the
HTTPS variant:

```ts
import { TikTokLiveConnection } from 'tiktok-live-connector';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';

const proxy = 'http://user:pwd@host:8080';

const connection = new TikTokLiveConnection('@username', {
    webClientOptions: {
        agent: {
            http: new HttpProxyAgent({ proxy, keepAlive: true }),
            https: new HttpsProxyAgent({ proxy, keepAlive: true })
        }
    },
    wsClientOptions: {
        agent: new HttpsProxyAgent({ proxy, keepAlive: true })
    }
});

// Connect as usual
await connection.connect();
```

### Authenticated Connection

To connect as a specific TikTok account, log into TikTok in your browser and copy two cookies: `sessionid` and
`tt-target-idc`. Both are required. `sessionid` is your account's session token (keep it secret), and
`tt-target-idc` is the datacenter region your account belongs to (for example `useast1a` for North America).

Pass them via the `session.cookie` bundle. To force the WebSocket itself to authenticate (so the server treats you
as that account in chat), also set `authenticateWs: true`.

```ts
import { TikTokLiveConnection } from 'tiktok-live-connector';

const connection = new TikTokLiveConnection(
    'tv_asahi_news',
    {
        signApiKey: 'your-api-key',
        authenticateWs: true,
        session: {
            cookie: {
                type: 'cookie',
                value: {
                    sessionId: '<account_session_id>',
                    ttTargetIdc: '<account_target_idc>'
                }
            }
        }
    }
);

await connection.connect();
console.log('Connected to TikTok LIVE chat!');
```

You can also set the session after construction by calling `setSessionBundle` on the cookie jar:

```ts
await connection.webClient.cookieJar.setSessionBundle({
    type: 'cookie',
    value: {
        sessionId: '<account_session_id>',
        ttTargetIdc: '<account_target_idc>'
    }
});
```

### Send Messages

`sendMessage` posts a chat message into the currently connected room. It requires a valid Euler Stream API key
(see [Signing Configuration](#signing-configuration)) and an authenticated session bundle (cookies or OAuth token).

```ts
import { TikTokLiveConnection } from 'tiktok-live-connector';

const connection = new TikTokLiveConnection(
    'tv_asahi_news',
    {
        signApiKey: 'your-api-key',
        authenticateWs: true,
        session: {
            cookie: {
                type: 'cookie',
                value: {
                    sessionId: '<account_session_id>',
                    ttTargetIdc: '<account_target_idc>'
                }
            }
        }
    }
);

await connection.connect();
await connection.sendMessage('Hello world!');
console.log('Message sent!');
```

## Contributors

* **Zerody** - *Initial Reverse-Engineering and Protobuf-Decoding* - [Zerody](https://github.com/zerodytrash/)
* **Isaac Kogan** - *TypeScript Rewrite, Sign-Server Maintainer* - [isaackogan](https://github.com/isaackogan)

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
