@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.cjs +451 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +5 -0
- package/lib/index.d.ts +5 -2
- package/lib/index.js +422 -233
- package/lib/index.js.map +1 -1
- package/package.json +29 -19
- package/README.md +0 -3
- package/lib/douyin_api.d.ts +0 -33
- package/lib/douyin_api.js +0 -100
- package/lib/douyin_api.js.map +0 -1
- package/lib/stream.d.ts +0 -23
- package/lib/stream.js +0 -109
- package/lib/stream.js.map +0 -1
- package/lib/test.d.ts +0 -1
- package/lib/test.js +0 -16
- package/lib/test.js.map +0 -1
- package/lib/utils.d.ts +0 -28
- package/lib/utils.js +0 -120
- package/lib/utils.js.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,234 +1,423 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|