@autorecord/douyin-recorder 1.0.8 → 1.2.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/lib/index.js CHANGED
@@ -1,234 +1,423 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.provider = void 0;
16
- const path_1 = __importDefault(require("path"));
17
- const mitt_1 = __importDefault(require("mitt"));
18
- const manager_1 = require("@autorecord/manager");
19
- const stream_1 = require("./stream");
20
- const utils_1 = require("./utils");
21
- function createRecorder(opts) {
22
- var _a, _b;
23
- // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
24
- // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。
25
- const recorder = Object.assign(Object.assign(Object.assign({ id: (_a = opts.id) !== null && _a !== void 0 ? _a : (0, manager_1.genRecorderUUID)(), extra: (_b = opts.extra) !== null && _b !== void 0 ? _b : {} }, (0, mitt_1.default)()), opts), { availableStreams: [], availableSources: [], state: 'idle', getChannelURL() {
26
- return `https://live.douyin.com/${this.channelId}`;
27
- }, checkLiveStatusAndRecord: (0, utils_1.singleton)(checkLiveStatusAndRecord), toJSON() {
28
- return (0, manager_1.defaultToJSON)(exports.provider, this);
29
- } });
30
- const recorderWithSupportUpdatedEvent = new Proxy(recorder, {
31
- set(obj, prop, value) {
32
- Reflect.set(obj, prop, value);
33
- if (typeof prop === 'string') {
34
- obj.emit('Updated', [prop]);
35
- }
36
- return true;
37
- },
38
- });
39
- return recorderWithSupportUpdatedEvent;
40
- }
41
- const ffmpegOutputOptions = ['-c', 'copy', '-movflags', 'frag_keyframe', '-min_frag_duration', '60000000'];
42
- const checkLiveStatusAndRecord = function ({ getSavePath }) {
43
- return __awaiter(this, void 0, void 0, function* () {
44
- if (this.recordHandle != null)
45
- return this.recordHandle;
46
- const { living, owner, title, roomId } = yield (0, stream_1.getInfo)(this.channelId);
47
- if (!living)
48
- return null;
49
- this.state = 'recording';
50
- let res;
51
- // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
52
- try {
53
- res = yield (0, stream_1.getStream)({
54
- channelId: this.channelId,
55
- quality: this.quality,
56
- streamPriorities: this.streamPriorities,
57
- sourcePriorities: this.sourcePriorities,
58
- });
59
- }
60
- catch (err) {
61
- this.state = 'idle';
62
- throw err;
63
- }
64
- const { currentStream: stream, sources: availableSources, streams: availableStreams } = res;
65
- this.availableStreams = availableStreams.map((s) => s.desc);
66
- this.availableSources = availableSources.map((s) => s.name);
67
- this.usedStream = stream.name;
68
- this.usedSource = stream.source;
69
- // TODO: emit update event
70
- const savePath = getSavePath({ owner, title });
71
- // TODO: 之后可能要结合 disableRecordMeta 之类的来确认是否要创建文件。
72
- const extraDataSavePath = (0, utils_1.replaceExtName)(savePath, '.json');
73
- // TODO: 这个 ensure 或许应该放在 createRecordExtraDataController 里实现?
74
- (0, utils_1.ensureFolderExist)(extraDataSavePath);
75
- const extraDataController = (0, manager_1.createRecordExtraDataController)(extraDataSavePath);
76
- extraDataController.setMeta({ title });
77
- // TODO: 弹幕录制
78
- const recordSavePath = savePath;
79
- (0, utils_1.ensureFolderExist)(recordSavePath);
80
- const onEnd = (...args) => {
81
- var _a;
82
- this.emit('DebugLog', {
83
- type: 'common',
84
- text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
85
- });
86
- (_a = this.recordHandle) === null || _a === void 0 ? void 0 : _a.stop();
87
- };
88
- const isInvalidStream = createInvalidStreamChecker();
89
- const timeoutChecker = createTimeoutChecker(() => onEnd('ffmpeg timeout'), 10e3);
90
- const command = (0, manager_1.createFFMPEGBuilder)(stream.url)
91
- .inputOptions('-user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
92
- /**
93
- * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。
94
- * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。
95
- *
96
- * Refs:
97
- * https://github.com/Sunoo/homebridge-camera-ffmpeg/issues/462#issuecomment-617723949
98
- * https://stackoverflow.com/a/49273163/21858805
99
- */
100
- '-probesize', (64 * 1024).toString())
101
- .outputOptions(ffmpegOutputOptions)
102
- .output(recordSavePath)
103
- .on('error', onEnd)
104
- .on('end', () => onEnd('end'))
105
- .on('stderr', (stderrLine) => {
106
- (0, utils_1.assertStringType)(stderrLine);
107
- this.emit('DebugLog', { type: 'ffmpeg', text: stderrLine });
108
- if (isInvalidStream(stderrLine)) {
109
- onEnd('invalid stream');
110
- }
111
- })
112
- .on('stderr', timeoutChecker.update);
113
- const ffmpegArgs = command._getArguments();
114
- extraDataController.setMeta({
115
- recordStartTimestamp: Date.now(),
116
- ffmpegArgs,
117
- });
118
- command.run();
119
- // TODO: 需要一个机制防止空录制,比如检查文件的大小变化、ffmpeg 的输出、直播状态等
120
- const stop = (0, utils_1.singleton)(() => __awaiter(this, void 0, void 0, function* () {
121
- if (!this.recordHandle)
122
- return;
123
- this.state = 'stopping-record';
124
- // TODO: emit update event
125
- timeoutChecker.stop();
126
- // 如果给 SIGKILL 信号会非正常退出,SIGINT 可以被 ffmpeg 正常处理。
127
- // TODO: fluent-ffmpeg 好像没处理好这个 SIGINT 导致的退出信息,会抛一个错。
128
- command.kill('SIGINT');
129
- // TODO: 这里可能会有内存泄露,因为事件还没清,之后再检查下看看。
130
- // client?.close()
131
- extraDataController.setMeta({ recordStopTimestamp: Date.now() });
132
- extraDataController.flush();
133
- this.usedStream = undefined;
134
- this.usedSource = undefined;
135
- // TODO: other codes
136
- // TODO: emit update event
137
- this.emit('RecordStop', this.recordHandle);
138
- this.recordHandle = undefined;
139
- this.state = 'idle';
140
- }));
141
- this.recordHandle = {
142
- id: (0, manager_1.genRecordUUID)(),
143
- stream: stream.name,
144
- source: stream.source,
145
- url: stream.url,
146
- ffmpegArgs,
147
- savePath: recordSavePath,
148
- stop,
149
- };
150
- this.emit('RecordStart', this.recordHandle);
151
- return this.recordHandle;
152
- });
153
- };
154
- function createTimeoutChecker(onTimeout, time) {
155
- let timer = null;
156
- let stopped = false;
157
- const update = () => {
158
- if (stopped)
159
- return;
160
- if (timer != null)
161
- clearTimeout(timer);
162
- timer = setTimeout(() => {
163
- timer = null;
164
- onTimeout();
165
- }, time);
166
- };
167
- update();
168
- return {
169
- update,
170
- stop() {
171
- stopped = true;
172
- if (timer != null)
173
- clearTimeout(timer);
174
- timer = null;
175
- },
176
- };
177
- }
178
- function createInvalidStreamChecker() {
179
- let prevFrame = 0;
180
- let frameUnchangedCount = 0;
181
- return (ffmpegLogLine) => {
182
- const streamInfo = ffmpegLogLine.match(/frame=\s*(\d+) fps=.*? q=.*? size=\s*(\d+)kB time=.*? bitrate=.*? speed=.*?/);
183
- if (streamInfo != null) {
184
- const [, frameText] = streamInfo;
185
- const frame = Number(frameText);
186
- if (frame === prevFrame) {
187
- if (++frameUnchangedCount >= 10) {
188
- return true;
189
- }
190
- }
191
- else {
192
- prevFrame = frame;
193
- frameUnchangedCount = 0;
194
- }
195
- return false;
196
- }
197
- if (ffmpegLogLine.includes('HTTP error 404 Not Found')) {
198
- return true;
199
- }
200
- return false;
201
- };
202
- }
203
- exports.provider = {
204
- id: 'DouYin',
205
- name: '抖音',
206
- siteURL: 'https://live.douyin.com/',
207
- matchURL(channelURL) {
208
- // TODO: 暂时不支持 v.douyin.com
209
- return /https?:\/\/live\.douyin\.com\//.test(channelURL);
210
- },
211
- resolveChannelInfoFromURL(channelURL) {
212
- return __awaiter(this, void 0, void 0, function* () {
213
- if (!this.matchURL(channelURL))
214
- return null;
215
- const id = path_1.default.basename(new URL(channelURL).pathname);
216
- const info = yield (0, stream_1.getInfo)(id);
217
- return {
218
- id: info.roomId,
219
- title: info.title,
220
- owner: info.owner,
221
- };
222
- });
223
- },
224
- createRecorder(opts) {
225
- return createRecorder(Object.assign({ providerId: exports.provider.id }, opts));
226
- },
227
- fromJSON(recorder) {
228
- return (0, manager_1.defaultFromJSON)(this, recorder);
229
- },
230
- setFFMPEGOutputArgs(args) {
231
- ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args);
232
- },
233
- };
1
+ // src/index.ts
2
+ import path2 from "path";
3
+ import mitt from "mitt";
4
+ import {
5
+ createFFMPEGBuilder,
6
+ defaultFromJSON,
7
+ defaultToJSON,
8
+ genRecorderUUID,
9
+ genRecordUUID,
10
+ createRecordExtraDataController
11
+ } from "@autorecord/manager";
12
+
13
+ // src/stream.ts
14
+ import { Qualities } from "@autorecord/manager";
15
+
16
+ // src/douyin_api.ts
17
+ import axios from "axios";
18
+ import { wrapper } from "axios-cookiejar-support";
19
+ import { CookieJar } from "tough-cookie";
20
+
21
+ // src/utils.ts
22
+ import fs from "fs";
23
+ import path from "path";
24
+ import * as R from "ramda";
25
+ function singleton(fn) {
26
+ let latestPromise = null;
27
+ return function(...args) {
28
+ if (latestPromise) return latestPromise;
29
+ const promise = fn.apply(this, args).finally(() => {
30
+ if (promise === latestPromise) {
31
+ latestPromise = null;
32
+ }
33
+ });
34
+ latestPromise = promise;
35
+ return promise;
36
+ };
37
+ }
38
+ function getValuesFromArrayLikeFlexSpaceBetween(array, columnCount) {
39
+ if (columnCount < 1) return [];
40
+ if (columnCount === 1) return [array[0]];
41
+ const spacingCount = columnCount - 1;
42
+ const spacingLength = array.length / spacingCount;
43
+ const columns = R.range(1, columnCount + 1);
44
+ const columnValues = columns.map((column, idx, columns2) => {
45
+ if (idx === 0) {
46
+ return array[0];
47
+ } else if (idx === columns2.length - 1) {
48
+ return array[array.length - 1];
49
+ }
50
+ const beforeSpacingCount = column - 1;
51
+ const colPos = beforeSpacingCount * spacingLength;
52
+ return array[Math.floor(colPos)];
53
+ });
54
+ return columnValues;
55
+ }
56
+ function ensureFolderExist(fileOrFolderPath) {
57
+ const folder = path.dirname(fileOrFolderPath);
58
+ if (!fs.existsSync(folder)) {
59
+ fs.mkdirSync(folder, { recursive: true });
60
+ }
61
+ }
62
+ function assert(assertion, msg) {
63
+ if (!assertion) {
64
+ throw new Error(msg);
65
+ }
66
+ }
67
+ function assertStringType(data, msg) {
68
+ assert(typeof data === "string", msg);
69
+ }
70
+ function replaceExtName(filePath, newExtName) {
71
+ return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName);
72
+ }
73
+
74
+ // src/douyin_api.ts
75
+ var jar = new CookieJar();
76
+ var requester = wrapper(
77
+ axios.create({
78
+ timeout: 1e4,
79
+ jar,
80
+ // axios 会自动读取环境变量中的 http_proxy https_proxy 并应用,这会让请求发往代理的 host。
81
+ // 于是 set-cookie 的 domain 与请求的 host 无法匹配上,tough-cookie 在检查时会丢弃它,导致 cookie 丢失。
82
+ // 所以这里需要主动禁用代理功能。
83
+ proxy: false
84
+ })
85
+ );
86
+ async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
87
+ await requester.get("https://live.douyin.com/");
88
+ const res = await requester.get("https://live.douyin.com/webcast/room/web/enter/", {
89
+ params: {
90
+ aid: 6383,
91
+ live_id: 1,
92
+ device_platform: "web",
93
+ language: "zh-CN",
94
+ enter_from: "web_live",
95
+ cookie_enabled: "true",
96
+ screen_width: 1920,
97
+ screen_height: 1080,
98
+ browser_language: "zh-CN",
99
+ browser_platform: "MacIntel",
100
+ browser_name: "Chrome",
101
+ browser_version: "108.0.0.0",
102
+ web_rid: webRoomId,
103
+ // enter_source:,
104
+ "Room-Enter-User-Login-Ab": 0,
105
+ is_need_double_stream: "false"
106
+ }
107
+ });
108
+ if (res.data.status_code === 10037 && retryOnSpecialCode) {
109
+ await requester.get("https://live.douyin.com/favicon.ico");
110
+ return getRoomInfo(webRoomId, false);
111
+ }
112
+ assert(
113
+ res.data.status_code === 0,
114
+ `Unexpected resp, code ${res.data.status_code}, msg ${res.data.data}, id ${webRoomId}`
115
+ );
116
+ const data = res.data.data;
117
+ const room = data.data[0];
118
+ if (room?.stream_url == null) {
119
+ return {
120
+ living: false,
121
+ roomId: webRoomId,
122
+ owner: data.user.nickname,
123
+ title: room?.title ?? data.user.nickname,
124
+ streams: [],
125
+ sources: []
126
+ };
127
+ }
128
+ const {
129
+ options: { qualities },
130
+ stream_data
131
+ } = room.stream_url.live_core_sdk_data.pull_data;
132
+ const streamData = JSON.parse(stream_data).data;
133
+ const streams = qualities.map((info) => ({
134
+ desc: info.name,
135
+ key: info.sdk_key,
136
+ bitRate: info.v_bit_rate
137
+ }));
138
+ const sources = [
139
+ {
140
+ name: "\u81EA\u52A8\u5207\u6362\u7EBF\u8DEF",
141
+ streamMap: streamData
142
+ }
143
+ ];
144
+ return {
145
+ living: data.room_status === 0,
146
+ // 接口里不会再返回 web room id,只能直接用入参原路返回了。
147
+ roomId: webRoomId,
148
+ owner: data.user.nickname,
149
+ title: room.title,
150
+ streams,
151
+ sources
152
+ };
153
+ }
154
+
155
+ // src/stream.ts
156
+ import * as R2 from "ramda";
157
+ async function getInfo(channelId) {
158
+ const info = await getRoomInfo(channelId);
159
+ return {
160
+ living: info.living,
161
+ owner: info.owner,
162
+ title: info.title,
163
+ roomId: info.roomId
164
+ };
165
+ }
166
+ async function getStream(opts) {
167
+ const info = await getRoomInfo(opts.channelId);
168
+ if (!info.living) {
169
+ throw new Error("It must be called getStream when living");
170
+ }
171
+ let expectStream;
172
+ const streamsWithPriority = sortAndFilterStreamsByPriority(info.streams, opts.streamPriorities);
173
+ if (streamsWithPriority.length > 0) {
174
+ expectStream = streamsWithPriority[0];
175
+ } else {
176
+ const flexedStreams = getValuesFromArrayLikeFlexSpaceBetween(info.streams, Qualities.length);
177
+ expectStream = flexedStreams[Qualities.indexOf(opts.quality)];
178
+ }
179
+ let expectSource = null;
180
+ const sourcesWithPriority = sortAndFilterSourcesByPriority(info.sources, opts.sourcePriorities);
181
+ if (sourcesWithPriority.length > 0) {
182
+ expectSource = sourcesWithPriority[0];
183
+ } else {
184
+ expectSource = info.sources[0];
185
+ }
186
+ return {
187
+ ...info,
188
+ currentStream: {
189
+ name: expectStream.desc,
190
+ source: expectSource.name,
191
+ url: expectSource.streamMap[expectStream.key].main.flv
192
+ }
193
+ };
194
+ }
195
+ function sortAndFilterStreamsByPriority(streams, streamPriorities) {
196
+ if (streamPriorities.length === 0) return [];
197
+ return R2.sortBy(
198
+ R2.prop("priority"),
199
+ // 分配优先级属性,数字越大优先级越高
200
+ streams.map((stream) => ({
201
+ ...stream,
202
+ priority: R2.reverse(streamPriorities).indexOf(stream.desc)
203
+ })).filter(({ priority }) => priority !== -1)
204
+ );
205
+ }
206
+ function sortAndFilterSourcesByPriority(sources, sourcePriorities) {
207
+ if (sourcePriorities.length === 0) return [];
208
+ return R2.sortBy(
209
+ R2.prop("priority"),
210
+ // 分配优先级属性,数字越大优先级越高
211
+ sources.map((source) => ({
212
+ ...source,
213
+ priority: R2.reverse(sourcePriorities).indexOf(source.name)
214
+ })).filter(({ priority }) => priority !== -1)
215
+ );
216
+ }
217
+
218
+ // src/index.ts
219
+ function createRecorder(opts) {
220
+ const recorder = {
221
+ id: opts.id ?? genRecorderUUID(),
222
+ extra: opts.extra ?? {},
223
+ ...mitt(),
224
+ ...opts,
225
+ availableStreams: [],
226
+ availableSources: [],
227
+ state: "idle",
228
+ getChannelURL() {
229
+ return `https://live.douyin.com/${this.channelId}`;
230
+ },
231
+ checkLiveStatusAndRecord: singleton(checkLiveStatusAndRecord),
232
+ toJSON() {
233
+ return defaultToJSON(provider, this);
234
+ }
235
+ };
236
+ const recorderWithSupportUpdatedEvent = new Proxy(recorder, {
237
+ set(obj, prop2, value) {
238
+ Reflect.set(obj, prop2, value);
239
+ if (typeof prop2 === "string") {
240
+ obj.emit("Updated", [prop2]);
241
+ }
242
+ return true;
243
+ }
244
+ });
245
+ return recorderWithSupportUpdatedEvent;
246
+ }
247
+ var ffmpegOutputOptions = ["-c", "copy", "-movflags", "frag_keyframe", "-min_frag_duration", "60000000"];
248
+ var checkLiveStatusAndRecord = async function({ getSavePath }) {
249
+ if (this.recordHandle != null) return this.recordHandle;
250
+ const { living, owner, title, roomId } = await getInfo(this.channelId);
251
+ if (!living) return null;
252
+ this.state = "recording";
253
+ let res;
254
+ try {
255
+ res = await getStream({
256
+ channelId: this.channelId,
257
+ quality: this.quality,
258
+ streamPriorities: this.streamPriorities,
259
+ sourcePriorities: this.sourcePriorities
260
+ });
261
+ } catch (err) {
262
+ this.state = "idle";
263
+ throw err;
264
+ }
265
+ const { currentStream: stream, sources: availableSources, streams: availableStreams } = res;
266
+ this.availableStreams = availableStreams.map((s) => s.desc);
267
+ this.availableSources = availableSources.map((s) => s.name);
268
+ this.usedStream = stream.name;
269
+ this.usedSource = stream.source;
270
+ const savePath = getSavePath({ owner, title });
271
+ const extraDataSavePath = replaceExtName(savePath, ".json");
272
+ const recordSavePath = savePath;
273
+ try {
274
+ ensureFolderExist(extraDataSavePath);
275
+ ensureFolderExist(recordSavePath);
276
+ } catch (err) {
277
+ this.state = "idle";
278
+ throw err;
279
+ }
280
+ const extraDataController = createRecordExtraDataController(extraDataSavePath);
281
+ extraDataController.setMeta({ title });
282
+ let isEnded = false;
283
+ const onEnd = (...args) => {
284
+ if (isEnded) return;
285
+ isEnded = true;
286
+ this.emit("DebugLog", {
287
+ type: "common",
288
+ text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => v instanceof Error ? v.stack : v)}`
289
+ });
290
+ const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
291
+ this.recordHandle?.stop(reason);
292
+ };
293
+ const isInvalidStream = createInvalidStreamChecker();
294
+ const timeoutChecker = createTimeoutChecker(() => onEnd("ffmpeg timeout"), 1e4);
295
+ const command = createFFMPEGBuilder(stream.url).inputOptions(
296
+ "-user_agent",
297
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
298
+ /**
299
+ * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。
300
+ * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。
301
+ *
302
+ * Refs:
303
+ * https://github.com/Sunoo/homebridge-camera-ffmpeg/issues/462#issuecomment-617723949
304
+ * https://stackoverflow.com/a/49273163/21858805
305
+ */
306
+ "-probesize",
307
+ (64 * 1024).toString()
308
+ ).outputOptions(ffmpegOutputOptions).output(recordSavePath).on("error", onEnd).on("end", () => onEnd("finished")).on("stderr", (stderrLine) => {
309
+ assertStringType(stderrLine);
310
+ this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
311
+ if (isInvalidStream(stderrLine)) {
312
+ onEnd("invalid stream");
313
+ }
314
+ }).on("stderr", timeoutChecker.update);
315
+ const ffmpegArgs = command._getArguments();
316
+ extraDataController.setMeta({
317
+ recordStartTimestamp: Date.now(),
318
+ ffmpegArgs
319
+ });
320
+ command.run();
321
+ const stop = singleton(async (reason) => {
322
+ if (!this.recordHandle) return;
323
+ this.state = "stopping-record";
324
+ timeoutChecker.stop();
325
+ command.kill("SIGINT");
326
+ extraDataController.setMeta({ recordStopTimestamp: Date.now() });
327
+ extraDataController.flush();
328
+ this.usedStream = void 0;
329
+ this.usedSource = void 0;
330
+ this.emit("RecordStop", { recordHandle: this.recordHandle, reason });
331
+ this.recordHandle = void 0;
332
+ this.state = "idle";
333
+ });
334
+ this.recordHandle = {
335
+ id: genRecordUUID(),
336
+ stream: stream.name,
337
+ source: stream.source,
338
+ url: stream.url,
339
+ ffmpegArgs,
340
+ savePath: recordSavePath,
341
+ stop
342
+ };
343
+ this.emit("RecordStart", this.recordHandle);
344
+ return this.recordHandle;
345
+ };
346
+ function createTimeoutChecker(onTimeout, time) {
347
+ let timer = null;
348
+ let stopped = false;
349
+ const update = () => {
350
+ if (stopped) return;
351
+ if (timer != null) clearTimeout(timer);
352
+ timer = setTimeout(() => {
353
+ timer = null;
354
+ onTimeout();
355
+ }, time);
356
+ };
357
+ update();
358
+ return {
359
+ update,
360
+ stop() {
361
+ stopped = true;
362
+ if (timer != null) clearTimeout(timer);
363
+ timer = null;
364
+ }
365
+ };
366
+ }
367
+ function createInvalidStreamChecker() {
368
+ let prevFrame = 0;
369
+ let frameUnchangedCount = 0;
370
+ return (ffmpegLogLine) => {
371
+ const streamInfo = ffmpegLogLine.match(
372
+ /frame=\s*(\d+) fps=.*? q=.*? size=\s*(\d+)kB time=.*? bitrate=.*? speed=.*?/
373
+ );
374
+ if (streamInfo != null) {
375
+ const [, frameText] = streamInfo;
376
+ const frame = Number(frameText);
377
+ if (frame === prevFrame) {
378
+ if (++frameUnchangedCount >= 10) {
379
+ return true;
380
+ }
381
+ } else {
382
+ prevFrame = frame;
383
+ frameUnchangedCount = 0;
384
+ }
385
+ return false;
386
+ }
387
+ if (ffmpegLogLine.includes("HTTP error 404 Not Found")) {
388
+ return true;
389
+ }
390
+ return false;
391
+ };
392
+ }
393
+ var provider = {
394
+ id: "DouYin",
395
+ name: "\u6296\u97F3",
396
+ siteURL: "https://live.douyin.com/",
397
+ matchURL(channelURL) {
398
+ return /https?:\/\/live\.douyin\.com\//.test(channelURL);
399
+ },
400
+ async resolveChannelInfoFromURL(channelURL) {
401
+ if (!this.matchURL(channelURL)) return null;
402
+ const id = path2.basename(new URL(channelURL).pathname);
403
+ const info = await getInfo(id);
404
+ return {
405
+ id: info.roomId,
406
+ title: info.title,
407
+ owner: info.owner
408
+ };
409
+ },
410
+ createRecorder(opts) {
411
+ return createRecorder({ providerId: provider.id, ...opts });
412
+ },
413
+ fromJSON(recorder) {
414
+ return defaultFromJSON(this, recorder);
415
+ },
416
+ setFFMPEGOutputArgs(args) {
417
+ ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args);
418
+ }
419
+ };
420
+ export {
421
+ provider
422
+ };
234
423
  //# sourceMappingURL=index.js.map