@zfadhli/tokrec 0.12.2 → 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.
- package/dist/app/abort.mjs +14 -0
- package/dist/app/abort.mjs.map +1 -0
- package/dist/{lib → app}/config.d.mts +73 -71
- package/dist/app/config.d.mts.map +1 -0
- package/dist/app/config.mjs +26 -0
- package/dist/app/config.mjs.map +1 -0
- package/dist/app/events.mjs +36 -0
- package/dist/app/events.mjs.map +1 -0
- package/dist/app/job.mjs +190 -0
- package/dist/app/job.mjs.map +1 -0
- package/dist/{lib → app}/logger.mjs +1 -1
- package/dist/app/logger.mjs.map +1 -0
- package/dist/{core/monitor.mjs → app/loop.mjs} +19 -9
- package/dist/app/loop.mjs.map +1 -0
- package/dist/app/recorder.d.mts +7 -0
- package/dist/app/recorder.d.mts.map +1 -0
- package/dist/app/recorder.mjs +233 -0
- package/dist/app/recorder.mjs.map +1 -0
- package/dist/core/errors.d.mts +16 -0
- package/dist/core/errors.d.mts.map +1 -0
- package/dist/core/errors.mjs +15 -0
- package/dist/core/errors.mjs.map +1 -0
- package/dist/core/format.d.mts +12 -0
- package/dist/core/format.d.mts.map +1 -0
- package/dist/{lib/utils.mjs → core/format.mjs} +6 -41
- package/dist/core/format.mjs.map +1 -0
- package/dist/core/plan.mjs +52 -0
- package/dist/core/plan.mjs.map +1 -0
- package/dist/core/verdict.d.mts +44 -0
- package/dist/core/verdict.d.mts.map +1 -0
- package/dist/core/verdict.mjs +47 -0
- package/dist/core/verdict.mjs.map +1 -0
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +5 -2
- package/dist/{core/api/client.mjs → io/http.mjs} +38 -10
- package/dist/io/http.mjs.map +1 -0
- package/dist/io/media/convert.mjs +60 -0
- package/dist/io/media/convert.mjs.map +1 -0
- package/dist/{core/recorder → io/media}/download.mjs +8 -7
- package/dist/io/media/download.mjs.map +1 -0
- package/dist/{core/recorder/ffmpeg-utils.mjs → io/media/ffmpeg.mjs} +45 -5
- package/dist/io/media/ffmpeg.mjs.map +1 -0
- package/dist/{core/recorder → io/media}/normalize.mjs +1 -1
- package/dist/io/media/normalize.mjs.map +1 -0
- package/dist/io/tiktok/client.mjs +77 -0
- package/dist/io/tiktok/client.mjs.map +1 -0
- package/dist/io/tiktok/discovery/chain.mjs +117 -0
- package/dist/io/tiktok/discovery/chain.mjs.map +1 -0
- package/dist/io/tiktok/discovery/fallback.mjs +64 -0
- package/dist/io/tiktok/discovery/fallback.mjs.map +1 -0
- package/dist/io/tiktok/discovery/page.mjs +102 -0
- package/dist/io/tiktok/discovery/page.mjs.map +1 -0
- package/dist/io/tiktok/page.mjs +34 -0
- package/dist/io/tiktok/page.mjs.map +1 -0
- package/dist/io/tiktok/parse.mjs +110 -0
- package/dist/io/tiktok/parse.mjs.map +1 -0
- package/dist/io/tiktok/webcast.mjs +132 -0
- package/dist/io/tiktok/webcast.mjs.map +1 -0
- package/package.json +5 -26
- package/src/app/abort.ts +18 -0
- package/src/app/config.ts +178 -0
- package/src/app/events.ts +46 -0
- package/src/app/job.ts +228 -0
- package/src/app/logger.ts +133 -0
- package/src/app/loop.ts +84 -0
- package/src/app/recorder.ts +288 -0
- package/src/core/errors.ts +24 -0
- package/src/core/format.ts +50 -0
- package/src/core/log.ts +13 -0
- package/src/core/plan.ts +49 -0
- package/src/core/verdict.ts +79 -0
- package/src/index.ts +38 -0
- package/src/io/http.ts +167 -0
- package/src/io/media/convert.ts +69 -0
- package/src/io/media/download.ts +120 -0
- package/src/io/media/ffmpeg.ts +201 -0
- package/src/io/media/normalize.ts +64 -0
- package/src/io/tiktok/client.ts +114 -0
- package/src/io/tiktok/discovery/chain.ts +146 -0
- package/src/io/tiktok/discovery/fallback.ts +88 -0
- package/src/io/tiktok/discovery/page.ts +142 -0
- package/src/io/tiktok/page.ts +42 -0
- package/src/io/tiktok/parse.ts +171 -0
- package/src/io/tiktok/webcast.ts +162 -0
- package/README.md +0 -430
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -215
- package/dist/bin.mjs.map +0 -1
- package/dist/core/api/client.mjs.map +0 -1
- package/dist/core/api/rate-limiter.mjs +0 -29
- package/dist/core/api/rate-limiter.mjs.map +0 -1
- package/dist/core/api/tiktok.mjs +0 -394
- package/dist/core/api/tiktok.mjs.map +0 -1
- package/dist/core/monitor.mjs.map +0 -1
- package/dist/core/recorder/convert.mjs +0 -55
- package/dist/core/recorder/convert.mjs.map +0 -1
- package/dist/core/recorder/download.mjs.map +0 -1
- package/dist/core/recorder/ffmpeg-utils.mjs.map +0 -1
- package/dist/core/recorder/index.d.mts +0 -7
- package/dist/core/recorder/index.d.mts.map +0 -1
- package/dist/core/recorder/index.mjs +0 -205
- package/dist/core/recorder/index.mjs.map +0 -1
- package/dist/core/recorder/normalize.mjs.map +0 -1
- package/dist/core/recorder/post-processing.mjs +0 -154
- package/dist/core/recorder/post-processing.mjs.map +0 -1
- package/dist/core/recorder/recorder-events.mjs +0 -34
- package/dist/core/recorder/recorder-events.mjs.map +0 -1
- package/dist/core/recorder/recorder-state.mjs +0 -38
- package/dist/core/recorder/recorder-state.mjs.map +0 -1
- package/dist/core/recorder/stream.mjs +0 -25
- package/dist/core/recorder/stream.mjs.map +0 -1
- package/dist/lib/browser-cookies.mjs +0 -113
- package/dist/lib/browser-cookies.mjs.map +0 -1
- package/dist/lib/cli.mjs +0 -137
- package/dist/lib/cli.mjs.map +0 -1
- package/dist/lib/config.d.mts.map +0 -1
- package/dist/lib/config.mjs +0 -47
- package/dist/lib/config.mjs.map +0 -1
- package/dist/lib/logger.mjs.map +0 -1
- package/dist/lib/tmux.mjs +0 -84
- package/dist/lib/tmux.mjs.map +0 -1
- package/dist/lib/ui.mjs +0 -136
- package/dist/lib/ui.mjs.map +0 -1
- package/dist/lib/utils.mjs.map +0 -1
- package/dist/package.mjs +0 -6
- package/dist/package.mjs.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export interface SigiState {
|
|
2
|
+
LiveRoom?: {
|
|
3
|
+
liveRoomStatus?: number
|
|
4
|
+
liveRoomUserInfo?: {
|
|
5
|
+
user?: {
|
|
6
|
+
id?: string | number
|
|
7
|
+
roomId?: string | number
|
|
8
|
+
}
|
|
9
|
+
liveRoom?: {
|
|
10
|
+
status?: number // 2 = live, 4 = offline
|
|
11
|
+
title?: string
|
|
12
|
+
startTime?: number
|
|
13
|
+
streamData?: {
|
|
14
|
+
pull_data?: {
|
|
15
|
+
stream_data?: string // JSON string with FLV/HLS URLs
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
UserModule?: {
|
|
22
|
+
users?: {
|
|
23
|
+
[username: string]: {
|
|
24
|
+
id?: string // numeric user ID
|
|
25
|
+
uniqueId?: string
|
|
26
|
+
roomId?: string | { id: string }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── HTML parsing ──────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @public Exported for testing.
|
|
36
|
+
*/
|
|
37
|
+
export function extractSigiState(html: string): SigiState | null {
|
|
38
|
+
const match = html.match(/<script\s+id="SIGI_STATE"\s+type="application\/json">(.*?)<\/script>/)
|
|
39
|
+
if (!match?.[1]) return null
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(match[1]) as SigiState
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract the numeric user ID and optional room ID from TikTok's new
|
|
49
|
+
* page structure (__UNIVERSAL_DATA_FOR_REHYDRATION__).
|
|
50
|
+
*
|
|
51
|
+
* The profile page (/@user) includes "webapp.user-detail" which
|
|
52
|
+
* contains the user's numeric ID. When the user is live, the roomId
|
|
53
|
+
* is also embedded here.
|
|
54
|
+
*
|
|
55
|
+
* Returns { userId, roomId } where userId is always set when the data
|
|
56
|
+
* is found, and roomId is set only when the user is currently live.
|
|
57
|
+
*
|
|
58
|
+
* @public Exported for testing and fixture capture.
|
|
59
|
+
*/
|
|
60
|
+
export function extractUserFromUniversalData(html: string): {
|
|
61
|
+
userId: string | null
|
|
62
|
+
roomId: string | null
|
|
63
|
+
} {
|
|
64
|
+
const match = html.match(
|
|
65
|
+
/<script\s+id="__UNIVERSAL_DATA_FOR_REHYDRATION__"\s+type="application\/json">(.*?)<\/script>/,
|
|
66
|
+
)
|
|
67
|
+
if (!match?.[1]) return { userId: null, roomId: null }
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.parse(match[1])
|
|
70
|
+
const scope = (data as Record<string, unknown>).__DEFAULT_SCOPE__ ?? data
|
|
71
|
+
const userDetail = scope?.["webapp.user-detail"]
|
|
72
|
+
const user = userDetail?.userInfo?.user
|
|
73
|
+
const userId = user?.id ? String(user.id) : null
|
|
74
|
+
const roomId = user?.roomId ? String(user.roomId) : null
|
|
75
|
+
return { userId, roomId }
|
|
76
|
+
} catch {
|
|
77
|
+
return { userId: null, roomId: null }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Stream URL extraction ─────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fast-path: extract stream URL from the known SIGI_STATE location.
|
|
85
|
+
* This covers ~95% of cases without needing a full recursive search.
|
|
86
|
+
*
|
|
87
|
+
* Prefers FLV (lower latency) over HLS/other protocols.
|
|
88
|
+
*/
|
|
89
|
+
export function extractStreamUrlFast(
|
|
90
|
+
liveRoom: NonNullable<NonNullable<SigiState["LiveRoom"]>["liveRoomUserInfo"]>["liveRoom"],
|
|
91
|
+
): string | null {
|
|
92
|
+
try {
|
|
93
|
+
const raw = liveRoom?.streamData?.pull_data?.stream_data
|
|
94
|
+
if (!raw) return null
|
|
95
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
96
|
+
const data = (parsed as Record<string, unknown>)?.data as Record<string, unknown> | undefined
|
|
97
|
+
if (!data) return null
|
|
98
|
+
|
|
99
|
+
// Try all known quality keys, preferring higher quality.
|
|
100
|
+
// FLV is preferred (lower latency); HLS is the fallback.
|
|
101
|
+
const qualities = ["origin", "fhd", "uhd", "hd", "sd", "ld"]
|
|
102
|
+
for (const q of qualities) {
|
|
103
|
+
const entry = data[q] as { main?: { flv?: string; hls?: string } } | undefined
|
|
104
|
+
// Prefer FLV over HLS when both are available
|
|
105
|
+
const url = entry?.main?.flv ?? entry?.main?.hls
|
|
106
|
+
if (url) return url
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
} catch {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Brute-force recursive search for a stream URL anywhere in a JSON value.
|
|
116
|
+
* Survives TikTok changing key names or nesting structure.
|
|
117
|
+
*
|
|
118
|
+
* Prefers FLV over HLS when both are present in the response.
|
|
119
|
+
*
|
|
120
|
+
* Inspired by PR #430 (Michele0303/tiktok-live-recorder).
|
|
121
|
+
*/
|
|
122
|
+
export function findStreamUrlRecursively(obj: unknown): string | null {
|
|
123
|
+
// Collect all candidate URLs with their preference
|
|
124
|
+
const flvUrls: string[] = []
|
|
125
|
+
const m3u8Urls: string[] = []
|
|
126
|
+
|
|
127
|
+
function walk(value: unknown): void {
|
|
128
|
+
if (typeof value === "string") {
|
|
129
|
+
// Direct URL match
|
|
130
|
+
if (
|
|
131
|
+
(value.startsWith("http://") || value.startsWith("https://")) &&
|
|
132
|
+
(value.includes(".flv") || value.includes(".m3u8"))
|
|
133
|
+
) {
|
|
134
|
+
if (value.includes(".flv")) {
|
|
135
|
+
flvUrls.push(value)
|
|
136
|
+
} else {
|
|
137
|
+
m3u8Urls.push(value)
|
|
138
|
+
}
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
// JSON-encoded string — parse and recurse
|
|
142
|
+
if (value.startsWith("{") || value.startsWith("[")) {
|
|
143
|
+
try {
|
|
144
|
+
walk(JSON.parse(value))
|
|
145
|
+
} catch {
|
|
146
|
+
// skip
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (value && typeof value === "object") {
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (const item of value) {
|
|
155
|
+
walk(item)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
for (const val of Object.values(value as Record<string, unknown>)) {
|
|
159
|
+
walk(val)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
walk(obj)
|
|
166
|
+
|
|
167
|
+
// Prefer FLV (lower latency) over HLS
|
|
168
|
+
if (flvUrls.length > 0) return flvUrls[0] as string
|
|
169
|
+
if (m3u8Urls.length > 0) return m3u8Urls[0] as string
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { RoomRef, StreamRef } from "../../core/verdict"
|
|
2
|
+
import type { HttpClient } from "../http"
|
|
3
|
+
import { findStreamUrlRecursively } from "./parse"
|
|
4
|
+
|
|
5
|
+
const WEBCAST_BASE = "https://webcast.tiktok.com"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Discriminated room/info result: refusals (notably status_code 4003110)
|
|
9
|
+
* and transport failures surface as `{ ok: false }` instead of null.
|
|
10
|
+
* `statusCode: 0` means the request threw before a status was read.
|
|
11
|
+
*/
|
|
12
|
+
export type RoomApiResult<T> =
|
|
13
|
+
| { ok: true; value: T | null }
|
|
14
|
+
| { ok: false; statusCode: number; body: string }
|
|
15
|
+
|
|
16
|
+
/** Authoritative room/info facts: the room plus its stream URL when present. */
|
|
17
|
+
export interface RoomFacts {
|
|
18
|
+
readonly room: RoomRef
|
|
19
|
+
readonly stream: StreamRef | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What discovery sources consume. `createWebcastApi` is the only implementation. */
|
|
23
|
+
export interface WebcastReader {
|
|
24
|
+
fetchFromRoomApi: <T>(
|
|
25
|
+
roomId: string,
|
|
26
|
+
extract: (data: Record<string, unknown>) => T | null,
|
|
27
|
+
) => Promise<RoomApiResult<T>>
|
|
28
|
+
fetchCheckAlive: (roomId: string) => Promise<boolean>
|
|
29
|
+
fetchStreamUrlFromApi: (roomId: string) => Promise<RoomApiResult<string>>
|
|
30
|
+
/** room/info as domain facts; 4003110 comes back as `{ ok: false }`. */
|
|
31
|
+
roomInfo: (roomId: string) => Promise<RoomApiResult<RoomFacts>>
|
|
32
|
+
/** Resolve a 4003110 refusal: alive → restricted candidate, else offline. */
|
|
33
|
+
refusedRoom: (roomId: string, title: string | null) => Promise<RoomRef>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createWebcastApi(
|
|
37
|
+
http: HttpClient,
|
|
38
|
+
debug: (msg: string) => void,
|
|
39
|
+
signal?: AbortSignal,
|
|
40
|
+
): WebcastReader {
|
|
41
|
+
/**
|
|
42
|
+
* Shared boilerplate for Webcast room/info API calls.
|
|
43
|
+
* Handles URL construction, HTTP GET, JSON parsing, and error handling.
|
|
44
|
+
* Room-gate refusals (status_code 4003110) are reported to the caller
|
|
45
|
+
* instead of being collapsed into null; callers map failures.
|
|
46
|
+
* The caller provides an extractor for the response data.
|
|
47
|
+
*/
|
|
48
|
+
async function fetchFromRoomApi<T>(
|
|
49
|
+
roomId: string,
|
|
50
|
+
extract: (data: Record<string, unknown>) => T | null,
|
|
51
|
+
): Promise<RoomApiResult<T>> {
|
|
52
|
+
try {
|
|
53
|
+
const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`
|
|
54
|
+
const res = await http.get(url, signal)
|
|
55
|
+
debug(`fetchFromRoomApi: roomId=${roomId} status=${res.status}, ok=${res.ok}`)
|
|
56
|
+
if (!res.ok) return { ok: false, statusCode: res.status, body: "" }
|
|
57
|
+
|
|
58
|
+
const text = await res.text()
|
|
59
|
+
debug(`fetchFromRoomApi: roomId=${roomId} returned ${text.length} bytes`)
|
|
60
|
+
if (text.length === 0) return { ok: true, value: null }
|
|
61
|
+
|
|
62
|
+
const data = JSON.parse(text) as Record<string, unknown>
|
|
63
|
+
|
|
64
|
+
// status_code 4003110 = age-protected/restricted room: report it
|
|
65
|
+
if ((data as { status_code?: unknown }).status_code === 4003110) {
|
|
66
|
+
return { ok: false, statusCode: 4003110, body: text }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: true, value: extract(data) }
|
|
70
|
+
} catch {
|
|
71
|
+
// statusCode 0 = transport/parse failure before a status was available.
|
|
72
|
+
return { ok: false, statusCode: 0, body: "" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a room is currently alive using the lightweight
|
|
78
|
+
* /webcast/room/check_alive/ endpoint.
|
|
79
|
+
*
|
|
80
|
+
* This is faster and cheaper than fetching the full room/info response.
|
|
81
|
+
* Returns `true` if the room is live, `false` otherwise (including errors).
|
|
82
|
+
*
|
|
83
|
+
* Endpoint: /webcast/room/check_alive/?aid=1988®ion=CH&room_ids={id}&user_is_login=true
|
|
84
|
+
* Response: { "data": [{ "alive": true/false, "room_id": "..." }] }
|
|
85
|
+
*/
|
|
86
|
+
async function fetchCheckAlive(roomId: string): Promise<boolean> {
|
|
87
|
+
try {
|
|
88
|
+
const url = `${WEBCAST_BASE}/webcast/room/check_alive/?aid=1988®ion=CH&room_ids=${roomId}&user_is_login=true`
|
|
89
|
+
const res = await http.get(url, signal)
|
|
90
|
+
debug(`fetchCheckAlive: roomId=${roomId} status=${res.status}, ok=${res.ok}`)
|
|
91
|
+
if (!res.ok) return false
|
|
92
|
+
|
|
93
|
+
const text = await res.text()
|
|
94
|
+
debug(`fetchCheckAlive: roomId=${roomId} returned ${text.length} bytes`)
|
|
95
|
+
if (text.length === 0) return false
|
|
96
|
+
|
|
97
|
+
const data = JSON.parse(text) as Record<string, unknown>
|
|
98
|
+
const items = data?.data as Array<Record<string, unknown>> | undefined
|
|
99
|
+
const alive = items?.[0]?.alive === true
|
|
100
|
+
debug(`fetchCheckAlive: roomId=${roomId} alive=${alive}`)
|
|
101
|
+
return alive
|
|
102
|
+
} catch (err) {
|
|
103
|
+
debug(
|
|
104
|
+
`fetchCheckAlive: roomId=${roomId} error — ${err instanceof Error ? err.message : String(err)}`,
|
|
105
|
+
)
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve a 4003110 room-gate refusal via check_alive: the room is live
|
|
112
|
+
* (restricted) when alive, offline otherwise. Single home for this mapping.
|
|
113
|
+
*/
|
|
114
|
+
async function refusedRoom(roomId: string, title: string | null): Promise<RoomRef> {
|
|
115
|
+
debug(`refusedRoom: room/info refused roomId=${roomId} (status_code 4003110)`)
|
|
116
|
+
const live = await fetchCheckAlive(roomId)
|
|
117
|
+
return { id: roomId, title, live }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fetch room/info as domain facts. Requires a known roomId. A 4003110
|
|
122
|
+
* refusal stays `{ ok: false, statusCode: 4003110 }` for the caller to map
|
|
123
|
+
* via `refusedRoom`.
|
|
124
|
+
*/
|
|
125
|
+
async function roomInfo(roomId: string): Promise<RoomApiResult<RoomFacts>> {
|
|
126
|
+
const res = await fetchFromRoomApi<Record<string, unknown>>(
|
|
127
|
+
roomId,
|
|
128
|
+
(data) => (data as { data?: Record<string, unknown> }).data ?? null,
|
|
129
|
+
)
|
|
130
|
+
if (!res.ok) return res
|
|
131
|
+
const roomData = res.value
|
|
132
|
+
if (!roomData) return { ok: true, value: null }
|
|
133
|
+
|
|
134
|
+
const title = (roomData.title as string | undefined) ?? null
|
|
135
|
+
const streamUrl = findStreamUrlRecursively(roomData)
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
value: {
|
|
139
|
+
room: { id: roomId, title, live: (roomData.status ?? 4) === 2 },
|
|
140
|
+
stream: streamUrl ? { url: streamUrl } : null,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fetch stream URL from the Webcast API.
|
|
147
|
+
* Covers cases where SIGI_STATE doesn't include stream data.
|
|
148
|
+
* Refusals (4003110) and transport failures are reported so callers
|
|
149
|
+
* can distinguish restricted rooms from missing URLs.
|
|
150
|
+
*/
|
|
151
|
+
async function fetchStreamUrlFromApi(roomId: string): Promise<RoomApiResult<string>> {
|
|
152
|
+
return fetchFromRoomApi(roomId, (data) => findStreamUrlRecursively(data))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
fetchFromRoomApi,
|
|
157
|
+
fetchCheckAlive,
|
|
158
|
+
fetchStreamUrlFromApi,
|
|
159
|
+
roomInfo,
|
|
160
|
+
refusedRoom,
|
|
161
|
+
}
|
|
162
|
+
}
|