tokwatchr 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ <!-- prettier-ignore -->
2
+ <div align="center">
3
+
4
+ # tokwatchr
5
+
6
+ **Download TikTok livestreams — given a username, download the livestream.**
7
+
8
+ [![npm version](https://img.shields.io/npm/v/tokwatchr?style=flat-square)](https://www.npmjs.com/package/tokwatchr)
9
+ [![npm downloads](https://img.shields.io/npm/dm/tokwatchr?style=flat-square)](https://www.npmjs.com/package/tokwatchr)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
11
+ [![Node.js](https://img.shields.io/badge/Node.js->=20-3c873a?style=flat-square)](https://nodejs.org)
12
+ [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](LICENSE)
13
+
14
+ [Install](#install) • [Quick start](#quick-start) • [API](#api) • [How it works](#how-it-works) • [Advanced usage](#advanced-usage)
15
+
16
+ </div>
17
+
18
+ A TypeScript library for downloading TikTok livestreams. Pass a username, it records the stream in crash-safe `.ts` segments, applies EBU R128 audio normalization, and remuxes to `.mp4`. Uses [impit](https://github.com/apify/impit) for browser TLS fingerprint emulation to bypass bot detection, and [ffmpeg](https://ffmpeg.org) for audio normalization and container remuxing.
19
+
20
+ > [!NOTE]
21
+ > This project is a reverse-engineering effort and is not affiliated with TikTok. Use at your own risk.
22
+
23
+ ## Features
24
+
25
+ - **One-shot or event-driven** — use the `download()` function for simplicity, or `TikTokLiveDownloader` for full control with progress and segment events.
26
+ - **Browser TLS emulation** — uses `impit` with Chrome fingerprints to bypass TikTok's bot detection.
27
+ - **System ffmpeg** — auto-detects ffmpeg on PATH; falls back to raw FLV download if not found.
28
+ - **EBU R128 audio normalization** — two-pass loudnorm (equivalent to `ffmpeg-normalize --preset streaming-video`), always applied.
29
+ - **Crash-safe `.ts` intermediate** — saves stream as MPEG-TS first (playable at any cut point), then remuxes to `.mp4`.
30
+ - **Automatic quality selection** — picks the best available quality (1080p → 720p → 540p → 360p).
31
+ - **Segment mode** — split long streams into configurable parts (e.g. 20min each) for reliability.
32
+ - **Wait-for-live mode** — polls periodically and starts recording when the user goes live.
33
+ - **Graceful stop & abort** — stop cleanly (keeps partial file) or abort immediately with `AbortSignal` support.
34
+ - **Proxy & cookie support** — HTTP/SOCKS proxies and cookie jars for authenticated streams.
35
+ - **Standalone utilities** — use `resolveRoomId()`, `fetchStreamInfo()`, or `createClient()` independently.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ npm install tokwatchr
41
+ # or
42
+ bun add tokwatchr
43
+ ```
44
+
45
+ > [!TIP]
46
+ > **System requirements:** [ffmpeg](https://ffmpeg.org) must be installed on your system for audio normalization and `.mp4` output. Without it, the library falls back to raw FLV download. On macOS `brew install ffmpeg`, on Ubuntu `sudo apt install ffmpeg`.
47
+
48
+ ## Quick start
49
+
50
+ ### One-shot download
51
+
52
+ ```ts
53
+ import { download } from "tokwatchr";
54
+
55
+ const result = await download("officialgeilegisela", {
56
+ output: "./recordings",
57
+ });
58
+
59
+ console.log(`Saved to ${result.filePath}`);
60
+ // → ./recordings/officialgeilegisela=20260604_143022.mp4
61
+ ```
62
+
63
+ ### With progress events
64
+
65
+ ```ts
66
+ import { TikTokLiveDownloader } from "tokwatchr";
67
+
68
+ const d = new TikTokLiveDownloader("tv_asahi_news", {
69
+ output: "./vods",
70
+ maxDuration: 7_200, // 2 hours
71
+ });
72
+
73
+ d.on("progress", (stats) => {
74
+ console.log(
75
+ `${stats.downloadedMB.toFixed(1)}MB @ ${stats.speedMBps.toFixed(1)}MB/s`,
76
+ );
77
+ });
78
+
79
+ d.on("complete", (results) => {
80
+ for (const r of results) {
81
+ console.log(`Done: ${r.filePath} (${r.sizeMB.toFixed(1)}MB)`);
82
+ }
83
+ });
84
+
85
+ d.on("error", (err) => {
86
+ console.error("Recording failed:", err.message);
87
+ });
88
+
89
+ await d.start();
90
+ ```
91
+
92
+ ### Segmented recording (20min parts, non-blocking remux)
93
+
94
+ ```ts
95
+ const d = new TikTokLiveDownloader("username", {
96
+ output: "./recordings",
97
+ maxSegmentDuration: 1200, // 20 minutes per segment
98
+ });
99
+
100
+ d.on("segment", (result, partNum) => {
101
+ console.log(`Part ${partNum} done: ${result.filePath}`);
102
+ });
103
+
104
+ d.on("complete", (results) => {
105
+ console.log(`All ${results.length} segments complete`);
106
+ });
107
+
108
+ await d.start();
109
+ ```
110
+
111
+ ## API
112
+
113
+ ### `download(username, options?)`
114
+
115
+ Functional shorthand. Returns a `Promise<DownloadResult>` (last segment when segmented).
116
+
117
+ ```ts
118
+ import { download } from "tokwatchr";
119
+
120
+ const result = await download("username", {
121
+ output: "./vods",
122
+ quality: "best",
123
+ onProgress: (s) => console.log(s.downloadedMB),
124
+ });
125
+ ```
126
+
127
+ ### `new TikTokLiveDownloader(username, options?)`
128
+
129
+ Class-based API with events and lifecycle control.
130
+
131
+ ```ts
132
+ import { TikTokLiveDownloader } from "tokwatchr";
133
+
134
+ const d = new TikTokLiveDownloader("username", {
135
+ output: "./vods",
136
+ quality: "hd1",
137
+ format: "ts", // keep as .ts (no remux)
138
+ proxyUrl: "socks5://localhost:1080",
139
+ });
140
+ ```
141
+
142
+ #### Events
143
+
144
+ | Event | Payload | Description |
145
+ |---|---|---|
146
+ | `start` | `StreamInfo` | Stream URL resolved, recording starting |
147
+ | `progress` | `DownloadStats` | Emitted every ~1s during recording |
148
+ | `segment` | `[result: DownloadResult, partNumber: number]` | A segment completed (only when `maxSegmentDuration` is set) |
149
+ | `complete` | `DownloadResult[]` | All segments done, remuxed files ready |
150
+ | `error` | `Error` | An error occurred |
151
+ | `stop` | — | Recording was stopped via `stop()` |
152
+
153
+ #### Methods
154
+
155
+ | Method | Returns | Description |
156
+ |---|---|---|
157
+ | `start()` | `Promise<DownloadResult>` | Wait for live, then record |
158
+ | `startRecording()` | `Promise<DownloadResult>` | Record now (fails if not live) |
159
+ | `waitForLive()` | `Promise<StreamInfo>` | Just wait, don't record |
160
+ | `stop()` | `Promise<void>` | Graceful stop (remuxes pending segments) |
161
+ | `abort()` | `void` | Immediate abort |
162
+ | `state` | `DownloaderState` | `"idle"` \| `"waiting"` \| `"recording"` \| `"stopping"` \| `"done"` |
163
+
164
+ ### Options
165
+
166
+ ```ts
167
+ interface TikTokLiveDownloaderOptions {
168
+ output?: string; // Output directory (default: process.cwd())
169
+ filename?: string; // Template: {username}, {date}, {time}, {title}, {part}
170
+ quality?: "best" | "worst" // Quality preference (default: "best")
171
+ | "fullhd1" | "hd1" | "sd2" | "sd1";
172
+ format?: "mp4" | "mkv" | "ts" | "flv"; // Output container (default: "mp4")
173
+ useFfmpeg?: boolean; // Auto-detects system ffmpeg (default: true if found)
174
+ ffmpegPath?: string; // Custom ffmpeg binary path
175
+ ffmpegArgs?: string[]; // Extra ffmpeg args (default: ["-c", "copy"])
176
+ bitrate?: string; // Re-encode bitrate (e.g. "1M")
177
+ maxDuration?: number; // Seconds before auto-stop (default: Infinity)
178
+ maxSegmentDuration?: number; // Split into segments this many seconds long
179
+ checkInterval?: number; // Poll interval for wait-for-live (ms, default: 30_000)
180
+ proxyUrl?: string; // HTTP/SOCKS proxy URL
181
+ cookieJar?: CookieJarLike; // tough-cookie compatible jar
182
+ browser?: Browser; // impit browser preset (default: "chrome")
183
+ timeout?: number; // Request timeout ms (default: 30_000)
184
+ headers?: Record<string, string>; // Extra HTTP headers
185
+ signal?: AbortSignal; // External cancellation
186
+ // Callbacks (functional shorthand):
187
+ onStart?: (info: StreamInfo) => void;
188
+ onProgress?: (stats: DownloadStats) => void;
189
+ onError?: (err: Error) => void;
190
+ }
191
+ ```
192
+
193
+ ### Types
194
+
195
+ ```ts
196
+ interface StreamInfo {
197
+ roomId: string;
198
+ username: string;
199
+ title: string;
200
+ qualities: QualityOption[];
201
+ selectedQuality: QualityOption;
202
+ streamUrl: string;
203
+ viewerCount: number;
204
+ startedAt: Date;
205
+ }
206
+
207
+ interface DownloadStats {
208
+ downloadedBytes: number;
209
+ downloadedMB: number;
210
+ duration: number; // seconds elapsed
211
+ speed: number; // bytes/sec
212
+ speedMBps: number;
213
+ quality: StreamQualityKey;
214
+ state: DownloaderState;
215
+ }
216
+
217
+ interface DownloadResult {
218
+ filePath: string;
219
+ sizeBytes: number;
220
+ sizeMB: number;
221
+ duration: number; // seconds of content
222
+ username: string;
223
+ roomId: string;
224
+ quality: StreamQualityKey;
225
+ format: OutputFormat; // "mp4" | "mkv" | "ts" | "flv"
226
+ startedAt: Date;
227
+ endedAt: Date;
228
+ }
229
+ ```
230
+
231
+ ### Error classes
232
+
233
+ ```ts
234
+ import {
235
+ TikTokLiveError, // Base error class
236
+ UserOfflineError, // User is not live
237
+ RoomResolveError, // Could not find room ID
238
+ StreamFetchError, // Could not get stream URL
239
+ DownloadFailedError, // Download failed mid-stream
240
+ FfmpegError, // ffmpeg subprocess error
241
+ AbortError, // Request was aborted
242
+ } from "tokwatchr";
243
+ ```
244
+
245
+ ## How it works
246
+
247
+ ```
248
+ Username
249
+
250
+ ├─ GET @{user}/live (HTML scrape for roomId)
251
+ │ └─ fallback: /api-live/user/room/
252
+
253
+
254
+ Room ID
255
+
256
+ ├─ GET /webcast/room/info/ (fetch stream URLs + qualities)
257
+
258
+
259
+ FLV endpoint ────► 1080p | 720p | 540p | 360p
260
+
261
+ ├─ With ffmpeg:
262
+ │ ffmpeg -i <flv_url> -c copy segment.ts (crash-safe TS)
263
+ │ → measure loudness with loudnorm
264
+ │ → remux with AAC encode + EBU R128 normalization
265
+ │ → segment.mp4 (final output)
266
+
267
+ └─ Without ffmpeg:
268
+ HTTP stream → file.flv
269
+ ```
270
+
271
+ The download process:
272
+
273
+ 1. **Room ID resolution** — scrapes the user's TikTok live page for the room ID embedded in `SIGI_STATE`. Falls back to the `api-live/user/room/` API endpoint.
274
+ 2. **Stream URL fetch** — calls `webcast/room/info/` to get available stream qualities. Selects the best available (1080p → 720p → 540p → 360p).
275
+ 3. **Download to `.ts`** — saves the raw stream as MPEG-TS, which is playable even if truncated mid-stream.
276
+ 4. **Remux with normalization** — two-pass EBU R128 loudnorm to -14 LUFS (streaming standard), AAC encode at 128k, video copied without re-encode.
277
+ 5. **Segment loop** — if `maxSegmentDuration` is set, the process repeats: download, remux, emit `segment`, check for live, next segment.
278
+
279
+ All HTTP requests use `impit` with Chrome TLS fingerprint emulation to bypass bot detection.
280
+
281
+ ## Advanced usage
282
+
283
+ ### Standalone utilities
284
+
285
+ ```ts
286
+ import { resolveRoomId, fetchStreamInfo, createClient } from "tokwatchr";
287
+
288
+ const impit = createClient({ browser: "chrome" });
289
+
290
+ const roomId = await resolveRoomId("username", impit);
291
+ const info = await fetchStreamInfo(roomId, "username", impit, {
292
+ quality: "best",
293
+ });
294
+
295
+ console.log(info.streamUrl); // FLV URL
296
+ ```
297
+
298
+ ### Custom filename template
299
+
300
+ ```ts
301
+ import { renderFilename } from "tokwatchr";
302
+
303
+ const name = renderFilename("{username}={date}_{time}", {
304
+ username: "testuser",
305
+ title: "My Stream Title",
306
+ });
307
+ // → "testuser=20260604_143022"
308
+ ```
309
+
310
+ ### Segmented download with custom part template
311
+
312
+ ```ts
313
+ const d = new TikTokLiveDownloader("username", {
314
+ maxSegmentDuration: 600, // 10 min segments
315
+ filename: "{username}_{title}_part{part}",
316
+ });
317
+ // → "officialgeilegisela_Live_Stream_part1.mp4"
318
+ // → "officialgeilegisela_Live_Stream_part2.mp4"
319
+ ```
320
+
321
+ ### Using a proxy
322
+
323
+ ```ts
324
+ const d = new TikTokLiveDownloader("username", {
325
+ proxyUrl: "http://user:pass@proxy:8080",
326
+ browser: "chrome",
327
+ });
328
+ ```
329
+
330
+ ### Authenticated streams (cookies)
331
+
332
+ ```ts
333
+ import { CookieJar } from "tough-cookie";
334
+
335
+ const jar = new CookieJar();
336
+ await jar.setCookie("sessionid=abc123", "https://www.tiktok.com");
337
+
338
+ const d = new TikTokLiveDownloader("username", {
339
+ cookieJar: jar,
340
+ });
341
+ ```
342
+
343
+ ### Abort via `AbortSignal`
344
+
345
+ ```ts
346
+ const controller = new AbortController();
347
+
348
+ const d = new TikTokLiveDownloader("username", {
349
+ signal: controller.signal,
350
+ });
351
+
352
+ setTimeout(() => controller.abort(), 10_000); // 10s timeout
353
+ await d.start().catch((err) => {
354
+ if (err.name === "AbortError") {
355
+ console.log("Timed out");
356
+ }
357
+ });
358
+ ```
359
+
360
+ ### Using your own ffmpeg
361
+
362
+ ```ts
363
+ const d = new TikTokLiveDownloader("username", {
364
+ ffmpegPath: "/usr/local/bin/ffmpeg",
365
+ ffmpegArgs: ["-c:v", "libx264", "-preset", "fast", "-c:a", "aac"],
366
+ bitrate: "2M",
367
+ });
368
+ ```
@@ -0,0 +1,327 @@
1
+ import { Browser, Impit } from "impit";
2
+
3
+ //#region src/types.d.ts
4
+ type StreamQualityKey = "fullhd1" | "hd1" | "sd2" | "sd1";
5
+ interface QualityOption {
6
+ key: StreamQualityKey;
7
+ label: string;
8
+ level: number;
9
+ flv: string;
10
+ hls: string;
11
+ }
12
+ interface StreamInfo {
13
+ roomId: string;
14
+ username: string;
15
+ title: string;
16
+ qualities: QualityOption[];
17
+ selectedQuality: QualityOption;
18
+ streamUrl: string;
19
+ viewerCount: number;
20
+ startedAt: Date;
21
+ }
22
+ interface DownloadStats {
23
+ downloadedBytes: number;
24
+ downloadedMB: number;
25
+ duration: number;
26
+ speed: number;
27
+ speedMBps: number;
28
+ quality: StreamQualityKey;
29
+ state: DownloaderState;
30
+ }
31
+ interface DownloadResult {
32
+ filePath: string;
33
+ sizeBytes: number;
34
+ sizeMB: number;
35
+ duration: number;
36
+ username: string;
37
+ roomId: string;
38
+ quality: StreamQualityKey;
39
+ format: OutputFormat;
40
+ startedAt: Date;
41
+ endedAt: Date;
42
+ }
43
+ type DownloaderState = "idle" | "waiting" | "recording" | "stopping" | "done";
44
+ type OutputFormat = "mp4" | "mkv" | "ts" | "flv";
45
+ interface TikTokLiveDownloaderOptions {
46
+ /** Output directory (default: process.cwd()) */
47
+ output?: string;
48
+ /** Filename template. Variables: {username}, {date}, {time}, {title} (default: "{username}={date}_{time}") */
49
+ filename?: string;
50
+ /** Quality preference (default: "best") */
51
+ quality?: "best" | "worst" | StreamQualityKey;
52
+ /** Output container format (default: "mp4" when ffmpeg available, "flv" otherwise) */
53
+ format?: OutputFormat;
54
+ /** Use ffmpeg for download + remux. Auto-detects system ffmpeg on PATH. */
55
+ useFfmpeg?: boolean;
56
+ /** Custom ffmpeg binary path. Overrides auto-detected system ffmpeg. */
57
+ ffmpegPath?: string;
58
+ /** Extra ffmpeg output arguments (default: ["-c", "copy"]). Set to override. */
59
+ ffmpegArgs?: string[];
60
+ /** Re-encode bitrate (e.g. "1M", "1000k"). Default: copy streams (no re-encode). */
61
+ bitrate?: string;
62
+ /** Max recording duration in seconds (default: Infinity). */
63
+ maxDuration?: number;
64
+ /** Split recording into segments of this many seconds (default: Infinity = single file). */
65
+ maxSegmentDuration?: number;
66
+ /** Polling interval in ms when waiting for live (default: 30_000). */
67
+ checkInterval?: number;
68
+ /** HTTP proxy URL (supports http, https, socks4, socks5). */
69
+ proxyUrl?: string;
70
+ /** Tough-cookie compatible cookie jar for authenticated sessions. */
71
+ cookieJar?: CookieJarLike;
72
+ /** Browser to emulate (default: "chrome"). */
73
+ browser?: Browser;
74
+ /** Request timeout in ms (default: 30_000). */
75
+ timeout?: number;
76
+ /** Extra headers to send with every request. */
77
+ headers?: Record<string, string>;
78
+ /** Progress callback (functional shorthand). */
79
+ onProgress?: (stats: DownloadStats) => void;
80
+ /** Start callback (functional shorthand). */
81
+ onStart?: (info: StreamInfo) => void;
82
+ /** Error callback (functional shorthand). */
83
+ onError?: (err: Error) => void;
84
+ /** AbortSignal for cancellation. */
85
+ signal?: AbortSignal;
86
+ }
87
+ interface CookieJarLike {
88
+ setCookie(raw: string, url: string): Promise<void>;
89
+ getCookieString(url: string): Promise<string>;
90
+ }
91
+ interface TikTokLiveDownloaderEvents {
92
+ /** Emitted when a live stream is detected and we're about to start recording. */
93
+ start: [info: StreamInfo];
94
+ /** Emitted periodically (every ~1s) during recording with current stats. */
95
+ progress: [stats: DownloadStats];
96
+ /** Emitted when each segment completes. Only fires when maxSegmentDuration is set. */
97
+ segment: [result: DownloadResult, partNumber: number];
98
+ /** Emitted when all segments are done and the stream has ended. */
99
+ complete: [results: DownloadResult[]];
100
+ /** Emitted on any error. The downloader will also throw. */
101
+ error: [err: Error];
102
+ /** Emitted when stop() is called and cleanup is done. */
103
+ stop: [];
104
+ }
105
+ //#endregion
106
+ //#region src/api/client.d.ts
107
+ interface CreateClientOptions {
108
+ browser?: Browser;
109
+ proxyUrl?: string | null;
110
+ timeout?: number;
111
+ headers?: Record<string, string>;
112
+ cookieJar?: CookieJarLike | null;
113
+ }
114
+ /**
115
+ * Create an `Impit` HTTP client configured for TikTok.
116
+ *
117
+ * Uses browser TLS fingerprint emulation to bypass bot detection.
118
+ */
119
+ declare function createClient(options?: CreateClientOptions): Impit;
120
+ //#endregion
121
+ //#region src/api/room.d.ts
122
+ interface RoomResolveOptions {
123
+ impIt?: Impit;
124
+ signal?: AbortSignal;
125
+ }
126
+ /**
127
+ * Resolve a TikTok username to a live room ID.
128
+ *
129
+ * Strategy:
130
+ * 0. Verify the user exists (profile page check).
131
+ * 1. Scrape the user's live page HTML for the room ID in SIGI_STATE.
132
+ * 2. Fall back to the TikTok API.
133
+ * 3. Throw if neither works.
134
+ */
135
+ declare function resolveRoomId(username: string, impIt: Impit, options?: RoomResolveOptions): Promise<string>;
136
+ //#endregion
137
+ //#region src/api/stream.d.ts
138
+ interface StreamInfoOptions {
139
+ quality?: "best" | "worst" | StreamQualityKey;
140
+ signal?: AbortSignal;
141
+ }
142
+ /**
143
+ * Fetch stream info for a given room ID.
144
+ *
145
+ * Calls the TikTok webcast room/info API and parses the response
146
+ * to extract stream URLs and quality options.
147
+ */
148
+ declare function fetchStreamInfo(roomId: string, username: string, impIt: Impit, options?: StreamInfoOptions): Promise<StreamInfo>;
149
+ //#endregion
150
+ //#region src/errors.d.ts
151
+ declare class TikTokLiveError extends Error {
152
+ name: string;
153
+ }
154
+ declare class UserOfflineError extends TikTokLiveError {
155
+ name: string;
156
+ constructor(username: string);
157
+ }
158
+ declare class UserNotFoundError extends TikTokLiveError {
159
+ name: string;
160
+ constructor(username: string);
161
+ }
162
+ declare class RoomResolveError extends TikTokLiveError {
163
+ name: string;
164
+ constructor(username: string, cause?: unknown);
165
+ }
166
+ declare class StreamFetchError extends TikTokLiveError {
167
+ name: string;
168
+ constructor(roomId: string, cause?: unknown);
169
+ }
170
+ declare class DownloadFailedError extends TikTokLiveError {
171
+ name: string;
172
+ constructor(message: string, cause?: unknown);
173
+ }
174
+ declare class FfmpegError extends TikTokLiveError {
175
+ name: string;
176
+ constructor(message: string, exitCode?: number | null);
177
+ }
178
+ declare class AbortError extends TikTokLiveError {
179
+ name: string;
180
+ constructor();
181
+ }
182
+ //#endregion
183
+ //#region src/TikTokLiveDownloader.d.ts
184
+ /**
185
+ * Main class for downloading TikTok livestreams.
186
+ *
187
+ * Usage:
188
+ * ```ts
189
+ * const d = new TikTokLiveDownloader('username')
190
+ * d.on('progress', s => console.log(`${s.downloadedMB}MB`))
191
+ * const result = await d.start()
192
+ * ```
193
+ */
194
+ declare class TikTokLiveDownloader {
195
+ private readonly username;
196
+ private readonly options;
197
+ private readonly emitter;
198
+ private readonly impIt;
199
+ private _state;
200
+ private abortController;
201
+ private _stats;
202
+ private _result;
203
+ constructor(username: string, opts?: TikTokLiveDownloaderOptions);
204
+ on<E extends keyof TikTokLiveDownloaderEvents>(event: E, listener: (...args: TikTokLiveDownloaderEvents[E]) => void): this;
205
+ once<E extends keyof TikTokLiveDownloaderEvents>(event: E, listener: (...args: TikTokLiveDownloaderEvents[E]) => void): this;
206
+ off<E extends keyof TikTokLiveDownloaderEvents>(event: E, listener: (...args: TikTokLiveDownloaderEvents[E]) => void): this;
207
+ private emit;
208
+ get state(): DownloaderState;
209
+ get stats(): DownloadStats | null;
210
+ get result(): DownloadResult | null;
211
+ /**
212
+ * Start recording. If the user is not currently live, polls
213
+ * until they go live and then starts recording.
214
+ *
215
+ * Resolves when the stream ends or maxDuration is reached.
216
+ */
217
+ start(): Promise<DownloadResult>;
218
+ /**
219
+ * Start recording immediately. Throws if the user is not live.
220
+ */
221
+ startRecording(): Promise<DownloadResult>;
222
+ /**
223
+ * Wait until the user goes live (does not record).
224
+ */
225
+ waitForLive(): Promise<StreamInfo>;
226
+ /**
227
+ * Gracefully stop the recording.
228
+ */
229
+ stop(): Promise<void>;
230
+ /**
231
+ * Immediately abort the recording.
232
+ */
233
+ abort(): void;
234
+ private setState;
235
+ private resolveOptions;
236
+ private _run;
237
+ private resolveRoomIdOnce;
238
+ private resolveRoomIdWithRetry;
239
+ private fetchStreamInfo;
240
+ /**
241
+ * Download a single segment. Always downloads to .ts (crash-safe).
242
+ * Does NOT remux — that is handled by remuxSegment() running in the background.
243
+ */
244
+ private downloadSegment;
245
+ /**
246
+ * Remux a downloaded .ts segment to the target format with audio normalization.
247
+ * Runs in background, does not block the download loop.
248
+ *
249
+ * If remux fails, keeps the .ts as a playable fallback.
250
+ */
251
+ private remuxSegment;
252
+ /**
253
+ * Remux a .ts file to the target container with EBU R128 audio normalization.
254
+ *
255
+ * Two-pass loudnorm (equivalent to `ffmpeg-normalize --preset streaming-video -c:a aac`):
256
+ * 1. Measure integrated loudness, LRA, true peak
257
+ * 2. Apply linear normalization + encode AAC + copy video
258
+ *
259
+ * If the measurement pass fails (short file, edge case), falls back to
260
+ * plain AAC encode without loudnorm.
261
+ */
262
+ private remuxAndNormalize;
263
+ /**
264
+ * Measure loudness of a .ts file using ffmpeg's loudnorm filter.
265
+ *
266
+ * Runs `loudnorm` with `print_format=json` and parses the JSON
267
+ * output from stderr. Returns the measured values for use in
268
+ * the second pass, or null if measurement failed.
269
+ */
270
+ private measureLoudness;
271
+ }
272
+ interface DownloadFunctionOptions extends TikTokLiveDownloaderOptions {
273
+ /** Override the target username (defaults to the one passed to download()). */
274
+ username?: string;
275
+ }
276
+ /**
277
+ * Download a TikTok livestream.
278
+ *
279
+ * Simplest one-shot API. Resolves when the stream ends.
280
+ *
281
+ * @param username - TikTok username (with or without @).
282
+ * @param options - Download options and callbacks.
283
+ */
284
+ declare function download(username: string, options?: DownloadFunctionOptions): Promise<DownloadResult>;
285
+ //#endregion
286
+ //#region src/utils/quality.d.ts
287
+ /**
288
+ * Build a QualityOption from a raw url object.
289
+ */
290
+ declare function buildQualityOption(key: StreamQualityKey, flv: string, hls?: string): QualityOption;
291
+ /**
292
+ * Select a quality from available options.
293
+ *
294
+ * @param qualities - Available quality options to choose from.
295
+ * @param preference - "best" (highest available), "worst" (lowest), or a specific key.
296
+ * @returns The selected quality option.
297
+ */
298
+ declare function selectQuality(qualities: QualityOption[], preference: "best" | "worst" | StreamQualityKey): QualityOption;
299
+ /**
300
+ * Parse quality options from the TikTok room/info response.
301
+ *
302
+ * Supports both the legacy `flv_pull_url` format and the newer
303
+ * `live_core_sdk_data` format.
304
+ */
305
+ declare function parseQualities(data: unknown): QualityOption[];
306
+ //#endregion
307
+ //#region src/utils/template.d.ts
308
+ /**
309
+ * Render a filename template with runtime values.
310
+ *
311
+ * Available variables:
312
+ * - {username} - TikTok username
313
+ * - {date} - Current date in YYYYMMDD format
314
+ * - {time} - Current time in HHmmss format
315
+ * - {title} - Stream title (sanitized)
316
+ *
317
+ * Default template: "{username}={date}_{time}"
318
+ */
319
+ declare function renderFilename(template: string, values: {
320
+ username: string;
321
+ title?: string;
322
+ date?: string;
323
+ time?: string;
324
+ part?: number;
325
+ }): string;
326
+ //#endregion
327
+ export { AbortError, type CookieJarLike, type CreateClientOptions, DownloadFailedError, type DownloadFunctionOptions, type DownloadResult, type DownloadStats, type DownloaderState, FfmpegError, type OutputFormat, type QualityOption, RoomResolveError, type RoomResolveOptions, StreamFetchError, type StreamInfo, type StreamInfoOptions, type StreamQualityKey, TikTokLiveDownloader, type TikTokLiveDownloaderEvents, type TikTokLiveDownloaderOptions, TikTokLiveError, UserNotFoundError, UserOfflineError, buildQualityOption, createClient, download, fetchStreamInfo, parseQualities, renderFilename, resolveRoomId, selectQuality };