tokwatchr 0.5.0 → 0.6.1
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/dist/index.d.mts +15 -3
- package/dist/index.mjs +27 -11
- package/package.json +1 -1
- package/src/TikTokLiveDownloader.ts +27 -6
- package/src/download/ffmpeg.ts +6 -6
- package/src/errors.ts +2 -2
- package/src/index.ts +1 -0
- package/src/types.ts +15 -2
package/dist/index.d.mts
CHANGED
|
@@ -63,7 +63,7 @@ interface TikTokLiveDownloaderOptions {
|
|
|
63
63
|
maxDuration?: number;
|
|
64
64
|
/** Split recording into segments of this many seconds (default: Infinity = single file). */
|
|
65
65
|
maxSegmentDuration?: number;
|
|
66
|
-
/** Polling interval in
|
|
66
|
+
/** Polling interval in seconds when waiting for live (default: 180). */
|
|
67
67
|
checkInterval?: number;
|
|
68
68
|
/** HTTP proxy URL (supports http, https, socks4, socks5). */
|
|
69
69
|
proxyUrl?: string;
|
|
@@ -71,7 +71,7 @@ interface TikTokLiveDownloaderOptions {
|
|
|
71
71
|
cookieJar?: CookieJarLike;
|
|
72
72
|
/** Browser to emulate (default: "chrome"). */
|
|
73
73
|
browser?: Browser;
|
|
74
|
-
/** Request timeout in
|
|
74
|
+
/** Request timeout in seconds (default: 30). */
|
|
75
75
|
timeout?: number;
|
|
76
76
|
/** Extra headers to send with every request. */
|
|
77
77
|
headers?: Record<string, string>;
|
|
@@ -96,9 +96,21 @@ interface WaitingInfo {
|
|
|
96
96
|
/** Seconds elapsed since this phase started. */
|
|
97
97
|
elapsed: number;
|
|
98
98
|
}
|
|
99
|
+
interface RemuxInfo {
|
|
100
|
+
/** Input .ts file path. */
|
|
101
|
+
filePath: string;
|
|
102
|
+
/** Input file size in MB. */
|
|
103
|
+
inputSizeMB: number;
|
|
104
|
+
/** Remux status: started → completed (with outputPath) or failed (keeping .ts). */
|
|
105
|
+
status: "started" | "completed" | "failed";
|
|
106
|
+
/** Output file path. Only set when status is "completed". */
|
|
107
|
+
outputPath?: string;
|
|
108
|
+
}
|
|
99
109
|
interface TikTokLiveDownloaderEvents {
|
|
100
110
|
/** Emitted periodically while waiting for a room or stream to become active. */
|
|
101
111
|
waiting: [info: WaitingInfo];
|
|
112
|
+
/** Emitted when a remux operation starts, completes, or fails. */
|
|
113
|
+
remux: [info: RemuxInfo];
|
|
102
114
|
/** Emitted when a live stream is detected and we're about to start recording. */
|
|
103
115
|
start: [info: StreamInfo];
|
|
104
116
|
/** Emitted periodically (every ~1s) during recording with current stats. */
|
|
@@ -340,4 +352,4 @@ declare function renderFilename(template: string, values: {
|
|
|
340
352
|
part?: number;
|
|
341
353
|
}): string;
|
|
342
354
|
//#endregion
|
|
343
|
-
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, type WaitingInfo, buildQualityOption, createClient, download, fetchStreamInfo, parseQualities, renderFilename, resolveRoomId, selectQuality };
|
|
355
|
+
export { AbortError, type CookieJarLike, type CreateClientOptions, DownloadFailedError, type DownloadFunctionOptions, type DownloadResult, type DownloadStats, type DownloaderState, FfmpegError, type OutputFormat, type QualityOption, type RemuxInfo, RoomResolveError, type RoomResolveOptions, StreamFetchError, type StreamInfo, type StreamInfoOptions, type StreamQualityKey, TikTokLiveDownloader, type TikTokLiveDownloaderEvents, type TikTokLiveDownloaderOptions, TikTokLiveError, UserNotFoundError, UserOfflineError, type WaitingInfo, buildQualityOption, createClient, download, fetchStreamInfo, parseQualities, renderFilename, resolveRoomId, selectQuality };
|
package/dist/index.mjs
CHANGED
|
@@ -31,7 +31,7 @@ var TikTokLiveError = class extends Error {
|
|
|
31
31
|
var UserOfflineError = class extends TikTokLiveError {
|
|
32
32
|
name = "UserOfflineError";
|
|
33
33
|
constructor(username) {
|
|
34
|
-
super(`User "${username}" is not currently live`);
|
|
34
|
+
super(`User "${username}" is not currently live (or does not exist)`);
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
var UserNotFoundError = class extends TikTokLiveError {
|
|
@@ -43,7 +43,7 @@ var UserNotFoundError = class extends TikTokLiveError {
|
|
|
43
43
|
var RoomResolveError = class extends TikTokLiveError {
|
|
44
44
|
name = "RoomResolveError";
|
|
45
45
|
constructor(username, cause) {
|
|
46
|
-
super(`Failed to resolve room ID for "${username}"${cause ? `: ${cause}` : ""}`);
|
|
46
|
+
super(`Failed to resolve room ID for "${username}" — may not exist or be offline${cause ? `: ${cause}` : ""}`);
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
var StreamFetchError = class extends TikTokLiveError {
|
|
@@ -368,7 +368,7 @@ function parseFfmpegProgress(line) {
|
|
|
368
368
|
* - Better handling of stream interruptions
|
|
369
369
|
*/
|
|
370
370
|
async function downloadWithFfmpeg(options) {
|
|
371
|
-
const { ffmpegPath, url, outputPath, quality, args: extraArgs, bitrate, signal, onProgress, maxDuration, timeout =
|
|
371
|
+
const { ffmpegPath, url, outputPath, quality, args: extraArgs, bitrate, signal, onProgress, maxDuration, timeout = 30 } = options;
|
|
372
372
|
const ffmpegArgs = [
|
|
373
373
|
"-y",
|
|
374
374
|
"-i",
|
|
@@ -394,8 +394,8 @@ async function downloadWithFfmpeg(options) {
|
|
|
394
394
|
let firstDataTimer = setTimeout(() => {
|
|
395
395
|
firstDataTimer = null;
|
|
396
396
|
proc.kill("SIGTERM");
|
|
397
|
-
reject(new FfmpegError(`ffmpeg produced no output within ${timeout}
|
|
398
|
-
}, timeout);
|
|
397
|
+
reject(new FfmpegError(`ffmpeg produced no output within ${timeout}s — check the stream URL`));
|
|
398
|
+
}, timeout * 1e3);
|
|
399
399
|
proc.stderr?.on("data", (chunk) => {
|
|
400
400
|
if (firstDataTimer) {
|
|
401
401
|
clearTimeout(firstDataTimer);
|
|
@@ -620,11 +620,11 @@ const DEFAULT_OPTIONS = {
|
|
|
620
620
|
bitrate: null,
|
|
621
621
|
maxDuration: Infinity,
|
|
622
622
|
maxSegmentDuration: Infinity,
|
|
623
|
-
checkInterval:
|
|
623
|
+
checkInterval: 180,
|
|
624
624
|
proxyUrl: null,
|
|
625
625
|
cookieJar: null,
|
|
626
626
|
browser: "chrome",
|
|
627
|
-
timeout:
|
|
627
|
+
timeout: 30,
|
|
628
628
|
headers: {},
|
|
629
629
|
onProgress: null,
|
|
630
630
|
onStart: null,
|
|
@@ -839,7 +839,7 @@ var TikTokLiveDownloader = class {
|
|
|
839
839
|
}
|
|
840
840
|
async resolveRoomIdWithRetry() {
|
|
841
841
|
const interval = this.options.checkInterval;
|
|
842
|
-
const maxInterval = Math.max(interval,
|
|
842
|
+
const maxInterval = Math.max(interval, 180);
|
|
843
843
|
const waitStart = Date.now();
|
|
844
844
|
while (true) {
|
|
845
845
|
this.abortController.signal?.throwIfAborted();
|
|
@@ -850,7 +850,7 @@ var TikTokLiveDownloader = class {
|
|
|
850
850
|
phase: "room",
|
|
851
851
|
elapsed: (Date.now() - waitStart) / 1e3
|
|
852
852
|
});
|
|
853
|
-
await sleep(maxInterval);
|
|
853
|
+
await sleep(maxInterval * 1e3);
|
|
854
854
|
}
|
|
855
855
|
}
|
|
856
856
|
async fetchStreamInfo(roomId) {
|
|
@@ -869,7 +869,7 @@ var TikTokLiveDownloader = class {
|
|
|
869
869
|
*/
|
|
870
870
|
async pollStreamInfo(roomId) {
|
|
871
871
|
const interval = this.options.checkInterval;
|
|
872
|
-
const maxInterval = Math.max(interval,
|
|
872
|
+
const maxInterval = Math.max(interval, 180);
|
|
873
873
|
const waitStart = Date.now();
|
|
874
874
|
while (true) {
|
|
875
875
|
this.abortController.signal?.throwIfAborted();
|
|
@@ -882,7 +882,7 @@ var TikTokLiveDownloader = class {
|
|
|
882
882
|
phase: "stream",
|
|
883
883
|
elapsed: (Date.now() - waitStart) / 1e3
|
|
884
884
|
});
|
|
885
|
-
await sleep(maxInterval);
|
|
885
|
+
await sleep(maxInterval * 1e3);
|
|
886
886
|
continue;
|
|
887
887
|
}
|
|
888
888
|
throw err;
|
|
@@ -981,12 +981,23 @@ var TikTokLiveDownloader = class {
|
|
|
981
981
|
const finalExt = targetFormat === "mkv" ? "mkv" : "mp4";
|
|
982
982
|
const finalPath = inputPath.replace(/\.ts$/, `.${finalExt}`);
|
|
983
983
|
if (finalPath === inputPath) return tsResult;
|
|
984
|
+
this.emit("remux", {
|
|
985
|
+
filePath: inputPath,
|
|
986
|
+
inputSizeMB: tsResult.sizeMB,
|
|
987
|
+
status: "started"
|
|
988
|
+
});
|
|
984
989
|
try {
|
|
985
990
|
await this.remuxAndNormalize(this.options.ffmpegPath, inputPath, finalPath);
|
|
986
991
|
try {
|
|
987
992
|
const { unlinkSync } = await import("node:fs");
|
|
988
993
|
unlinkSync(inputPath);
|
|
989
994
|
} catch {}
|
|
995
|
+
this.emit("remux", {
|
|
996
|
+
filePath: inputPath,
|
|
997
|
+
outputPath: finalPath,
|
|
998
|
+
inputSizeMB: tsResult.sizeMB,
|
|
999
|
+
status: "completed"
|
|
1000
|
+
});
|
|
990
1001
|
return {
|
|
991
1002
|
...tsResult,
|
|
992
1003
|
filePath: finalPath,
|
|
@@ -995,6 +1006,11 @@ var TikTokLiveDownloader = class {
|
|
|
995
1006
|
} catch (err) {
|
|
996
1007
|
const msg = err instanceof Error ? err.message : String(err);
|
|
997
1008
|
console.warn(`tokwatchr: remux failed for ${inputPath}, keeping .ts as fallback: ${msg}`);
|
|
1009
|
+
this.emit("remux", {
|
|
1010
|
+
filePath: inputPath,
|
|
1011
|
+
inputSizeMB: tsResult.sizeMB,
|
|
1012
|
+
status: "failed"
|
|
1013
|
+
});
|
|
998
1014
|
return tsResult;
|
|
999
1015
|
}
|
|
1000
1016
|
}
|
package/package.json
CHANGED
|
@@ -36,11 +36,11 @@ const DEFAULT_OPTIONS: ResolvedOptions = {
|
|
|
36
36
|
bitrate: null,
|
|
37
37
|
maxDuration: Infinity,
|
|
38
38
|
maxSegmentDuration: Infinity,
|
|
39
|
-
checkInterval:
|
|
39
|
+
checkInterval: 180,
|
|
40
40
|
proxyUrl: null,
|
|
41
41
|
cookieJar: null,
|
|
42
42
|
browser: "chrome",
|
|
43
|
-
timeout:
|
|
43
|
+
timeout: 30,
|
|
44
44
|
headers: {},
|
|
45
45
|
onProgress: null,
|
|
46
46
|
onStart: null,
|
|
@@ -377,7 +377,7 @@ export class TikTokLiveDownloader {
|
|
|
377
377
|
|
|
378
378
|
private async resolveRoomIdWithRetry(): Promise<string> {
|
|
379
379
|
const interval = this.options.checkInterval;
|
|
380
|
-
const maxInterval = Math.max(interval,
|
|
380
|
+
const maxInterval = Math.max(interval, 180);
|
|
381
381
|
const waitStart = Date.now();
|
|
382
382
|
|
|
383
383
|
while (true) {
|
|
@@ -393,7 +393,7 @@ export class TikTokLiveDownloader {
|
|
|
393
393
|
phase: "room",
|
|
394
394
|
elapsed: (Date.now() - waitStart) / 1_000,
|
|
395
395
|
});
|
|
396
|
-
await sleep(maxInterval);
|
|
396
|
+
await sleep(maxInterval * 1_000);
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
399
|
|
|
@@ -416,7 +416,7 @@ export class TikTokLiveDownloader {
|
|
|
416
416
|
*/
|
|
417
417
|
private async pollStreamInfo(roomId: string): Promise<StreamInfo> {
|
|
418
418
|
const interval = this.options.checkInterval;
|
|
419
|
-
const maxInterval = Math.max(interval,
|
|
419
|
+
const maxInterval = Math.max(interval, 180);
|
|
420
420
|
const waitStart = Date.now();
|
|
421
421
|
|
|
422
422
|
while (true) {
|
|
@@ -431,7 +431,7 @@ export class TikTokLiveDownloader {
|
|
|
431
431
|
phase: "stream",
|
|
432
432
|
elapsed: (Date.now() - waitStart) / 1_000,
|
|
433
433
|
});
|
|
434
|
-
await sleep(maxInterval);
|
|
434
|
+
await sleep(maxInterval * 1_000);
|
|
435
435
|
continue;
|
|
436
436
|
}
|
|
437
437
|
throw err;
|
|
@@ -572,6 +572,12 @@ export class TikTokLiveDownloader {
|
|
|
572
572
|
|
|
573
573
|
if (finalPath === inputPath) return tsResult;
|
|
574
574
|
|
|
575
|
+
this.emit("remux", {
|
|
576
|
+
filePath: inputPath,
|
|
577
|
+
inputSizeMB: tsResult.sizeMB,
|
|
578
|
+
status: "started",
|
|
579
|
+
});
|
|
580
|
+
|
|
575
581
|
try {
|
|
576
582
|
await this.remuxAndNormalize(
|
|
577
583
|
this.options.ffmpegPath,
|
|
@@ -585,6 +591,14 @@ export class TikTokLiveDownloader {
|
|
|
585
591
|
} catch {
|
|
586
592
|
// ignore cleanup failure
|
|
587
593
|
}
|
|
594
|
+
|
|
595
|
+
this.emit("remux", {
|
|
596
|
+
filePath: inputPath,
|
|
597
|
+
outputPath: finalPath,
|
|
598
|
+
inputSizeMB: tsResult.sizeMB,
|
|
599
|
+
status: "completed",
|
|
600
|
+
});
|
|
601
|
+
|
|
588
602
|
return {
|
|
589
603
|
...tsResult,
|
|
590
604
|
filePath: finalPath,
|
|
@@ -595,6 +609,13 @@ export class TikTokLiveDownloader {
|
|
|
595
609
|
console.warn(
|
|
596
610
|
`tokwatchr: remux failed for ${inputPath}, keeping .ts as fallback: ${msg}`,
|
|
597
611
|
);
|
|
612
|
+
|
|
613
|
+
this.emit("remux", {
|
|
614
|
+
filePath: inputPath,
|
|
615
|
+
inputSizeMB: tsResult.sizeMB,
|
|
616
|
+
status: "failed",
|
|
617
|
+
});
|
|
618
|
+
|
|
598
619
|
return tsResult;
|
|
599
620
|
}
|
|
600
621
|
}
|
package/src/download/ffmpeg.ts
CHANGED
|
@@ -13,10 +13,10 @@ export interface FfmpegDownloadOptions {
|
|
|
13
13
|
onProgress?: (stats: DownloadStats) => void;
|
|
14
14
|
maxDuration?: number;
|
|
15
15
|
/**
|
|
16
|
-
* Startup timeout in
|
|
16
|
+
* Startup timeout in seconds. If ffmpeg produces no stderr output within
|
|
17
17
|
* this window, it is killed and the promise rejects. Prevents
|
|
18
18
|
* indefinite hangs on bad/ stalled stream URLs.
|
|
19
|
-
* @default
|
|
19
|
+
* @default 30
|
|
20
20
|
*/
|
|
21
21
|
timeout?: number;
|
|
22
22
|
}
|
|
@@ -89,7 +89,7 @@ export async function downloadWithFfmpeg(
|
|
|
89
89
|
signal,
|
|
90
90
|
onProgress,
|
|
91
91
|
maxDuration,
|
|
92
|
-
timeout =
|
|
92
|
+
timeout = 30,
|
|
93
93
|
} = options;
|
|
94
94
|
|
|
95
95
|
const ffmpegArgs: string[] = [
|
|
@@ -123,7 +123,7 @@ export async function downloadWithFfmpeg(
|
|
|
123
123
|
let duration = 0;
|
|
124
124
|
|
|
125
125
|
// Startup timeout — if ffmpeg produces no stderr output within
|
|
126
|
-
// `timeout`
|
|
126
|
+
// `timeout` seconds, it likely stalled on a bad URL. Kill it so the
|
|
127
127
|
// caller doesn't hang indefinitely.
|
|
128
128
|
let firstDataTimer: ReturnType<typeof setTimeout> | null = setTimeout(
|
|
129
129
|
() => {
|
|
@@ -131,11 +131,11 @@ export async function downloadWithFfmpeg(
|
|
|
131
131
|
proc.kill("SIGTERM");
|
|
132
132
|
reject(
|
|
133
133
|
new FfmpegError(
|
|
134
|
-
`ffmpeg produced no output within ${timeout}
|
|
134
|
+
`ffmpeg produced no output within ${timeout}s — check the stream URL`,
|
|
135
135
|
),
|
|
136
136
|
);
|
|
137
137
|
},
|
|
138
|
-
timeout,
|
|
138
|
+
timeout * 1_000,
|
|
139
139
|
);
|
|
140
140
|
|
|
141
141
|
// Parse stderr for progress
|
package/src/errors.ts
CHANGED
|
@@ -5,7 +5,7 @@ export class TikTokLiveError extends Error {
|
|
|
5
5
|
export class UserOfflineError extends TikTokLiveError {
|
|
6
6
|
override name = "UserOfflineError";
|
|
7
7
|
constructor(username: string) {
|
|
8
|
-
super(`User "${username}" is not currently live`);
|
|
8
|
+
super(`User "${username}" is not currently live (or does not exist)`);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ export class RoomResolveError extends TikTokLiveError {
|
|
|
20
20
|
override name = "RoomResolveError";
|
|
21
21
|
constructor(username: string, cause?: unknown) {
|
|
22
22
|
super(
|
|
23
|
-
`Failed to resolve room ID for "${username}"${cause ? `: ${cause}` : ""}`,
|
|
23
|
+
`Failed to resolve room ID for "${username}" — may not exist or be offline${cause ? `: ${cause}` : ""}`,
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -85,7 +85,7 @@ export interface TikTokLiveDownloaderOptions {
|
|
|
85
85
|
maxDuration?: number;
|
|
86
86
|
/** Split recording into segments of this many seconds (default: Infinity = single file). */
|
|
87
87
|
maxSegmentDuration?: number;
|
|
88
|
-
/** Polling interval in
|
|
88
|
+
/** Polling interval in seconds when waiting for live (default: 180). */
|
|
89
89
|
checkInterval?: number;
|
|
90
90
|
/** HTTP proxy URL (supports http, https, socks4, socks5). */
|
|
91
91
|
proxyUrl?: string;
|
|
@@ -93,7 +93,7 @@ export interface TikTokLiveDownloaderOptions {
|
|
|
93
93
|
cookieJar?: CookieJarLike;
|
|
94
94
|
/** Browser to emulate (default: "chrome"). */
|
|
95
95
|
browser?: Browser;
|
|
96
|
-
/** Request timeout in
|
|
96
|
+
/** Request timeout in seconds (default: 30). */
|
|
97
97
|
timeout?: number;
|
|
98
98
|
/** Extra headers to send with every request. */
|
|
99
99
|
headers?: Record<string, string>;
|
|
@@ -123,9 +123,22 @@ export interface WaitingInfo {
|
|
|
123
123
|
elapsed: number;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export interface RemuxInfo {
|
|
127
|
+
/** Input .ts file path. */
|
|
128
|
+
filePath: string;
|
|
129
|
+
/** Input file size in MB. */
|
|
130
|
+
inputSizeMB: number;
|
|
131
|
+
/** Remux status: started → completed (with outputPath) or failed (keeping .ts). */
|
|
132
|
+
status: "started" | "completed" | "failed";
|
|
133
|
+
/** Output file path. Only set when status is "completed". */
|
|
134
|
+
outputPath?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
export interface TikTokLiveDownloaderEvents {
|
|
127
138
|
/** Emitted periodically while waiting for a room or stream to become active. */
|
|
128
139
|
waiting: [info: WaitingInfo];
|
|
140
|
+
/** Emitted when a remux operation starts, completes, or fails. */
|
|
141
|
+
remux: [info: RemuxInfo];
|
|
129
142
|
/** Emitted when a live stream is detected and we're about to start recording. */
|
|
130
143
|
start: [info: StreamInfo];
|
|
131
144
|
/** Emitted periodically (every ~1s) during recording with current stats. */
|