tokwatchr 0.5.0 → 0.6.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/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 ms when waiting for live (default: 30_000). */
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 ms (default: 30_000). */
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
@@ -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 = 3e4 } = options;
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}ms — check the stream URL`));
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: 18e4,
623
+ checkInterval: 180,
624
624
  proxyUrl: null,
625
625
  cookieJar: null,
626
626
  browser: "chrome",
627
- timeout: 3e4,
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, 18e4);
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, 18e4);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokwatchr",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Download TikTok livestreams. Given a username, download the livestream.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -36,11 +36,11 @@ const DEFAULT_OPTIONS: ResolvedOptions = {
36
36
  bitrate: null,
37
37
  maxDuration: Infinity,
38
38
  maxSegmentDuration: Infinity,
39
- checkInterval: 180_000,
39
+ checkInterval: 180,
40
40
  proxyUrl: null,
41
41
  cookieJar: null,
42
42
  browser: "chrome",
43
- timeout: 30_000,
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, 180_000);
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, 180_000);
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
  }
@@ -13,10 +13,10 @@ export interface FfmpegDownloadOptions {
13
13
  onProgress?: (stats: DownloadStats) => void;
14
14
  maxDuration?: number;
15
15
  /**
16
- * Startup timeout in ms. If ffmpeg produces no stderr output within
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 30_000
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 = 30_000,
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` ms, it likely stalled on a bad URL. Kill it so the
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}ms — check the stream URL`,
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/index.ts CHANGED
@@ -28,6 +28,7 @@ export type {
28
28
  DownloadStats,
29
29
  OutputFormat,
30
30
  QualityOption,
31
+ RemuxInfo,
31
32
  StreamInfo,
32
33
  StreamQualityKey,
33
34
  TikTokLiveDownloaderEvents,
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 ms when waiting for live (default: 30_000). */
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 ms (default: 30_000). */
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. */