@zfadhli/tokrec 0.13.0 → 0.15.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.
Files changed (126) hide show
  1. package/dist/app/abort.mjs +14 -0
  2. package/dist/app/abort.mjs.map +1 -0
  3. package/dist/{lib → app}/config.d.mts +73 -71
  4. package/dist/app/config.d.mts.map +1 -0
  5. package/dist/app/config.mjs +26 -0
  6. package/dist/app/config.mjs.map +1 -0
  7. package/dist/app/events.mjs +36 -0
  8. package/dist/app/events.mjs.map +1 -0
  9. package/dist/app/job.mjs +190 -0
  10. package/dist/app/job.mjs.map +1 -0
  11. package/dist/{lib → app}/logger.mjs +1 -1
  12. package/dist/app/logger.mjs.map +1 -0
  13. package/dist/{core/monitor.mjs → app/loop.mjs} +19 -9
  14. package/dist/app/loop.mjs.map +1 -0
  15. package/dist/app/recorder.d.mts +7 -0
  16. package/dist/app/recorder.d.mts.map +1 -0
  17. package/dist/app/recorder.mjs +233 -0
  18. package/dist/app/recorder.mjs.map +1 -0
  19. package/dist/core/errors.d.mts +16 -0
  20. package/dist/core/errors.d.mts.map +1 -0
  21. package/dist/core/errors.mjs +15 -0
  22. package/dist/core/errors.mjs.map +1 -0
  23. package/dist/core/format.d.mts +12 -0
  24. package/dist/core/format.d.mts.map +1 -0
  25. package/dist/{lib/utils.mjs → core/format.mjs} +6 -41
  26. package/dist/core/format.mjs.map +1 -0
  27. package/dist/core/plan.mjs +52 -0
  28. package/dist/core/plan.mjs.map +1 -0
  29. package/dist/core/verdict.d.mts +44 -0
  30. package/dist/core/verdict.d.mts.map +1 -0
  31. package/dist/core/verdict.mjs +47 -0
  32. package/dist/core/verdict.mjs.map +1 -0
  33. package/dist/index.d.mts +6 -3
  34. package/dist/index.mjs +5 -2
  35. package/dist/{core/api/client.mjs → io/http.mjs} +38 -10
  36. package/dist/io/http.mjs.map +1 -0
  37. package/dist/io/media/convert.mjs +60 -0
  38. package/dist/io/media/convert.mjs.map +1 -0
  39. package/dist/{core/recorder → io/media}/download.mjs +8 -7
  40. package/dist/io/media/download.mjs.map +1 -0
  41. package/dist/{core/recorder/ffmpeg-utils.mjs → io/media/ffmpeg.mjs} +45 -5
  42. package/dist/io/media/ffmpeg.mjs.map +1 -0
  43. package/dist/{core/recorder → io/media}/normalize.mjs +1 -1
  44. package/dist/io/media/normalize.mjs.map +1 -0
  45. package/dist/io/tiktok/client.mjs +77 -0
  46. package/dist/io/tiktok/client.mjs.map +1 -0
  47. package/dist/io/tiktok/discovery/chain.mjs +117 -0
  48. package/dist/io/tiktok/discovery/chain.mjs.map +1 -0
  49. package/dist/io/tiktok/discovery/fallback.mjs +64 -0
  50. package/dist/io/tiktok/discovery/fallback.mjs.map +1 -0
  51. package/dist/io/tiktok/discovery/page.mjs +102 -0
  52. package/dist/io/tiktok/discovery/page.mjs.map +1 -0
  53. package/dist/io/tiktok/page.mjs +34 -0
  54. package/dist/io/tiktok/page.mjs.map +1 -0
  55. package/dist/io/tiktok/parse.mjs +110 -0
  56. package/dist/io/tiktok/parse.mjs.map +1 -0
  57. package/dist/io/tiktok/webcast.mjs +132 -0
  58. package/dist/io/tiktok/webcast.mjs.map +1 -0
  59. package/package.json +5 -26
  60. package/src/app/abort.ts +18 -0
  61. package/src/app/config.ts +178 -0
  62. package/src/app/events.ts +46 -0
  63. package/src/app/job.ts +228 -0
  64. package/src/app/logger.ts +133 -0
  65. package/src/app/loop.ts +84 -0
  66. package/src/app/recorder.ts +288 -0
  67. package/src/core/errors.ts +24 -0
  68. package/src/core/format.ts +50 -0
  69. package/src/core/log.ts +13 -0
  70. package/src/core/plan.ts +49 -0
  71. package/src/core/verdict.ts +79 -0
  72. package/src/index.ts +38 -0
  73. package/src/io/http.ts +167 -0
  74. package/src/io/media/convert.ts +69 -0
  75. package/src/io/media/download.ts +120 -0
  76. package/src/io/media/ffmpeg.ts +201 -0
  77. package/src/io/media/normalize.ts +64 -0
  78. package/src/io/tiktok/client.ts +114 -0
  79. package/src/io/tiktok/discovery/chain.ts +146 -0
  80. package/src/io/tiktok/discovery/fallback.ts +88 -0
  81. package/src/io/tiktok/discovery/page.ts +142 -0
  82. package/src/io/tiktok/page.ts +42 -0
  83. package/src/io/tiktok/parse.ts +171 -0
  84. package/src/io/tiktok/webcast.ts +162 -0
  85. package/README.md +0 -430
  86. package/dist/bin.d.mts +0 -1
  87. package/dist/bin.mjs +0 -215
  88. package/dist/bin.mjs.map +0 -1
  89. package/dist/core/api/client.mjs.map +0 -1
  90. package/dist/core/api/rate-limiter.mjs +0 -29
  91. package/dist/core/api/rate-limiter.mjs.map +0 -1
  92. package/dist/core/api/tiktok.mjs +0 -394
  93. package/dist/core/api/tiktok.mjs.map +0 -1
  94. package/dist/core/monitor.mjs.map +0 -1
  95. package/dist/core/recorder/convert.mjs +0 -55
  96. package/dist/core/recorder/convert.mjs.map +0 -1
  97. package/dist/core/recorder/download.mjs.map +0 -1
  98. package/dist/core/recorder/ffmpeg-utils.mjs.map +0 -1
  99. package/dist/core/recorder/index.d.mts +0 -7
  100. package/dist/core/recorder/index.d.mts.map +0 -1
  101. package/dist/core/recorder/index.mjs +0 -205
  102. package/dist/core/recorder/index.mjs.map +0 -1
  103. package/dist/core/recorder/normalize.mjs.map +0 -1
  104. package/dist/core/recorder/post-processing.mjs +0 -154
  105. package/dist/core/recorder/post-processing.mjs.map +0 -1
  106. package/dist/core/recorder/recorder-events.mjs +0 -34
  107. package/dist/core/recorder/recorder-events.mjs.map +0 -1
  108. package/dist/core/recorder/recorder-state.mjs +0 -38
  109. package/dist/core/recorder/recorder-state.mjs.map +0 -1
  110. package/dist/core/recorder/stream.mjs +0 -25
  111. package/dist/core/recorder/stream.mjs.map +0 -1
  112. package/dist/lib/browser-cookies.mjs +0 -113
  113. package/dist/lib/browser-cookies.mjs.map +0 -1
  114. package/dist/lib/cli.mjs +0 -137
  115. package/dist/lib/cli.mjs.map +0 -1
  116. package/dist/lib/config.d.mts.map +0 -1
  117. package/dist/lib/config.mjs +0 -47
  118. package/dist/lib/config.mjs.map +0 -1
  119. package/dist/lib/logger.mjs.map +0 -1
  120. package/dist/lib/tmux.mjs +0 -84
  121. package/dist/lib/tmux.mjs.map +0 -1
  122. package/dist/lib/ui.mjs +0 -136
  123. package/dist/lib/ui.mjs.map +0 -1
  124. package/dist/lib/utils.mjs.map +0 -1
  125. package/dist/package.mjs +0 -6
  126. package/dist/package.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../../src/core/api/client.ts"],"sourcesContent":["/**\n * HTTP client — thin wrapper around wreq-js createSession.\n * Provides TLS fingerprint impersonation, cookie jar, proxy support,\n * and configurable rate limiting to prevent WAF triggering.\n */\n\nimport { createSession, type Session } from \"wreq-js\"\nimport type { RecorderConfig } from \"../../lib/config\"\nimport { createRateLimiter } from \"./rate-limiter\"\n\n// BodyInit is only available inside declare module \"bun\" in bun-types, not as a global\n// under the ES2022 lib. We explicitly import it to keep lib focused on ES2022.\ntype _BodyInit = import(\"bun\").BodyInit\n\nexport interface HttpClient {\n get: (url: string) => Promise<Response>\n post: (url: string, body?: _BodyInit, headers?: Record<string, string>) => Promise<Response>\n close: () => Promise<void>\n}\n\nconst REQUEST_TIMEOUT_MS = 15_000\n\n/** Standard browser headers to avoid TikTok WAF detection. */\nconst BROWSER_HEADERS: Record<string, string> = {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0\",\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\",\n \"Accept-Language\": \"en-US,en;q=0.5\",\n \"Accept-Encoding\": \"gzip, deflate, br\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\",\n}\n\nexport async function createHttpClient(config: RecorderConfig): Promise<HttpClient> {\n const session: Session = await createSession({\n browser: \"firefox_149\",\n os: \"windows\",\n proxy: config.proxy,\n })\n\n // Seed cookies into the session jar so all subsequent requests are authenticated.\n // NOTE: Using a Cookie header does NOT populate wreq-js's cookie jar —\n // we must use session.setCookie() instead.\n // Seed for all TikTok domains since cookies may not propagate across\n // subdomains automatically in wreq-js's cookie jar.\n if (config.cookies && \"sessionid_ss\" in config.cookies) {\n const domains = [\"https://www.tiktok.com\", \"https://webcast.tiktok.com\", \"https://m.tiktok.com\"]\n for (const domain of domains) {\n for (const [name, value] of Object.entries(config.cookies)) {\n session.setCookie(name, value, domain)\n }\n }\n }\n\n const rateLimiter = createRateLimiter(config.ratePerSecond ?? 5)\n\n async function rateLimitedFetch(\n url: string,\n options?: { method?: string; body?: unknown; headers?: Record<string, string> },\n ): Promise<Response> {\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)\n try {\n await rateLimiter.acquire(controller.signal)\n const res = await session.fetch(url, {\n method: options?.method,\n body: options?.body as never,\n headers: {\n ...BROWSER_HEADERS,\n ...options?.headers,\n },\n signal: controller.signal,\n })\n return res as unknown as Response\n } finally {\n clearTimeout(timeout)\n }\n }\n\n return {\n get: async (url: string) => rateLimitedFetch(url),\n post: async (url: string, body?: _BodyInit, headers?: Record<string, string>) =>\n rateLimitedFetch(url, {\n method: \"POST\",\n body: body as never,\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded; charset=UTF-8\",\n ...headers,\n },\n }),\n close: async () => {\n await session.close()\n },\n }\n}\n"],"mappings":";;;;;;;;AAoBA,MAAM,qBAAqB;;AAG3B,MAAM,kBAA0C;CAC9C,cAAc;CACd,QAAQ;CACR,mBAAmB;CACnB,mBAAmB;CACnB,6BAA6B;CAC7B,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;AACpB;AAEA,eAAsB,iBAAiB,QAA6C;CAClF,MAAM,UAAmB,MAAM,cAAc;EAC3C,SAAS;EACT,IAAI;EACJ,OAAO,OAAO;CAChB,CAAC;CAOD,IAAI,OAAO,WAAW,kBAAkB,OAAO,SAE7C,KAAK,MAAM,UAAU;EADJ;EAA0B;EAA8B;CAC9C,GACzB,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,OAAO,GACvD,QAAQ,UAAU,MAAM,OAAO,MAAM;CAK3C,MAAM,cAAc,kBAAkB,OAAO,iBAAiB,CAAC;CAE/D,eAAe,iBACb,KACA,SACmB;EACnB,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,UAAU,iBAAiB,WAAW,MAAM,GAAG,kBAAkB;EACvE,IAAI;GACF,MAAM,YAAY,QAAQ,WAAW,MAAM;GAU3C,OAAO,MATW,QAAQ,MAAM,KAAK;IACnC,QAAQ,SAAS;IACjB,MAAM,SAAS;IACf,SAAS;KACP,GAAG;KACH,GAAG,SAAS;IACd;IACA,QAAQ,WAAW;GACrB,CAAC;EAEH,UAAU;GACR,aAAa,OAAO;EACtB;CACF;CAEA,OAAO;EACL,KAAK,OAAO,QAAgB,iBAAiB,GAAG;EAChD,MAAM,OAAO,KAAa,MAAkB,YAC1C,iBAAiB,KAAK;GACpB,QAAQ;GACF;GACN,SAAS;IACP,gBAAgB;IAChB,GAAG;GACL;EACF,CAAC;EACH,OAAO,YAAY;GACjB,MAAM,QAAQ,MAAM;EACtB;CACF;AACF"}
