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 +368 -0
- package/dist/index.d.mts +327 -0
- package/dist/index.mjs +1146 -0
- package/package.json +55 -0
- package/src/TikTokLiveDownloader.ts +794 -0
- package/src/api/client.ts +46 -0
- package/src/api/room.ts +193 -0
- package/src/api/stream.ts +76 -0
- package/src/download/ffmpeg.ts +303 -0
- package/src/download/raw-http.ts +150 -0
- package/src/errors.ts +58 -0
- package/src/index.ts +45 -0
- package/src/types.ts +155 -0
- package/src/utils/quality.ts +171 -0
- package/src/utils/template.ts +67 -0
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
|
+
[](https://www.npmjs.com/package/tokwatchr)
|
|
9
|
+
[](https://www.npmjs.com/package/tokwatchr)
|
|
10
|
+
[](https://www.typescriptlang.org)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](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
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|