@@ -1,29 +0,0 @@
1
- //#region src/core/api/rate-limiter.ts
2
- /**
3
- * Create a token-bucket rate limiter.
4
- *
5
- * @param tokensPerSecond Max sustained requests per second.
6
- * 0 or negative = unlimited (no waiting).
7
- */
8
- function createRateLimiter(tokensPerSecond) {
9
- if (tokensPerSecond <= 0) return { acquire: async () => {} };
10
- const MIN_INTERVAL_MS = 1e3 / tokensPerSecond;
11
- let lastTokenTime = 0;
12
- return { async acquire(signal) {
13
- if (signal?.aborted) return;
14
- const now = Date.now();
15
- const waitTime = Math.max(0, lastTokenTime + MIN_INTERVAL_MS - now);
16
- if (waitTime > 0) await new Promise((resolve) => {
17
- const timer = setTimeout(resolve, waitTime);
18
- if (signal) signal.addEventListener("abort", () => {
19
- clearTimeout(timer);
20
- resolve();
21
- }, { once: true });
22
- });
23
- lastTokenTime = Date.now();
24
- } };
25
- }
26
- //#endregion
27
- export { createRateLimiter };
28
-
29
- //# sourceMappingURL=rate-limiter.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rate-limiter.mjs","names":[],"sources":["../../../src/core/api/rate-limiter.ts"],"sourcesContent":["/**\n * Token-bucket rate limiter — limits API request frequency to prevent\n * TikTok WAF triggering on rapid polling or URL refresh loops.\n *\n * Design:\n * - Token bucket with configurable refill rate (tokens/sec)\n * - AbortSignal-aware wait so cancellation works through the queue\n * - Thread-safe for cooperative JS concurrency (all awaiters share\n * the same monotonically-increasing last-token timestamp)\n */\n\nexport interface RateLimiter {\n /** Wait until a rate-limit token is available, then consume it. */\n acquire(signal?: AbortSignal): Promise<void>\n}\n\n/**\n * Create a token-bucket rate limiter.\n *\n * @param tokensPerSecond Max sustained requests per second.\n * 0 or negative = unlimited (no waiting).\n */\nexport function createRateLimiter(tokensPerSecond: number): RateLimiter {\n // Unlimited mode — no-op acquire\n if (tokensPerSecond <= 0) {\n return { acquire: async () => {} }\n }\n\n const MIN_INTERVAL_MS = 1000 / tokensPerSecond\n let lastTokenTime = 0\n\n return {\n async acquire(signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) return\n\n const now = Date.now()\n const waitTime = Math.max(0, lastTokenTime + MIN_INTERVAL_MS - now)\n\n if (waitTime > 0) {\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, waitTime)\n if (signal) {\n signal.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer)\n resolve()\n },\n { once: true },\n )\n }\n })\n }\n\n lastTokenTime = Date.now()\n },\n }\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,kBAAkB,iBAAsC;CAEtE,IAAI,mBAAmB,GACrB,OAAO,EAAE,SAAS,YAAY,CAAC,EAAE;CAGnC,MAAM,kBAAkB,MAAO;CAC/B,IAAI,gBAAgB;CAEpB,OAAO,EACL,MAAM,QAAQ,QAAqC;EACjD,IAAI,QAAQ,SAAS;EAErB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,KAAK,IAAI,GAAG,gBAAgB,kBAAkB,GAAG;EAElE,IAAI,WAAW,GACb,MAAM,IAAI,SAAe,YAAY;GACnC,MAAM,QAAQ,WAAW,SAAS,QAAQ;GAC1C,IAAI,QACF,OAAO,iBACL,eACM;IACJ,aAAa,KAAK;IAClB,QAAQ;GACV,GACA,EAAE,MAAM,KAAK,CACf;EAEJ,CAAC;EAGH,gBAAgB,KAAK,IAAI;CAC3B,EACF;AACF"}
@@ -1,394 +0,0 @@
1
- //#region src/core/api/tiktok.ts
2
- const TIKTOK_BASES = ["https://www.tiktok.com", "https://m.tiktok.com"];
3
- const WEBCAST_BASE = "https://webcast.tiktok.com";
4
- function createTikTokApi(http, logger, showDebug) {
5
- const debug = (msg) => {
6
- logger?.debug(msg);
7
- if (showDebug) process.stderr.write(`[API_DEBUG] ${msg}\n`);
8
- };
9
- let cached = null;
10
- let cachedUser = "";
11
- async function ensureCache(user) {
12
- if (cached && cachedUser === user) return cached;
13
- cached = await fetchLiveInfo(user);
14
- cachedUser = user;
15
- return cached;
16
- }
17
- function invalidateCache() {
18
- cached = null;
19
- cachedUser = "";
20
- }
21
- return {
22
- invalidateCache,
23
- async getRoomId(user) {
24
- return (await ensureCache(user))?.roomId ?? null;
25
- },
26
- async isRoomAlive(roomId) {
27
- if (cached && String(cached.roomId) === String(roomId)) return cached.isLive;
28
- return fetchCheckAlive(roomId);
29
- },
30
- async getLiveUrl(roomId) {
31
- return (await ensureCache(cachedUser || roomId))?.streamUrl ?? null;
32
- }
33
- };
34
- /**
35
- * Try to fetch a TikTok user page from multiple domains.
36
- * www.tiktok.com is increasingly blocked by Slardar WAF; m.tiktok.com
37
- * often has different bot protection and may succeed.
38
- * Returns the HTML and the base URL that worked, or null if all fail.
39
- */
40
- async function tryFetchPage(user, path) {
41
- for (const base of TIKTOK_BASES) {
42
- const url = `${base}/@${user}${path}`;
43
- const res = await http.get(url);
44
- debug(`tryFetchPage: ${url} status=${res.status}, ok=${res.ok}`);
45
- if (!res.ok) continue;
46
- const html = await res.text();
47
- if (html.length < 5e3 || html.includes("SlardarWAF")) {
48
- debug(`tryFetchPage: ${url} WAF blocked (${html.length} bytes, slardar=${html.includes("SlardarWAF")})`);
49
- continue;
50
- }
51
- debug(`tryFetchPage: ${url} succeeded (${html.length} bytes)`);
52
- return {
53
- html,
54
- baseUrl: base
55
- };
56
- }
57
- return null;
58
- }
59
- async function fetchLiveInfo(user) {
60
- let sigi = null;
61
- let html = "";
62
- try {
63
- const page = await tryFetchPage(user, "/live");
64
- if (!page) {
65
- debug("fetchLiveInfo: all domains blocked for /live page");
66
- html = "";
67
- } else {
68
- html = page.html;
69
- debug(`fetchLiveInfo: /live returned ${html.length} bytes (${page.baseUrl}), first 300: ${html.slice(0, 300).replace(/\n/g, "\\n")}`);
70
- sigi = extractSigiState(html);
71
- debug(`fetchLiveInfo: SIGI_STATE ${sigi ? "found" : "not found"}`);
72
- if (sigi) {
73
- const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom;
74
- const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user;
75
- debug(`fetchLiveInfo: liveRoom=${!!liveRoom}, userInfo=${!!userInfo}, userInfo.roomId=${userInfo?.roomId ?? "null"}`);
76
- if (liveRoom && userInfo?.roomId) {
77
- const roomId = String(userInfo.roomId);
78
- const isLive = liveRoom.status === 2;
79
- debug(`fetchLiveInfo: primary path — roomId=${roomId}, status=${liveRoom.status}, isLive=${isLive}`);
80
- let streamUrl = null;
81
- if (isLive) {
82
- streamUrl = extractStreamUrlFast(liveRoom);
83
- if (!streamUrl) streamUrl = findStreamUrlRecursively(sigi);
84
- if (!streamUrl) streamUrl = await fetchStreamUrlFromApi(roomId);
85
- }
86
- return {
87
- roomId,
88
- isLive,
89
- streamUrl,
90
- title: liveRoom.title ?? null
91
- };
92
- }
93
- if (userInfo?.roomId) {
94
- const roomId = String(userInfo.roomId);
95
- debug(`fetchLiveInfo: userInfo fallback — roomId=${roomId}, querying Webcast API`);
96
- const roomInfo = await fetchRoomInfoFromRoomApi(roomId);
97
- debug(`fetchLiveInfo: Webcast API ${roomInfo ? "returned room info" : "returned null"}`);
98
- if (roomInfo) return roomInfo;
99
- }
100
- debug("fetchLiveInfo: trying legacy fallback (UserModule)");
101
- const legacy = await fetchLiveInfoFallback(sigi, user);
102
- debug(`fetchLiveInfo: legacy fallback ${legacy ? "succeeded" : "returned null"}`);
103
- if (legacy) return legacy;
104
- }
105
- }
106
- } catch (_err) {
107
- debug(`fetchLiveInfo: caught error — ${_err instanceof Error ? _err.message : String(_err)}`);
108
- return null;
109
- }
110
- if (!html || html.length < 5e3) {
111
- debug("fetchLiveInfo: trying API fallback (api-live/user/room)");
112
- const apiResult = await fetchLiveInfoFromApi(user);
113
- if (apiResult) return apiResult;
114
- }
115
- debug(`fetchLiveInfo: trying universal data fallback (html length ${html.length})`);
116
- const { roomId } = extractUserFromUniversalData(html);
117
- debug(`fetchLiveInfo: universal data roomId=${roomId ?? "null"}`);
118
- if (roomId) {
119
- const result = await fetchRoomInfoFromRoomApi(roomId);
120
- debug(`fetchLiveInfo: universal data Webcast API ${result ? "succeeded" : "returned null"}`);
121
- if (result) return result;
122
- }
123
- debug("fetchLiveInfo: trying profile page fallback");
124
- const profileResult = await fetchLiveInfoFromProfile(user);
125
- debug(`fetchLiveInfo: profile page ${profileResult ? "succeeded" : "returned null"}`);
126
- return profileResult;
127
- }
128
- /**
129
- * Fetch the main profile page to extract user info from
130
- * __UNIVERSAL_DATA_FOR_REHYDRATION__. When the user is live, their
131
- * roomId is embedded in the data, allowing us to fetch room info and
132
- * stream URL via the /webcast/room/info/ endpoint (which works
133
- * reliably). If no roomId is present the user is offline.
134
- */
135
- async function fetchLiveInfoFromProfile(user) {
136
- try {
137
- const page = await tryFetchPage(user, "");
138
- if (!page) {
139
- debug("fetchLiveInfoFromProfile: all domains blocked for profile page");
140
- return null;
141
- }
142
- debug(`fetchLiveInfoFromProfile: returned ${page.html.length} bytes (${page.baseUrl}), first 300: ${page.html.slice(0, 300).replace(/\n/g, "\\n")}`);
143
- const { userId, roomId } = extractUserFromUniversalData(page.html);
144
- if (!userId) return null;
145
- if (roomId) return fetchRoomInfoFromRoomApi(roomId);
146
- return null;
147
- } catch {
148
- return null;
149
- }
150
- }
151
- /**
152
- * Fetch live info from TikTok's internal API endpoint.
153
- * This bypasses the Slardar WAF that blocks www.tiktok.com HTML pages.
154
- *
155
- * Endpoint: /api-live/user/room/?aid=1988&uniqueId=${user}&sourceType=54
156
- * Returns the roomId, user status (2 = live), and other user metadata.
157
- * When the user is live, also queries the Webcast API for a stream URL.
158
- */
159
- async function fetchLiveInfoFromApi(user) {
160
- try {
161
- const url = `https://www.tiktok.com/api-live/user/room/?aid=1988&uniqueId=${encodeURIComponent(user)}&sourceType=54`;
162
- const res = await http.get(url);
163
- debug(`fetchLiveInfoFromApi: ${url} status=${res.status}`);
164
- if (!res.ok) return null;
165
- const text = await res.text();
166
- debug(`fetchLiveInfoFromApi: returned ${text.length} bytes`);
167
- if (text.length === 0) return null;
168
- const userData = (JSON.parse(text)?.data)?.user;
169
- if (!userData?.roomId) {
170
- debug("fetchLiveInfoFromApi: no roomId in API response");
171
- return null;
172
- }
173
- const roomId = String(userData.roomId);
174
- const isLive = userData.status === 2;
175
- debug(`fetchLiveInfoFromApi: roomId=${roomId}, status=${userData.status}, isLive=${isLive}`);
176
- if (!isLive) return {
177
- roomId,
178
- isLive: false,
179
- streamUrl: null,
180
- title: null
181
- };
182
- return {
183
- roomId,
184
- isLive: true,
185
- streamUrl: await fetchStreamUrlFromApi(roomId),
186
- title: null
187
- };
188
- } catch {
189
- return null;
190
- }
191
- }
192
- /**
193
- * Shared boilerplate for Webcast room/info API calls.
194
- * Handles URL construction, HTTP GET, JSON parsing, status code checks,
195
- * and error handling. The caller provides an extractor for the response data.
196
- */
197
- async function fetchFromRoomApi(roomId, extract) {
198
- try {
199
- const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`;
200
- const res = await http.get(url);
201
- debug(`fetchFromRoomApi: roomId=${roomId} status=${res.status}, ok=${res.ok}`);
202
- if (!res.ok) return null;
203
- const text = await res.text();
204
- debug(`fetchFromRoomApi: roomId=${roomId} returned ${text.length} bytes`);
205
- if (text.length === 0) return null;
206
- const data = JSON.parse(text);
207
- if (data.status_code === 4003110) return null;
208
- return extract(data);
209
- } catch {
210
- return null;
211
- }
212
- }
213
- /**
214
- * Check if a room is currently alive using the lightweight
215
- * /webcast/room/check_alive/ endpoint.
216
- *
217
- * This is faster and cheaper than fetching the full room/info response.
218
- * Returns `true` if the room is live, `false` otherwise (including errors).
219
- *
220
- * Endpoint: /webcast/room/check_alive/?aid=1988&region=CH&room_ids={id}&user_is_login=true
221
- * Response: { "data": [{ "alive": true/false, "room_id": "..." }] }
222
- */
223
- async function fetchCheckAlive(roomId) {
224
- try {
225
- const url = `${WEBCAST_BASE}/webcast/room/check_alive/?aid=1988&region=CH&room_ids=${roomId}&user_is_login=true`;
226
- const res = await http.get(url);
227
- debug(`fetchCheckAlive: roomId=${roomId} status=${res.status}, ok=${res.ok}`);
228
- if (!res.ok) return false;
229
- const text = await res.text();
230
- debug(`fetchCheckAlive: roomId=${roomId} returned ${text.length} bytes`);
231
- if (text.length === 0) return false;
232
- const alive = (JSON.parse(text)?.data)?.[0]?.alive === true;
233
- debug(`fetchCheckAlive: roomId=${roomId} alive=${alive}`);
234
- return alive;
235
- } catch (err) {
236
- debug(`fetchCheckAlive: roomId=${roomId} error — ${err instanceof Error ? err.message : String(err)}`);
237
- return false;
238
- }
239
- }
240
- /**
241
- * Fetch room info from the Webcast room/info API. Requires a known roomId.
242
- */
243
- async function fetchRoomInfoFromRoomApi(roomId) {
244
- return fetchFromRoomApi(roomId, (data) => {
245
- const roomData = data.data;
246
- if (!roomData) return null;
247
- const isLive = (roomData.status ?? 4) === 2;
248
- return {
249
- roomId,
250
- isLive,
251
- streamUrl: isLive ? findStreamUrlRecursively(roomData) : null,
252
- title: roomData.title ?? null
253
- };
254
- });
255
- }
256
- /**
257
- * Secondary live-info extraction when SIGI_STATE's LiveRoom data is
258
- * async-loaded. Uses LiveRoom.liveRoomStatus for the live flag and
259
- * UserModule.users[user].roomId for the room ID. Stream URL is obtained
260
- * from the Webcast API (room/info/) as a final fallback.
261
- */
262
- async function fetchLiveInfoFallback(sigi, user) {
263
- const isLive = sigi.LiveRoom?.liveRoomStatus === 2;
264
- const userModule = sigi.UserModule?.users?.[user];
265
- let roomId = null;
266
- if (userModule?.roomId) roomId = typeof userModule.roomId === "object" ? String(userModule.roomId.id ?? "") : String(userModule.roomId);
267
- if (roomId) {
268
- const streamUrl = isLive ? await fetchStreamUrlFromApi(roomId) : null;
269
- return {
270
- roomId,
271
- isLive,
272
- streamUrl,
273
- title: null
274
- };
275
- }
276
- return null;
277
- }
278
- /**
279
- * Fetch stream URL from the Webcast API.
280
- * Covers cases where SIGI_STATE doesn't include stream data.
281
- */
282
- async function fetchStreamUrlFromApi(roomId) {
283
- return fetchFromRoomApi(roomId, (data) => findStreamUrlRecursively(data));
284
- }
285
- }
286
- /**
287
- * @public Exported for testing.
288
- */
289
- function extractSigiState(html) {
290
- const match = html.match(/<script\s+id="SIGI_STATE"\s+type="application\/json">(.*?)<\/script>/);
291
- if (!match?.[1]) return null;
292
- try {
293
- return JSON.parse(match[1]);
294
- } catch {
295
- return null;
296
- }
297
- }
298
- /**
299
- * Extract the numeric user ID and optional room ID from TikTok's new
300
- * page structure (__UNIVERSAL_DATA_FOR_REHYDRATION__).
301
- *
302
- * The profile page (/@user) includes "webapp.user-detail" which
303
- * contains the user's numeric ID. When the user is live, the roomId
304
- * is also embedded here.
305
- *
306
- * Returns { userId, roomId } where userId is always set when the data
307
- * is found, and roomId is set only when the user is currently live.
308
- *
309
- * @public Exported for testing and fixture capture.
310
- */
311
- function extractUserFromUniversalData(html) {
312
- const match = html.match(/<script\s+id="__UNIVERSAL_DATA_FOR_REHYDRATION__"\s+type="application\/json">(.*?)<\/script>/);
313
- if (!match?.[1]) return {
314
- userId: null,
315
- roomId: null
316
- };
317
- try {
318
- const data = JSON.parse(match[1]);
319
- const user = ((data.__DEFAULT_SCOPE__ ?? data)?.["webapp.user-detail"])?.userInfo?.user;
320
- return {
321
- userId: user?.id ? String(user.id) : null,
322
- roomId: user?.roomId ? String(user.roomId) : null
323
- };
324
- } catch {
325
- return {
326
- userId: null,
327
- roomId: null
328
- };
329
- }
330
- }
331
- /**
332
- * Fast-path: extract stream URL from the known SIGI_STATE location.
333
- * This covers ~95% of cases without needing a full recursive search.
334
- *
335
- * Prefers FLV (lower latency) over HLS/other protocols.
336
- */
337
- function extractStreamUrlFast(liveRoom) {
338
- try {
339
- const raw = liveRoom?.streamData?.pull_data?.stream_data;
340
- if (!raw) return null;
341
- const data = JSON.parse(raw)?.data;
342
- if (!data) return null;
343
- for (const q of [
344
- "origin",
345
- "fhd",
346
- "uhd",
347
- "hd",
348
- "sd",
349
- "ld"
350
- ]) {
351
- const entry = data[q];
352
- const url = entry?.main?.flv ?? entry?.main?.hls;
353
- if (url) return url;
354
- }
355
- return null;
356
- } catch {
357
- return null;
358
- }
359
- }
360
- /**
361
- * Brute-force recursive search for a stream URL anywhere in a JSON value.
362
- * Survives TikTok changing key names or nesting structure.
363
- *
364
- * Prefers FLV over HLS when both are present in the response.
365
- *
366
- * Inspired by PR #430 (Michele0303/tiktok-live-recorder).
367
- */
368
- function findStreamUrlRecursively(obj) {
369
- const flvUrls = [];
370
- const m3u8Urls = [];
371
- function walk(value) {
372
- if (typeof value === "string") {
373
- if ((value.startsWith("http://") || value.startsWith("https://")) && (value.includes(".flv") || value.includes(".m3u8"))) {
374
- if (value.includes(".flv")) flvUrls.push(value);
375
- else m3u8Urls.push(value);
376
- return;
377
- }
378
- if (value.startsWith("{") || value.startsWith("[")) try {
379
- walk(JSON.parse(value));
380
- } catch {}
381
- return;
382
- }
383
- if (value && typeof value === "object") if (Array.isArray(value)) for (const item of value) walk(item);
384
- else for (const val of Object.values(value)) walk(val);
385
- }
386
- walk(obj);
387
- if (flvUrls.length > 0) return flvUrls[0];
388
- if (m3u8Urls.length > 0) return m3u8Urls[0];
389
- return null;
390
- }
391
- //#endregion
392
- export { createTikTokApi };
393
-
394
- //# sourceMappingURL=tiktok.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tiktok.mjs","names":[],"sources":["../../../src/core/api/tiktok.ts"],"sourcesContent":["/**\n * TikTok API — resolves room IDs, live status, and stream URLs.\n *\n * Primary path: parse SIGI_STATE from the user's /live page (fast path for\n * older TikTok pages). When SIGI_STATE is absent (TikTok's new unified page\n * structure), falls back to __UNIVERSAL_DATA_FOR_REHYDRATION__ on the profile\n * page to extract the room ID, then calls the Webcast room/info API for live\n * status and stream URL.\n *\n * Architecture:\n * createTikTokApi(httpClient) → { getRoomId, isRoomAlive, getLiveUrl }\n *\n * All public methods share a per-tick cache so the page is fetched at most once\n * per poll cycle.\n *\n * Note: The room/enter Webcast endpoint has been deprecated due to frequent\n * 403 errors. All lookups go through room/info (reliable for live + offline).\n */\n\nimport type { Logger } from \"../../lib/logger\"\nimport type { HttpClient } from \"./client\"\n\nexport interface TikTokApi {\n /** Resolve a username to a room ID. Returns null if not found. */\n getRoomId(user: string): Promise<string | null>\n /** Check if a room is currently live. */\n isRoomAlive(roomId: string): Promise<boolean>\n /** Get the best-quality FLV stream URL for a room. Returns null on failure. */\n getLiveUrl(roomId: string): Promise<string | null>\n /** Invalidate per-tick cache (call at start of each poll tick). */\n invalidateCache(): void\n}\n\nconst TIKTOK_BASES = [\"https://www.tiktok.com\", \"https://m.tiktok.com\"]\nconst WEBCAST_BASE = \"https://webcast.tiktok.com\"\n\nexport function createTikTokApi(http: HttpClient, logger?: Logger, showDebug?: boolean): TikTokApi {\n // Debug logging helper — writes to stderr only when --debug is passed.\n const debug = (msg: string) => {\n logger?.debug(msg)\n if (showDebug) process.stderr.write(`[API_DEBUG] ${msg}\\n`)\n }\n let cached: LiveInfo | null = null\n let cachedUser = \"\"\n\n async function ensureCache(user: string): Promise<LiveInfo | null> {\n if (cached && cachedUser === user) return cached\n cached = await fetchLiveInfo(user)\n cachedUser = user\n return cached\n }\n\n function invalidateCache(): void {\n cached = null\n cachedUser = \"\"\n }\n\n return {\n invalidateCache,\n\n async getRoomId(user: string): Promise<string | null> {\n const info = await ensureCache(user)\n return info?.roomId ?? null\n },\n\n async isRoomAlive(roomId: string): Promise<boolean> {\n if (cached && String(cached.roomId) === String(roomId)) {\n return cached.isLive\n }\n // Use the lightweight check_alive endpoint instead of re-fetching\n // the full room/info response.\n return fetchCheckAlive(roomId)\n },\n\n async getLiveUrl(roomId: string): Promise<string | null> {\n const info = await ensureCache(cachedUser || roomId)\n return info?.streamUrl ?? null\n },\n }\n\n // ─── Internal: fetch and parse SIGI_STATE / Universal Data ──\n\n /**\n * Try to fetch a TikTok user page from multiple domains.\n * www.tiktok.com is increasingly blocked by Slardar WAF; m.tiktok.com\n * often has different bot protection and may succeed.\n * Returns the HTML and the base URL that worked, or null if all fail.\n */\n async function tryFetchPage(\n user: string,\n path: string,\n ): Promise<{ html: string; baseUrl: string } | null> {\n for (const base of TIKTOK_BASES) {\n const url = `${base}/@${user}${path}`\n const res = await http.get(url)\n debug(`tryFetchPage: ${url} status=${res.status}, ok=${res.ok}`)\n if (!res.ok) continue\n\n const html = await res.text()\n // Reject Slardar WAF challenge pages (small + keyword)\n if (html.length < 5000 || html.includes(\"SlardarWAF\")) {\n debug(\n `tryFetchPage: ${url} WAF blocked (${html.length} bytes, slardar=${html.includes(\"SlardarWAF\")})`,\n )\n continue\n }\n\n debug(`tryFetchPage: ${url} succeeded (${html.length} bytes)`)\n return { html, baseUrl: base }\n }\n return null\n }\n\n async function fetchLiveInfo(user: string): Promise<LiveInfo | null> {\n let sigi: SigiState | null = null\n let html = \"\"\n\n try {\n const page = await tryFetchPage(user, \"/live\")\n if (!page) {\n debug(\"fetchLiveInfo: all domains blocked for /live page\")\n // Fall through to universal data / profile page fallback\n html = \"\"\n } else {\n html = page.html\n debug(\n `fetchLiveInfo: /live returned ${html.length} bytes (${page.baseUrl}), first 300: ${html.slice(0, 300).replace(/\\n/g, \"\\\\n\")}`,\n )\n\n sigi = extractSigiState(html)\n debug(`fetchLiveInfo: SIGI_STATE ${sigi ? \"found\" : \"not found\"}`)\n\n if (sigi) {\n const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom\n const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user\n\n debug(\n `fetchLiveInfo: liveRoom=${!!liveRoom}, userInfo=${!!userInfo}, userInfo.roomId=${userInfo?.roomId ?? \"null\"}`,\n )\n\n // ── Primary path: liveRoom is populated (server-rendered) ──\n if (liveRoom && userInfo?.roomId) {\n const roomId = String(userInfo.roomId)\n const isLive = liveRoom.status === 2 // 2 = live, 4 = offline\n debug(\n `fetchLiveInfo: primary path — roomId=${roomId}, status=${liveRoom.status}, isLive=${isLive}`,\n )\n\n // Extract stream URL: try SIGI_STATE first, then Webcast API fallback\n let streamUrl: string | null = null\n if (isLive) {\n streamUrl = extractStreamUrlFast(liveRoom)\n if (!streamUrl) streamUrl = findStreamUrlRecursively(sigi)\n if (!streamUrl) streamUrl = await fetchStreamUrlFromApi(roomId)\n }\n\n return { roomId, isLive, streamUrl, title: liveRoom.title ?? null }\n }\n\n // ── Fallback: userInfo has roomId, but liveRoom is async-loaded ──\n // TikTok may omit liveRoom from the server-rendered page and load it\n // via JavaScript. Query the Webcast API directly — it's authoritative.\n if (userInfo?.roomId) {\n const roomId = String(userInfo.roomId)\n debug(`fetchLiveInfo: userInfo fallback — roomId=${roomId}, querying Webcast API`)\n const roomInfo = await fetchRoomInfoFromRoomApi(roomId)\n debug(`fetchLiveInfo: Webcast API ${roomInfo ? \"returned room info\" : \"returned null\"}`)\n if (roomInfo) return roomInfo\n }\n\n // ── Legacy fallback: async-loaded SIGI_STATE ──\n debug(\"fetchLiveInfo: trying legacy fallback (UserModule)\")\n const legacy = await fetchLiveInfoFallback(sigi, user)\n debug(`fetchLiveInfo: legacy fallback ${legacy ? \"succeeded\" : \"returned null\"}`)\n if (legacy) return legacy\n } // end if (sigi)\n } // end else (page loaded)\n } catch (_err) {\n // Timeout (15s) or network error → treat as offline\n debug(`fetchLiveInfo: caught error — ${_err instanceof Error ? _err.message : String(_err)}`)\n return null\n }\n\n // ── API fallback: when page scraping fails (WAF), use TikTok API directly ──\n // The /api-live/user/room/ endpoint returns roomId + live status without\n // the Slardar WAF that blocks www.tiktok.com HTML pages.\n if (!html || html.length < 5000) {\n debug(\"fetchLiveInfo: trying API fallback (api-live/user/room)\")\n const apiResult = await fetchLiveInfoFromApi(user)\n if (apiResult) return apiResult\n }\n\n // ── New fallback: TikTok's unified page structure ──\n // TikTok migrated from SIGI_STATE to __UNIVERSAL_DATA_FOR_REHYDRATION__.\n // The /live page may not have it, so try the main profile page.\n debug(`fetchLiveInfo: trying universal data fallback (html length ${html.length})`)\n const { roomId } = extractUserFromUniversalData(html)\n debug(`fetchLiveInfo: universal data roomId=${roomId ?? \"null\"}`)\n if (roomId) {\n // If the /live page has the roomId, use room/info (reliable)\n const result = await fetchRoomInfoFromRoomApi(roomId)\n debug(`fetchLiveInfo: universal data Webcast API ${result ? \"succeeded\" : \"returned null\"}`)\n if (result) return result\n }\n // No roomId available — fall through to profile page to check for\n // __UNIVERSAL_DATA_FOR_REHYDRATION__ which may have the roomId when live.\n\n // Final fallback: fetch the main profile page for webapp.user-detail\n debug(\"fetchLiveInfo: trying profile page fallback\")\n const profileResult = await fetchLiveInfoFromProfile(user)\n debug(`fetchLiveInfo: profile page ${profileResult ? \"succeeded\" : \"returned null\"}`)\n return profileResult\n }\n\n /**\n * Fetch the main profile page to extract user info from\n * __UNIVERSAL_DATA_FOR_REHYDRATION__. When the user is live, their\n * roomId is embedded in the data, allowing us to fetch room info and\n * stream URL via the /webcast/room/info/ endpoint (which works\n * reliably). If no roomId is present the user is offline.\n */\n async function fetchLiveInfoFromProfile(user: string): Promise<LiveInfo | null> {\n try {\n const page = await tryFetchPage(user, \"\")\n if (!page) {\n debug(\"fetchLiveInfoFromProfile: all domains blocked for profile page\")\n return null\n }\n debug(\n `fetchLiveInfoFromProfile: returned ${page.html.length} bytes (${page.baseUrl}), first 300: ${page.html.slice(0, 300).replace(/\\n/g, \"\\\\n\")}`,\n )\n const { userId, roomId } = extractUserFromUniversalData(page.html)\n if (!userId) return null\n\n // If roomId is embedded in the profile page, the user is live.\n // Use room/info which works reliably for both live and offline rooms.\n if (roomId) {\n return fetchRoomInfoFromRoomApi(roomId)\n }\n\n // No roomId means the user is offline (room/enter was unreliable).\n return null\n } catch {\n return null\n }\n }\n\n /**\n * Fetch live info from TikTok's internal API endpoint.\n * This bypasses the Slardar WAF that blocks www.tiktok.com HTML pages.\n *\n * Endpoint: /api-live/user/room/?aid=1988&uniqueId=${user}&sourceType=54\n * Returns the roomId, user status (2 = live), and other user metadata.\n * When the user is live, also queries the Webcast API for a stream URL.\n */\n async function fetchLiveInfoFromApi(user: string): Promise<LiveInfo | null> {\n try {\n const url = `https://www.tiktok.com/api-live/user/room/?aid=1988&uniqueId=${encodeURIComponent(user)}&sourceType=54`\n const res = await http.get(url)\n debug(`fetchLiveInfoFromApi: ${url} status=${res.status}`)\n if (!res.ok) return null\n\n const text = await res.text()\n debug(`fetchLiveInfoFromApi: returned ${text.length} bytes`)\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n const userData = (data?.data as Record<string, unknown> | undefined)?.user as\n | { roomId?: string; status?: number; uniqueId?: string }\n | undefined\n\n if (!userData?.roomId) {\n debug(\"fetchLiveInfoFromApi: no roomId in API response\")\n return null\n }\n\n const roomId = String(userData.roomId)\n // status: 2 = live, 4 = offline, 1 = ...\n const isLive = userData.status === 2\n debug(`fetchLiveInfoFromApi: roomId=${roomId}, status=${userData.status}, isLive=${isLive}`)\n\n if (!isLive) {\n return { roomId, isLive: false, streamUrl: null, title: null }\n }\n\n // Query Webcast API for stream URL\n const streamUrl = await fetchStreamUrlFromApi(roomId)\n return { roomId, isLive: true, streamUrl, title: null }\n } catch {\n return null\n }\n }\n\n /**\n * Shared boilerplate for Webcast room/info API calls.\n * Handles URL construction, HTTP GET, JSON parsing, status code checks,\n * and error handling. The caller provides an extractor for the response data.\n */\n async function fetchFromRoomApi<T>(\n roomId: string,\n extract: (data: Record<string, unknown>) => T | null,\n ): Promise<T | null> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`\n const res = await http.get(url)\n debug(`fetchFromRoomApi: roomId=${roomId} status=${res.status}, ok=${res.ok}`)\n if (!res.ok) return null\n\n const text = await res.text()\n debug(`fetchFromRoomApi: roomId=${roomId} returned ${text.length} bytes`)\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n\n // status_code 4003110 = live restriction / room not found\n if ((data as { status_code?: unknown }).status_code === 4003110) return null\n\n return extract(data)\n } catch {\n return null\n }\n }\n\n /**\n * Check if a room is currently alive using the lightweight\n * /webcast/room/check_alive/ endpoint.\n *\n * This is faster and cheaper than fetching the full room/info response.\n * Returns `true` if the room is live, `false` otherwise (including errors).\n *\n * Endpoint: /webcast/room/check_alive/?aid=1988&region=CH&room_ids={id}&user_is_login=true\n * Response: { \"data\": [{ \"alive\": true/false, \"room_id\": \"...\" }] }\n */\n async function fetchCheckAlive(roomId: string): Promise<boolean> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/check_alive/?aid=1988&region=CH&room_ids=${roomId}&user_is_login=true`\n const res = await http.get(url)\n debug(`fetchCheckAlive: roomId=${roomId} status=${res.status}, ok=${res.ok}`)\n if (!res.ok) return false\n\n const text = await res.text()\n debug(`fetchCheckAlive: roomId=${roomId} returned ${text.length} bytes`)\n if (text.length === 0) return false\n\n const data = JSON.parse(text) as Record<string, unknown>\n const items = data?.data as Array<Record<string, unknown>> | undefined\n const alive = items?.[0]?.alive === true\n debug(`fetchCheckAlive: roomId=${roomId} alive=${alive}`)\n return alive\n } catch (err) {\n debug(\n `fetchCheckAlive: roomId=${roomId} error — ${err instanceof Error ? err.message : String(err)}`,\n )\n return false\n }\n }\n\n /**\n * Fetch room info from the Webcast room/info API. Requires a known roomId.\n */\n async function fetchRoomInfoFromRoomApi(roomId: string): Promise<LiveInfo | null> {\n return fetchFromRoomApi(roomId, (data) => {\n const roomData = (data as { data?: Record<string, unknown> }).data\n if (!roomData) return null\n\n const status = roomData.status ?? 4\n const isLive = status === 2\n const streamUrl = isLive ? findStreamUrlRecursively(roomData) : null\n\n return { roomId, isLive, streamUrl, title: (roomData.title as string | undefined) ?? null }\n })\n }\n\n /**\n * Secondary live-info extraction when SIGI_STATE's LiveRoom data is\n * async-loaded. Uses LiveRoom.liveRoomStatus for the live flag and\n * UserModule.users[user].roomId for the room ID. Stream URL is obtained\n * from the Webcast API (room/info/) as a final fallback.\n */\n async function fetchLiveInfoFallback(sigi: SigiState, user: string): Promise<LiveInfo | null> {\n const liveRoomStatus = sigi.LiveRoom?.liveRoomStatus\n const isLive = liveRoomStatus === 2 // 2 = live\n\n // Try to get roomId from UserModule (usually populated even when LiveRoom is not)\n const userModule = sigi.UserModule?.users?.[user]\n let roomId: string | null = null\n\n if (userModule?.roomId) {\n roomId =\n typeof userModule.roomId === \"object\"\n ? String((userModule.roomId as { id: string }).id ?? \"\")\n : String(userModule.roomId)\n }\n\n // If we have a roomId, use Webcast room/info for the stream URL\n if (roomId) {\n const streamUrl = isLive ? await fetchStreamUrlFromApi(roomId) : null\n return { roomId, isLive, streamUrl, title: null }\n }\n\n // No roomId available — treat as offline (room/enter was unreliable).\n return null\n }\n\n /**\n * Fetch stream URL from the Webcast API.\n * Covers cases where SIGI_STATE doesn't include stream data.\n */\n async function fetchStreamUrlFromApi(roomId: string): Promise<string | null> {\n return fetchFromRoomApi(roomId, (data) => findStreamUrlRecursively(data))\n }\n}\n\n// ─── Types ──────────────────────────────────────────────────\n\ninterface LiveInfo {\n roomId: string\n isLive: boolean\n streamUrl: string | null\n title: string | null\n}\n\ninterface SigiState {\n LiveRoom?: {\n liveRoomStatus?: number\n liveRoomUserInfo?: {\n user?: {\n id?: string | number\n roomId?: string | number\n }\n liveRoom?: {\n status?: number // 2 = live, 4 = offline\n title?: string\n startTime?: number\n streamData?: {\n pull_data?: {\n stream_data?: string // JSON string with FLV/HLS URLs\n }\n }\n }\n }\n }\n UserModule?: {\n users?: {\n [username: string]: {\n id?: string // numeric user ID\n uniqueId?: string\n roomId?: string | { id: string }\n }\n }\n }\n}\n\n// ─── HTML parsing ──────────────────────────────────────────\n\n/**\n * @public Exported for testing.\n */\nexport function extractSigiState(html: string): SigiState | null {\n const match = html.match(/<script\\s+id=\"SIGI_STATE\"\\s+type=\"application\\/json\">(.*?)<\\/script>/)\n if (!match?.[1]) return null\n try {\n return JSON.parse(match[1]) as SigiState\n } catch {\n return null\n }\n}\n\n/**\n * Extract the numeric user ID and optional room ID from TikTok's new\n * page structure (__UNIVERSAL_DATA_FOR_REHYDRATION__).\n *\n * The profile page (/@user) includes \"webapp.user-detail\" which\n * contains the user's numeric ID. When the user is live, the roomId\n * is also embedded here.\n *\n * Returns { userId, roomId } where userId is always set when the data\n * is found, and roomId is set only when the user is currently live.\n *\n * @public Exported for testing and fixture capture.\n */\nexport function extractUserFromUniversalData(html: string): {\n userId: string | null\n roomId: string | null\n} {\n const match = html.match(\n /<script\\s+id=\"__UNIVERSAL_DATA_FOR_REHYDRATION__\"\\s+type=\"application\\/json\">(.*?)<\\/script>/,\n )\n if (!match?.[1]) return { userId: null, roomId: null }\n try {\n const data = JSON.parse(match[1])\n const scope = (data as Record<string, unknown>).__DEFAULT_SCOPE__ ?? data\n const userDetail = scope?.[\"webapp.user-detail\"]\n const user = userDetail?.userInfo?.user\n const userId = user?.id ? String(user.id) : null\n const roomId = user?.roomId ? String(user.roomId) : null\n return { userId, roomId }\n } catch {\n return { userId: null, roomId: null }\n }\n}\n\n// ─── Stream URL extraction ─────────────────────────────────\n\n/**\n * Fast-path: extract stream URL from the known SIGI_STATE location.\n * This covers ~95% of cases without needing a full recursive search.\n *\n * Prefers FLV (lower latency) over HLS/other protocols.\n */\nfunction extractStreamUrlFast(\n liveRoom: NonNullable<NonNullable<SigiState[\"LiveRoom\"]>[\"liveRoomUserInfo\"]>[\"liveRoom\"],\n): string | null {\n try {\n const raw = liveRoom?.streamData?.pull_data?.stream_data\n if (!raw) return null\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const data = (parsed as Record<string, unknown>)?.data as Record<string, unknown> | undefined\n if (!data) return null\n\n // Try all known quality keys, preferring higher quality.\n // FLV is preferred (lower latency); HLS is the fallback.\n const qualities = [\"origin\", \"fhd\", \"uhd\", \"hd\", \"sd\", \"ld\"]\n for (const q of qualities) {\n const entry = data[q] as { main?: { flv?: string; hls?: string } } | undefined\n // Prefer FLV over HLS when both are available\n const url = entry?.main?.flv ?? entry?.main?.hls\n if (url) return url\n }\n return null\n } catch {\n return null\n }\n}\n\n/**\n * Brute-force recursive search for a stream URL anywhere in a JSON value.\n * Survives TikTok changing key names or nesting structure.\n *\n * Prefers FLV over HLS when both are present in the response.\n *\n * Inspired by PR #430 (Michele0303/tiktok-live-recorder).\n */\nexport function findStreamUrlRecursively(obj: unknown): string | null {\n // Collect all candidate URLs with their preference\n const flvUrls: string[] = []\n const m3u8Urls: string[] = []\n\n function walk(value: unknown): void {\n if (typeof value === \"string\") {\n // Direct URL match\n if (\n (value.startsWith(\"http://\") || value.startsWith(\"https://\")) &&\n (value.includes(\".flv\") || value.includes(\".m3u8\"))\n ) {\n if (value.includes(\".flv\")) {\n flvUrls.push(value)\n } else {\n m3u8Urls.push(value)\n }\n return\n }\n // JSON-encoded string — parse and recurse\n if (value.startsWith(\"{\") || value.startsWith(\"[\")) {\n try {\n walk(JSON.parse(value))\n } catch {\n // skip\n }\n }\n return\n }\n\n if (value && typeof value === \"object\") {\n if (Array.isArray(value)) {\n for (const item of value) {\n walk(item)\n }\n } else {\n for (const val of Object.values(value as Record<string, unknown>)) {\n walk(val)\n }\n }\n }\n }\n\n walk(obj)\n\n // Prefer FLV (lower latency) over HLS\n if (flvUrls.length > 0) return flvUrls[0] as string\n if (m3u8Urls.length > 0) return m3u8Urls[0] as string\n return null\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"monitor.mjs","names":[],"sources":["../../src/core/monitor.ts"],"sourcesContent":["/**\n * Polling monitor — calls a tick function on a configurable interval.\n * Fires the first tick immediately, then every N minutes.\n *\n * Uses an AbortController so stop() can interrupt the sleep between\n * ticks immediately, avoiding a busy 1-second polling loop.\n */\n\nimport type { Logger } from \"../lib/logger\"\nimport { sleep } from \"../lib/utils\"\n\nexport interface PollingMonitor {\n start: () => Promise<void>\n stop: () => Promise<void>\n /** Schedule the monitor to stop after the current tick completes. */\n stopAfterCurrentTick: () => void\n}\n\nexport function createPollingMonitor(opts: {\n intervalMinutes: number\n onTick: () => Promise<void>\n logger?: Logger\n}): PollingMonitor {\n const intervalMs = opts.intervalMinutes * 60 * 1000\n const stopSignal = new AbortController()\n let currentTick: Promise<void> | null = null\n let stopAfterTick = false\n\n async function start(): Promise<void> {\n opts.logger?.info(`Polling started (interval: ${opts.intervalMinutes} min)`)\n\n while (!stopSignal.signal.aborted) {\n currentTick = opts.onTick()\n try {\n await currentTick\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n opts.logger?.error(`Tick error: ${msg}`)\n } finally {\n currentTick = null\n }\n\n if (stopAfterTick) {\n stopSignal.abort()\n }\n if (stopSignal.signal.aborted) break\n\n // Sleep for the full interval — stop() aborts the signal to wake us up early\n await sleep(intervalMs, stopSignal.signal)\n }\n\n opts.logger?.info(\"Polling stopped\")\n }\n\n async function stop(): Promise<void> {\n // Signal start() to exit the loop immediately\n stopSignal.abort()\n // Wait for in-flight tick to finish (e.g. recording → converting → MP4)\n if (currentTick) {\n await currentTick\n }\n }\n\n function stopAfterCurrentTick(): void {\n stopAfterTick = true\n }\n\n return { start, stop, stopAfterCurrentTick }\n}\n"],"mappings":";;AAkBA,SAAgB,qBAAqB,MAIlB;CACjB,MAAM,aAAa,KAAK,kBAAkB,KAAK;CAC/C,MAAM,aAAa,IAAI,gBAAgB;CACvC,IAAI,cAAoC;CACxC,IAAI,gBAAgB;CAEpB,eAAe,QAAuB;EACpC,KAAK,QAAQ,KAAK,8BAA8B,KAAK,gBAAgB,MAAM;EAE3E,OAAO,CAAC,WAAW,OAAO,SAAS;GACjC,cAAc,KAAK,OAAO;GAC1B,IAAI;IACF,MAAM;GACR,SAAS,KAAK;IACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;IAC3D,KAAK,QAAQ,MAAM,eAAe,KAAK;GACzC,UAAU;IACR,cAAc;GAChB;GAEA,IAAI,eACF,WAAW,MAAM;GAEnB,IAAI,WAAW,OAAO,SAAS;GAG/B,MAAM,MAAM,YAAY,WAAW,MAAM;EAC3C;EAEA,KAAK,QAAQ,KAAK,iBAAiB;CACrC;CAEA,eAAe,OAAsB;EAEnC,WAAW,MAAM;EAEjB,IAAI,aACF,MAAM;CAEV;CAEA,SAAS,uBAA6B;EACpC,gBAAgB;CAClB;CAEA,OAAO;EAAE;EAAO;EAAM;CAAqB;AAC7C"}
@@ -1,55 +0,0 @@
1
- import { TikTokError } from "../../lib/config.mjs";
2
- import { findFfmpegPath } from "../../lib/utils.mjs";
3
- import { execa } from "execa";
4
- import { unlinkSync } from "node:fs";
5
- //#region src/core/recorder/convert.ts
6
- /**
7
- * Converter — spawns FFmpeg to convert TS → MP4 (stream copy by default).
8
- * Deletes the original file on success.
9
- */
10
- function createConverter(logger, signal) {
11
- async function convert(input) {
12
- const output = input.replace(/\.(flv|ts)$/i, ".mp4");
13
- logger?.info(`Converting: ${input} → ${output}`);
14
- const ffmpegPath = findFfmpegPath();
15
- if (!ffmpegPath) throw new TikTokError("ffmpeg-not-found", "FFmpeg not found. Install it:\n Linux: apt install ffmpeg / brew install ffmpeg / pacman -S ffmpeg\n macOS: brew install ffmpeg\n Windows: choco install ffmpeg");
16
- await runFfmpeg(ffmpegPath, input, output, signal);
17
- try {
18
- unlinkSync(input);
19
- logger?.info(`Deleted original: ${input}`);
20
- } catch {
21
- logger?.warn(`Could not delete original: ${input}`);
22
- }
23
- logger?.info(`Converted: ${output}`);
24
- return output;
25
- }
26
- return { convert };
27
- }
28
- async function runFfmpeg(ffmpegPath, input, output, signal) {
29
- const args = [
30
- "-i",
31
- input,
32
- "-c",
33
- "copy",
34
- "-y",
35
- output
36
- ];
37
- try {
38
- await execa(ffmpegPath, args, {
39
- cancelSignal: signal,
40
- stdio: [
41
- "ignore",
42
- "pipe",
43
- "pipe"
44
- ]
45
- });
46
- } catch (err) {
47
- if (signal?.aborted) throw new TikTokError("aborted", "Aborted by user");
48
- const stderr = err.stderr ?? "";
49
- throw new TikTokError("ffmpeg-error", `FFmpeg exited with code ${err.exitCode}\n${stderr.slice(-500)}`);
50
- }
51
- }
52
- //#endregion
53
- export { createConverter };
54
-
55
- //# sourceMappingURL=convert.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"convert.mjs","names":[],"sources":["../../../src/core/recorder/convert.ts"],"sourcesContent":["/**\n * Converter — spawns FFmpeg to convert TS → MP4 (stream copy by default).\n * Deletes the original file on success.\n */\n\nimport { unlinkSync } from \"node:fs\"\nimport { execa } from \"execa\"\nimport { TikTokError } from \"../../lib/config\"\nimport type { Logger } from \"../../lib/logger\"\nimport { findFfmpegPath } from \"../../lib/utils\"\n\nexport interface Converter {\n /** Convert a TS file to MP4. Returns the output filepath. */\n convert: (input: string) => Promise<string>\n}\n\nexport function createConverter(logger?: Logger, signal?: AbortSignal): Converter {\n async function convert(input: string): Promise<string> {\n const output = input.replace(/\\.(flv|ts)$/i, \".mp4\")\n\n logger?.info(`Converting: ${input} → ${output}`)\n\n const ffmpegPath = findFfmpegPath()\n if (!ffmpegPath) {\n throw new TikTokError(\n \"ffmpeg-not-found\",\n \"FFmpeg not found. Install it:\\n\" +\n \" Linux: apt install ffmpeg / brew install ffmpeg / pacman -S ffmpeg\\n\" +\n \" macOS: brew install ffmpeg\\n\" +\n \" Windows: choco install ffmpeg\",\n )\n }\n\n await runFfmpeg(ffmpegPath, input, output, signal)\n\n // Delete original TS\n try {\n unlinkSync(input)\n logger?.info(`Deleted original: ${input}`)\n } catch {\n logger?.warn(`Could not delete original: ${input}`)\n }\n\n logger?.info(`Converted: ${output}`)\n return output\n }\n\n return { convert }\n}\n\nasync function runFfmpeg(\n ffmpegPath: string,\n input: string,\n output: string,\n signal?: AbortSignal,\n): Promise<void> {\n const args = [\"-i\", input, \"-c\", \"copy\", \"-y\", output]\n\n try {\n await execa(ffmpegPath, args, { cancelSignal: signal, stdio: [\"ignore\", \"pipe\", \"pipe\"] })\n } catch (err: unknown) {\n if (signal?.aborted) {\n throw new TikTokError(\"aborted\", \"Aborted by user\")\n }\n const stderr = (err as { stderr?: string }).stderr ?? \"\"\n throw new TikTokError(\n \"ffmpeg-error\",\n `FFmpeg exited with code ${(err as { exitCode?: number }).exitCode}\\n${stderr.slice(-500)}`,\n )\n }\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAgB,gBAAgB,QAAiB,QAAiC;CAChF,eAAe,QAAQ,OAAgC;EACrD,MAAM,SAAS,MAAM,QAAQ,gBAAgB,MAAM;EAEnD,QAAQ,KAAK,eAAe,MAAM,KAAK,QAAQ;EAE/C,MAAM,aAAa,eAAe;EAClC,IAAI,CAAC,YACH,MAAM,IAAI,YACR,oBACA,sKAIF;EAGF,MAAM,UAAU,YAAY,OAAO,QAAQ,MAAM;EAGjD,IAAI;GACF,WAAW,KAAK;GAChB,QAAQ,KAAK,qBAAqB,OAAO;EAC3C,QAAQ;GACN,QAAQ,KAAK,8BAA8B,OAAO;EACpD;EAEA,QAAQ,KAAK,cAAc,QAAQ;EACnC,OAAO;CACT;CAEA,OAAO,EAAE,QAAQ;AACnB;AAEA,eAAe,UACb,YACA,OACA,QACA,QACe;CACf,MAAM,OAAO;EAAC;EAAM;EAAO;EAAM;EAAQ;EAAM;CAAM;CAErD,IAAI;EACF,MAAM,MAAM,YAAY,MAAM;GAAE,cAAc;GAAQ,OAAO;IAAC;IAAU;IAAQ;GAAM;EAAE,CAAC;CAC3F,SAAS,KAAc;EACrB,IAAI,QAAQ,SACV,MAAM,IAAI,YAAY,WAAW,iBAAiB;EAEpD,MAAM,SAAU,IAA4B,UAAU;EACtD,MAAM,IAAI,YACR,gBACA,2BAA4B,IAA8B,SAAS,IAAI,OAAO,MAAM,IAAI,GAC1F;CACF;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"download.mjs","names":[],"sources":["../../../src/core/recorder/download.ts"],"sourcesContent":["/**\n * Live stream downloader — downloads a live stream via FFmpeg stdout pipe.\n *\n * Outputs MPEG-TS (.ts) format, which is crash-safe and append-friendly.\n * FFmpeg handles HTTP reconnection via -reconnect flags. When TikTok's\n * short-lived stream URL expires, FFmpeg exits and the outer loop fetches\n * a fresh URL via getNextUrl.\n *\n * Both FLV and HLS streams are handled by the same download function —\n * the only difference is an optional label in the log messages.\n */\n\nimport { createWriteStream, statSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { TikTokError } from \"../../lib/config\"\nimport type { Logger } from \"../../lib/logger\"\nimport { bytesToHuman, findFfmpegPath, formatDuration, formatFilename } from \"../../lib/utils\"\nimport { pipeFfmpegSegment } from \"./ffmpeg-utils\"\nimport type { DownloadResult, ProgressInfo } from \"./stream\"\n\nexport async function downloadStream(\n liveUrl: string,\n user: string,\n outputDir: string,\n maxDuration: number,\n onProgress: ((info: ProgressInfo) => void) | undefined,\n getNextUrl: (() => Promise<string | null>) | undefined,\n signal: AbortSignal,\n logger?: Logger,\n label?: string,\n cookies?: Record<string, string>,\n): Promise<DownloadResult> {\n const filename = formatFilename(user, \"ts\")\n const filepath = join(outputDir, filename)\n const prefix = label ? ` (${label})` : \"\"\n logger?.info(`Recording${prefix}: ${filename}`)\n\n const startTime = Date.now()\n\n /** File size read directly from disk — authoritative, avoids in-memory counter drift. */\n let fileSize = 0\n let lastProgressTime = 0\n\n const ffmpegPath = findFfmpegPath()\n if (!ffmpegPath) {\n throw new TikTokError(\n \"ffmpeg-not-found\",\n \"FFmpeg not found — required for stream download. Install it:\\n\" +\n \" Linux: apt install ffmpeg\\n macOS: brew install ffmpeg\",\n )\n }\n\n function elapsed(): number {\n return (Date.now() - startTime) / 1000\n }\n\n const writer = createWriteStream(filepath)\n\n try {\n while (!signal.aborted) {\n const remaining = maxDuration > 0 ? Math.max(1, maxDuration - elapsed()) : undefined\n await pipeFfmpegSegment(ffmpegPath, liveUrl, writer, signal, remaining, cookies)\n\n // Read actual file size from disk — always authoritative\n try {\n fileSize = statSync(filepath).size\n } catch {\n // File may not be accessible; leave fileSize at last known value\n }\n\n // Report progress after each segment\n const now = Date.now()\n const e = elapsed()\n if (onProgress && now - lastProgressTime >= 1000) {\n lastProgressTime = now\n const speed = e > 0 ? fileSize / e : 0\n onProgress({ bytes: fileSize, elapsed: e, speed })\n }\n\n if (signal.aborted) break\n if (maxDuration > 0 && e >= maxDuration) {\n logger?.info(`Duration limit reached (${maxDuration}s)`)\n break\n }\n if (!getNextUrl) break\n\n const nextUrl = await getNextUrl()\n if (!nextUrl) {\n logger?.info(\"No new stream URL — recording finished\")\n break\n }\n liveUrl = nextUrl\n }\n } catch (err) {\n logger?.error(`Recording error: ${err instanceof Error ? err.message : String(err)}`)\n } finally {\n await new Promise<void>((resolve) => writer.end(resolve))\n }\n\n const duration = elapsed()\n // Final authoritative size after flush\n try {\n fileSize = statSync(filepath).size\n } catch {\n // keep last known value\n }\n if (onProgress) {\n onProgress({\n bytes: fileSize,\n elapsed: duration,\n speed: duration > 0 ? fileSize / duration : 0,\n })\n }\n logger?.info(\n `Recording finished: ${filename} (${formatDuration(duration)}, ${bytesToHuman(fileSize)})`,\n )\n\n return { file: filepath, duration, size: fileSize }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,eAAsB,eACpB,SACA,MACA,WACA,aACA,YACA,YACA,QACA,QACA,OACA,SACyB;CACzB,MAAM,WAAW,eAAe,MAAM,IAAI;CAC1C,MAAM,WAAW,KAAK,WAAW,QAAQ;CACzC,MAAM,SAAS,QAAQ,KAAK,MAAM,KAAK;CACvC,QAAQ,KAAK,YAAY,OAAO,IAAI,UAAU;CAE9C,MAAM,YAAY,KAAK,IAAI;;CAG3B,IAAI,WAAW;CACf,IAAI,mBAAmB;CAEvB,MAAM,aAAa,eAAe;CAClC,IAAI,CAAC,YACH,MAAM,IAAI,YACR,oBACA,2HAEF;CAGF,SAAS,UAAkB;EACzB,QAAQ,KAAK,IAAI,IAAI,aAAa;CACpC;CAEA,MAAM,SAAS,kBAAkB,QAAQ;CAEzC,IAAI;EACF,OAAO,CAAC,OAAO,SAAS;GACtB,MAAM,YAAY,cAAc,IAAI,KAAK,IAAI,GAAG,cAAc,QAAQ,CAAC,IAAI,KAAA;GAC3E,MAAM,kBAAkB,YAAY,SAAS,QAAQ,QAAQ,WAAW,OAAO;GAG/E,IAAI;IACF,WAAW,SAAS,QAAQ,CAAC,CAAC;GAChC,QAAQ,CAER;GAGA,MAAM,MAAM,KAAK,IAAI;GACrB,MAAM,IAAI,QAAQ;GAClB,IAAI,cAAc,MAAM,oBAAoB,KAAM;IAChD,mBAAmB;IACnB,MAAM,QAAQ,IAAI,IAAI,WAAW,IAAI;IACrC,WAAW;KAAE,OAAO;KAAU,SAAS;KAAG;IAAM,CAAC;GACnD;GAEA,IAAI,OAAO,SAAS;GACpB,IAAI,cAAc,KAAK,KAAK,aAAa;IACvC,QAAQ,KAAK,2BAA2B,YAAY,GAAG;IACvD;GACF;GACA,IAAI,CAAC,YAAY;GAEjB,MAAM,UAAU,MAAM,WAAW;GACjC,IAAI,CAAC,SAAS;IACZ,QAAQ,KAAK,wCAAwC;IACrD;GACF;GACA,UAAU;EACZ;CACF,SAAS,KAAK;EACZ,QAAQ,MAAM,oBAAoB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG;CACtF,UAAU;EACR,MAAM,IAAI,SAAe,YAAY,OAAO,IAAI,OAAO,CAAC;CAC1D;CAEA,MAAM,WAAW,QAAQ;CAEzB,IAAI;EACF,WAAW,SAAS,QAAQ,CAAC,CAAC;CAChC,QAAQ,CAER;CACA,IAAI,YACF,WAAW;EACT,OAAO;EACP,SAAS;EACT,OAAO,WAAW,IAAI,WAAW,WAAW;CAC9C,CAAC;CAEH,QAAQ,KACN,uBAAuB,SAAS,IAAI,eAAe,QAAQ,EAAE,IAAI,aAAa,QAAQ,EAAE,EAC1F;CAEA,OAAO;EAAE,MAAM;EAAU;EAAU,MAAM;CAAS;AACpD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ffmpeg-utils.mjs","names":[],"sources":["../../../src/core/recorder/ffmpeg-utils.ts"],"sourcesContent":["/**\n * Shared FFmpeg utilities — stream-download pipe primitive and constants.\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\"\nimport type { WriteStream } from \"node:fs\"\nimport { TikTokError } from \"../../lib/config\"\n\n/** Kill FFmpeg if no output is received within this window after spawn. */\nexport const FFMPEG_STARTUP_TIMEOUT = 30_000\n\n/**\n * Build FFmpeg HTTP option arguments for cookies and headers.\n * TikTok's CDN requires session cookies and a Referer to serve the stream.\n */\nfunction buildHttpArgs(cookies?: Record<string, string>): string[] {\n const args: string[] = []\n\n // ponytail: user-agent header, CDNs tend to be picky\n args.push(\n \"-user_agent\",\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0\",\n )\n args.push(\"-headers\", \"Referer: https://www.tiktok.com/\\r\\n\")\n\n if (cookies && Object.keys(cookies).length > 0) {\n const cookieStr = Object.entries(cookies)\n .map(([k, v]) => `${k}=${v}`)\n .join(\"\\r\\n\")\n args.push(\"-cookies\", cookieStr)\n }\n\n return args\n}\n\n/** Standard FFmpeg reconnect + mpegts pipe arguments. */\nconst FFMPEG_BASE_ARGS = [\n \"-reconnect\",\n \"1\",\n \"-reconnect_at_eof\",\n \"1\",\n \"-reconnect_streamed\",\n \"1\",\n \"-reconnect_delay_max\",\n \"5\",\n \"-c\",\n \"copy\",\n \"-f\",\n \"mpegts\",\n \"pipe:1\",\n]\n\n/**\n * Spawn an FFmpeg process to download from `url` and pipe its stdout to\n * `writer` with backpressure handling.\n *\n * Uses child_process.spawn directly (not execa) because execa v9 defaults\n * to encoding stdout as UTF-8, which corrupts binary MPEG-TS data.\n */\nexport async function pipeFfmpegSegment(\n ffmpegPath: string,\n url: string,\n writer: WriteStream,\n signal: AbortSignal,\n maxDuration?: number,\n cookies?: Record<string, string>,\n): Promise<void> {\n const durationArgs = maxDuration ? [\"-t\", String(maxDuration)] : []\n const httpArgs = buildHttpArgs(cookies)\n // ponytail: input options (-headers, -cookies, -user_agent) must come before -i\n const args = [...httpArgs, \"-i\", url, ...durationArgs, ...FFMPEG_BASE_ARGS]\n const proc: ChildProcess = spawn(ffmpegPath, args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n\n let stderr = \"\"\n let firstDataTimer: ReturnType<typeof setTimeout> | null = null\n\n const clearStartupTimer = () => {\n if (firstDataTimer) {\n clearTimeout(firstDataTimer)\n firstDataTimer = null\n }\n }\n\n // Forward abort signal to process\n const onAbort = () => {\n if (!proc.killed) proc.kill(\"SIGTERM\")\n }\n signal.addEventListener(\"abort\", onAbort, { once: true })\n\n proc.stderr?.on(\"data\", (chunk: Buffer) => {\n clearStartupTimer()\n stderr += chunk.toString()\n if (stderr.length > 10000) stderr = stderr.slice(-5000)\n })\n\n proc.stdout?.on(\"data\", (chunk: Buffer) => {\n clearStartupTimer()\n const canContinue = writer.write(chunk)\n if (!canContinue) {\n proc.stdout?.pause()\n writer.once(\"drain\", () => proc.stdout?.resume())\n }\n })\n\n await new Promise<void>((resolve, reject) => {\n firstDataTimer = setTimeout(() => {\n if (!proc.killed) {\n proc.kill(\"SIGTERM\")\n setTimeout(() => {\n if (!proc.killed) proc.kill(\"SIGKILL\")\n }, 2000)\n }\n reject(\n new TikTokError(\n \"ffmpeg-error\",\n `FFmpeg startup timed out after ${FFMPEG_STARTUP_TIMEOUT / 1000}s\\n${stderr.slice(-500)}`,\n ),\n )\n }, FFMPEG_STARTUP_TIMEOUT)\n\n proc.on(\"close\", (code) => {\n clearStartupTimer()\n signal.removeEventListener(\"abort\", onAbort)\n if (signal.aborted) {\n resolve()\n return\n }\n if (code === 0 || code === null) {\n resolve()\n return\n }\n reject(\n new TikTokError(\"ffmpeg-error\", `FFmpeg exited with code ${code}\\n${stderr.slice(-500)}`),\n )\n })\n\n proc.on(\"error\", (err) => {\n clearStartupTimer()\n signal.removeEventListener(\"abort\", onAbort)\n reject(err)\n })\n })\n}\n"],"mappings":";;;;;;;AASA,MAAa,yBAAyB;;;;;AAMtC,SAAS,cAAc,SAA4C;CACjE,MAAM,OAAiB,CAAC;CAGxB,KAAK,KACH,eACA,kFACF;CACA,KAAK,KAAK,YAAY,sCAAsC;CAE5D,IAAI,WAAW,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG;EAC9C,MAAM,YAAY,OAAO,QAAQ,OAAO,CAAC,CACtC,KAAK,CAAC,GAAG,OAAO,GAAG,EAAE,GAAG,GAAG,CAAC,CAC5B,KAAK,MAAM;EACd,KAAK,KAAK,YAAY,SAAS;CACjC;CAEA,OAAO;AACT;;AAGA,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;AASA,eAAsB,kBACpB,YACA,KACA,QACA,QACA,aACA,SACe;CACf,MAAM,eAAe,cAAc,CAAC,MAAM,OAAO,WAAW,CAAC,IAAI,CAAC;CAIlE,MAAM,OAAqB,MAAM,YAAY;EAD/B,GAFG,cAAc,OAEP;EAAG;EAAM;EAAK,GAAG;EAAc,GAAG;CACV,GAAG,EACjD,OAAO;EAAC;EAAU;EAAQ;CAAM,EAClC,CAAC;CAED,IAAI,SAAS;CACb,IAAI,iBAAuD;CAE3D,MAAM,0BAA0B;EAC9B,IAAI,gBAAgB;GAClB,aAAa,cAAc;GAC3B,iBAAiB;EACnB;CACF;CAGA,MAAM,gBAAgB;EACpB,IAAI,CAAC,KAAK,QAAQ,KAAK,KAAK,SAAS;CACvC;CACA,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAExD,KAAK,QAAQ,GAAG,SAAS,UAAkB;EACzC,kBAAkB;EAClB,UAAU,MAAM,SAAS;EACzB,IAAI,OAAO,SAAS,KAAO,SAAS,OAAO,MAAM,IAAK;CACxD,CAAC;CAED,KAAK,QAAQ,GAAG,SAAS,UAAkB;EACzC,kBAAkB;EAElB,IAAI,CADgB,OAAO,MAAM,KAClB,GAAG;GAChB,KAAK,QAAQ,MAAM;GACnB,OAAO,KAAK,eAAe,KAAK,QAAQ,OAAO,CAAC;EAClD;CACF,CAAC;CAED,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,iBAAiB,iBAAiB;GAChC,IAAI,CAAC,KAAK,QAAQ;IAChB,KAAK,KAAK,SAAS;IACnB,iBAAiB;KACf,IAAI,CAAC,KAAK,QAAQ,KAAK,KAAK,SAAS;IACvC,GAAG,GAAI;GACT;GACA,OACE,IAAI,YACF,gBACA,kCAAkC,yBAAyB,IAAK,KAAK,OAAO,MAAM,IAAI,GACxF,CACF;EACF,GAAG,sBAAsB;EAEzB,KAAK,GAAG,UAAU,SAAS;GACzB,kBAAkB;GAClB,OAAO,oBAAoB,SAAS,OAAO;GAC3C,IAAI,OAAO,SAAS;IAClB,QAAQ;IACR;GACF;GACA,IAAI,SAAS,KAAK,SAAS,MAAM;IAC/B,QAAQ;IACR;GACF;GACA,OACE,IAAI,YAAY,gBAAgB,2BAA2B,KAAK,IAAI,OAAO,MAAM,IAAI,GAAG,CAC1F;EACF,CAAC;EAED,KAAK,GAAG,UAAU,QAAQ;GACxB,kBAAkB;GAClB,OAAO,oBAAoB,SAAS,OAAO;GAC3C,OAAO,GAAG;EACZ,CAAC;CACH,CAAC;AACH"}
@@ -1,7 +0,0 @@
1
- import { RecorderConfig, RecorderController } from "../../lib/config.mjs";
2
-
3
- //#region src/core/recorder/index.d.ts
4
- declare function createRecorder(config: RecorderConfig): RecorderController;
5
- //#endregion
6
- export { createRecorder };
7
- //# sourceMappingURL=index.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/core/recorder/index.ts"],"mappings":";;;iBAqBgB,cAAA,CAAe,MAAA,EAAQ,cAAA,GAAiB,kBAAA"}