@autorecord/douyin-recorder 1.2.0 → 1.3.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 CHANGED
@@ -110,6 +110,20 @@ var requester = (0, import_axios_cookiejar_support.wrapper)(
110
110
  proxy: false
111
111
  })
112
112
  );
113
+ var authCookie;
114
+ function setAuthCookie(cookie) {
115
+ authCookie = cookie;
116
+ }
117
+ function getAuthCookie() {
118
+ return authCookie;
119
+ }
120
+ requester.interceptors.request.use((config) => {
121
+ if (authCookie) {
122
+ const existing = config.headers.Cookie;
123
+ config.headers.Cookie = existing ? `${existing}; ${authCookie}` : authCookie;
124
+ }
125
+ return config;
126
+ });
113
127
  async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
114
128
  await requester.get("https://live.douyin.com/");
115
129
  const res = await requester.get("https://live.douyin.com/webcast/room/web/enter/", {
@@ -319,9 +333,14 @@ var checkLiveStatusAndRecord = async function({ getSavePath }) {
319
333
  };
320
334
  const isInvalidStream = createInvalidStreamChecker();
321
335
  const timeoutChecker = createTimeoutChecker(() => onEnd("ffmpeg timeout"), 1e4);
336
+ const cookie = getAuthCookie();
337
+ const headersValue = cookie ? `Referer: https://live.douyin.com/\r
338
+ Cookie: ${cookie}` : "Referer: https://live.douyin.com/";
322
339
  const command = (0, import_manager2.createFFMPEGBuilder)(stream.url).inputOptions(
323
340
  "-user_agent",
324
341
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
342
+ "-headers",
343
+ headersValue,
325
344
  /**
326
345
  * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。
327
346
  * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。
@@ -442,6 +461,37 @@ var provider = {
442
461
  },
443
462
  setFFMPEGOutputArgs(args) {
444
463
  ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args);
464
+ },
465
+ authFields: [
466
+ {
467
+ key: "cookie",
468
+ label: "Cookie",
469
+ type: "textarea",
470
+ required: false,
471
+ placeholder: "sessionid_ss=xxx; ttwid=xxx; ...",
472
+ description: "\u4ECE\u6D4F\u89C8\u5668\u83B7\u53D6\u6296\u97F3\u767B\u5F55 Cookie\uFF0C\u7528\u4E8E\u83B7\u53D6\u66F4\u9AD8\u753B\u8D28\u76F4\u64AD\u6D41\u6216\u89E3\u51B3\u8BBF\u95EE\u9650\u5236"
473
+ }
474
+ ],
475
+ authFlow: {
476
+ loginURL: "https://live.douyin.com/",
477
+ checkLoginResult({ cookies }) {
478
+ const douyinCookies = cookies.filter((c) => c.domain === ".douyin.com" || c.domain === "live.douyin.com");
479
+ const hasSession = douyinCookies.some((c) => c.name === "sessionid_ss");
480
+ if (!hasSession) return { success: false };
481
+ const cookieString = douyinCookies.map((c) => `${c.name}=${c.value}`).join("; ");
482
+ return { success: true, authConfig: { cookie: cookieString } };
483
+ },
484
+ timeout: 3e5
485
+ },
486
+ setAuth(config) {
487
+ setAuthCookie(config.cookie || void 0);
488
+ },
489
+ async checkAuth() {
490
+ const cookie = getAuthCookie();
491
+ if (!cookie) return { isAuthenticated: false };
492
+ const hasSession = /sessionid_ss=/.test(cookie);
493
+ if (!hasSession) return { isAuthenticated: false, description: "Cookie \u4E2D\u7F3A\u5C11 sessionid_ss" };
494
+ return { isAuthenticated: true, description: "\u5DF2\u8BBE\u7F6E\u767B\u5F55 Cookie" };
445
495
  }
446
496
  };
447
497
  // Annotate the CommonJS export names for ESM import in node:
package/lib/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/stream.ts","../src/douyin_api.ts","../src/utils.ts"],"sourcesContent":["import path from 'path'\nimport mitt from 'mitt'\nimport {\n Recorder,\n RecorderCreateOpts,\n RecorderProvider,\n createFFMPEGBuilder,\n RecordHandle,\n defaultFromJSON,\n defaultToJSON,\n genRecorderUUID,\n genRecordUUID,\n createRecordExtraDataController,\n Comment,\n GiveGift,\n} from '@autorecord/manager'\nimport { getInfo, getStream } from './stream'\nimport { assertStringType, ensureFolderExist, replaceExtName, singleton } from './utils'\n\nfunction createRecorder(opts: RecorderCreateOpts): Recorder {\n // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过\n // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。\n const recorder: Recorder = {\n id: opts.id ?? genRecorderUUID(),\n extra: opts.extra ?? {},\n ...mitt(),\n ...opts,\n\n availableStreams: [],\n availableSources: [],\n state: 'idle',\n\n getChannelURL() {\n return `https://live.douyin.com/${this.channelId}`\n },\n checkLiveStatusAndRecord: singleton(checkLiveStatusAndRecord),\n\n toJSON() {\n return defaultToJSON(provider, this)\n },\n }\n\n const recorderWithSupportUpdatedEvent = new Proxy(recorder, {\n set(obj, prop, value) {\n Reflect.set(obj, prop, value)\n\n if (typeof prop === 'string') {\n obj.emit('Updated', [prop])\n }\n\n return true\n },\n })\n\n return recorderWithSupportUpdatedEvent\n}\n\nconst ffmpegOutputOptions: string[] = ['-c', 'copy', '-movflags', 'frag_keyframe', '-min_frag_duration', '60000000']\nconst checkLiveStatusAndRecord: Recorder['checkLiveStatusAndRecord'] = async function ({ getSavePath }) {\n if (this.recordHandle != null) return this.recordHandle\n\n const { living, owner, title, roomId } = await getInfo(this.channelId)\n if (!living) return null\n\n this.state = 'recording'\n let res\n // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方\n try {\n res = await getStream({\n channelId: this.channelId,\n quality: this.quality,\n streamPriorities: this.streamPriorities,\n sourcePriorities: this.sourcePriorities,\n })\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n const { currentStream: stream, sources: availableSources, streams: availableStreams } = res\n this.availableStreams = availableStreams.map((s) => s.desc)\n this.availableSources = availableSources.map((s) => s.name)\n this.usedStream = stream.name\n this.usedSource = stream.source\n // TODO: emit update event\n\n const savePath = getSavePath({ owner, title })\n const extraDataSavePath = replaceExtName(savePath, '.json')\n const recordSavePath = savePath\n try {\n // TODO: 这个 ensure 或许应该放在 createRecordExtraDataController 里实现?\n ensureFolderExist(extraDataSavePath)\n ensureFolderExist(recordSavePath)\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n\n // TODO: 之后可能要结合 disableRecordMeta 之类的来确认是否要创建文件。\n const extraDataController = createRecordExtraDataController(extraDataSavePath)\n extraDataController.setMeta({ title })\n\n // TODO: 弹幕录制\n\n let isEnded = false\n const onEnd = (...args: unknown[]) => {\n if (isEnded) return\n isEnded = true\n this.emit('DebugLog', {\n type: 'common',\n text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,\n })\n const reason = args[0] instanceof Error ? args[0].message : String(args[0])\n this.recordHandle?.stop(reason)\n }\n\n const isInvalidStream = createInvalidStreamChecker()\n const timeoutChecker = createTimeoutChecker(() => onEnd('ffmpeg timeout'), 10e3)\n const command = createFFMPEGBuilder(stream.url)\n .inputOptions(\n '-user_agent',\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',\n /**\n * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。\n * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。\n *\n * Refs:\n * https://github.com/Sunoo/homebridge-camera-ffmpeg/issues/462#issuecomment-617723949\n * https://stackoverflow.com/a/49273163/21858805\n */\n '-probesize',\n (64 * 1024).toString(),\n )\n .outputOptions(ffmpegOutputOptions)\n .output(recordSavePath)\n .on('error', onEnd)\n .on('end', () => onEnd('finished'))\n .on('stderr', (stderrLine) => {\n assertStringType(stderrLine)\n this.emit('DebugLog', { type: 'ffmpeg', text: stderrLine })\n\n if (isInvalidStream(stderrLine)) {\n onEnd('invalid stream')\n }\n })\n .on('stderr', timeoutChecker.update)\n const ffmpegArgs = command._getArguments()\n extraDataController.setMeta({\n recordStartTimestamp: Date.now(),\n ffmpegArgs,\n })\n command.run()\n\n // TODO: 需要一个机制防止空录制,比如检查文件的大小变化、ffmpeg 的输出、直播状态等\n\n const stop = singleton<RecordHandle['stop']>(async (reason?: string) => {\n if (!this.recordHandle) return\n this.state = 'stopping-record'\n // TODO: emit update event\n\n timeoutChecker.stop()\n\n // 如果给 SIGKILL 信号会非正常退出,SIGINT 可以被 ffmpeg 正常处理。\n // TODO: fluent-ffmpeg 好像没处理好这个 SIGINT 导致的退出信息,会抛一个错。\n command.kill('SIGINT')\n // TODO: 这里可能会有内存泄露,因为事件还没清,之后再检查下看看。\n // client?.close()\n extraDataController.setMeta({ recordStopTimestamp: Date.now() })\n extraDataController.flush()\n\n this.usedStream = undefined\n this.usedSource = undefined\n // TODO: other codes\n // TODO: emit update event\n\n this.emit('RecordStop', { recordHandle: this.recordHandle, reason })\n this.recordHandle = undefined\n this.state = 'idle'\n })\n\n this.recordHandle = {\n id: genRecordUUID(),\n stream: stream.name,\n source: stream.source,\n url: stream.url,\n ffmpegArgs,\n savePath: recordSavePath,\n stop,\n }\n this.emit('RecordStart', this.recordHandle)\n\n return this.recordHandle\n}\n\nfunction createTimeoutChecker(\n onTimeout: () => void,\n time: number,\n): {\n update: () => void\n stop: () => void\n} {\n let timer: NodeJS.Timeout | null = null\n let stopped: boolean = false\n\n const update = () => {\n if (stopped) return\n if (timer != null) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n onTimeout()\n }, time)\n }\n\n update()\n\n return {\n update,\n stop() {\n stopped = true\n if (timer != null) clearTimeout(timer)\n timer = null\n },\n }\n}\n\nfunction createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean {\n let prevFrame = 0\n let frameUnchangedCount = 0\n\n return (ffmpegLogLine) => {\n const streamInfo = ffmpegLogLine.match(\n /frame=\\s*(\\d+) fps=.*? q=.*? size=\\s*(\\d+)kB time=.*? bitrate=.*? speed=.*?/,\n )\n if (streamInfo != null) {\n const [, frameText] = streamInfo\n const frame = Number(frameText)\n\n if (frame === prevFrame) {\n if (++frameUnchangedCount >= 10) {\n return true\n }\n } else {\n prevFrame = frame\n frameUnchangedCount = 0\n }\n\n return false\n }\n\n if (ffmpegLogLine.includes('HTTP error 404 Not Found')) {\n return true\n }\n\n return false\n }\n}\n\nexport const provider: RecorderProvider<{}> = {\n id: 'DouYin',\n name: '抖音',\n siteURL: 'https://live.douyin.com/',\n\n matchURL(channelURL) {\n // TODO: 暂时不支持 v.douyin.com\n return /https?:\\/\\/live\\.douyin\\.com\\//.test(channelURL)\n },\n\n async resolveChannelInfoFromURL(channelURL) {\n if (!this.matchURL(channelURL)) return null\n\n const id = path.basename(new URL(channelURL).pathname)\n const info = await getInfo(id)\n\n return {\n id: info.roomId,\n title: info.title,\n owner: info.owner,\n }\n },\n\n createRecorder(opts) {\n return createRecorder({ providerId: provider.id, ...opts })\n },\n\n fromJSON(recorder) {\n return defaultFromJSON(this, recorder)\n },\n\n setFFMPEGOutputArgs(args) {\n ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args)\n },\n}\n","import { Qualities, Recorder } from '@autorecord/manager'\nimport { getRoomInfo, SourceProfile, StreamProfile } from './douyin_api'\nimport * as R from 'ramda'\nimport { getValuesFromArrayLikeFlexSpaceBetween } from './utils'\n\nexport async function getInfo(channelId: string): Promise<{\n living: boolean\n owner: string\n title: string\n roomId: string\n}> {\n const info = await getRoomInfo(channelId)\n\n return {\n living: info.living,\n owner: info.owner,\n title: info.title,\n roomId: info.roomId,\n }\n}\n\nexport async function getStream(\n opts: Pick<Recorder, 'channelId' | 'quality' | 'streamPriorities' | 'sourcePriorities'> & { rejectCache?: boolean },\n) {\n const info = await getRoomInfo(opts.channelId)\n if (!info.living) {\n throw new Error('It must be called getStream when living')\n }\n\n let expectStream: StreamProfile\n const streamsWithPriority = sortAndFilterStreamsByPriority(info.streams, opts.streamPriorities)\n if (streamsWithPriority.length > 0) {\n // 通过优先级来选择对应流\n expectStream = streamsWithPriority[0]\n } else {\n // 通过设置的画质选项来选择对应流\n const flexedStreams = getValuesFromArrayLikeFlexSpaceBetween(info.streams, Qualities.length)\n expectStream = flexedStreams[Qualities.indexOf(opts.quality)]\n }\n\n let expectSource: SourceProfile | null = null\n const sourcesWithPriority = sortAndFilterSourcesByPriority(info.sources, opts.sourcePriorities)\n if (sourcesWithPriority.length > 0) {\n expectSource = sourcesWithPriority[0]\n } else {\n expectSource = info.sources[0]\n }\n\n return {\n ...info,\n currentStream: {\n name: expectStream.desc,\n source: expectSource.name,\n url: expectSource.streamMap[expectStream.key].main.flv,\n },\n }\n}\n\n/**\n * 按提供的流优先级去给流列表排序,并过滤掉不在优先级配置中的流\n */\nfunction sortAndFilterStreamsByPriority(\n streams: StreamProfile[],\n streamPriorities: Recorder['streamPriorities'],\n): (StreamProfile & {\n priority: number\n})[] {\n if (streamPriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n streams\n .map((stream) => ({\n ...stream,\n priority: R.reverse(streamPriorities).indexOf(stream.desc),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n\n/**\n * 按提供的源优先级去给源列表排序,并过滤掉不在优先级配置中的源\n */\nfunction sortAndFilterSourcesByPriority(\n sources: SourceProfile[],\n sourcePriorities: Recorder['sourcePriorities'],\n): (SourceProfile & {\n priority: number\n})[] {\n if (sourcePriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n sources\n .map((source) => ({\n ...source,\n priority: R.reverse(sourcePriorities).indexOf(source.name),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n","import axios from 'axios'\nimport { wrapper } from 'axios-cookiejar-support'\nimport { CookieJar } from 'tough-cookie'\nimport { assert } from './utils'\n\nconst jar = new CookieJar()\nconst requester = wrapper(\n axios.create({\n timeout: 10e3,\n jar,\n // axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,这会让请求发往代理的 host。\n // 于是 set-cookie 的 domain 与请求的 host 无法匹配上,tough-cookie 在检查时会丢弃它,导致 cookie 丢失。\n // 所以这里需要主动禁用代理功能。\n proxy: false,\n }),\n)\n\nexport async function getRoomInfo(\n webRoomId: string,\n retryOnSpecialCode = true,\n): Promise<{\n living: boolean\n roomId: string\n owner: string\n title: string\n streams: StreamProfile[]\n sources: SourceProfile[]\n}> {\n // 抖音的 'webcast/room/web/enter' api 会需要 ttwid 的 cookie,这个 cookie 是由这个请求的响应头设置的,\n // 所以在这里请求一次自动设置。\n await requester.get('https://live.douyin.com/')\n\n const res = await requester.get<EnterRoomApiResp>('https://live.douyin.com/webcast/room/web/enter/', {\n params: {\n aid: 6383,\n live_id: 1,\n device_platform: 'web',\n language: 'zh-CN',\n enter_from: 'web_live',\n cookie_enabled: 'true',\n screen_width: 1920,\n screen_height: 1080,\n browser_language: 'zh-CN',\n browser_platform: 'MacIntel',\n browser_name: 'Chrome',\n browser_version: '108.0.0.0',\n web_rid: webRoomId,\n // enter_source:,\n 'Room-Enter-User-Login-Ab': 0,\n is_need_double_stream: 'false',\n },\n })\n\n // 无 cookie 时 code 为 10037\n if (res.data.status_code === 10037 && retryOnSpecialCode) {\n // resp 自动设置 cookie\n await requester.get('https://live.douyin.com/favicon.ico')\n return getRoomInfo(webRoomId, false)\n }\n\n assert(\n res.data.status_code === 0,\n `Unexpected resp, code ${res.data.status_code}, msg ${res.data.data}, id ${webRoomId}`,\n )\n\n const data = res.data.data\n const room = data.data[0]\n\n if (room?.stream_url == null) {\n return {\n living: false,\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room?.title ?? data.user.nickname,\n streams: [],\n sources: [],\n }\n }\n\n const {\n options: { qualities },\n stream_data,\n } = room.stream_url.live_core_sdk_data.pull_data\n const streamData = (JSON.parse(stream_data) as StreamData).data\n\n const streams: StreamProfile[] = qualities.map((info) => ({\n desc: info.name,\n key: info.sdk_key,\n bitRate: info.v_bit_rate,\n }))\n\n // 看起来抖音是自动切换 cdn 的,所以这里固定返回一个默认的 source。\n const sources: SourceProfile[] = [\n {\n name: '自动切换线路',\n streamMap: streamData,\n },\n ]\n\n return {\n living: data.room_status === 0,\n // 接口里不会再返回 web room id,只能直接用入参原路返回了。\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room.title,\n streams,\n sources,\n }\n}\n\nexport interface StreamProfile {\n desc: string\n key: string\n bitRate: number\n}\n\nexport interface SourceProfile {\n name: string\n streamMap: StreamData['data']\n}\n\ninterface EnterRoomApiResp {\n data: {\n data: [\n | undefined\n | {\n id_str: string\n status: number\n status_str: string\n title: string\n user_count_str: string\n cover: {\n url_list: string[]\n }\n stream_url?: {\n flv_pull_url: PullURLMap\n default_resolution: string\n hls_pull_url_map: PullURLMap\n hls_pull_url: string\n stream_orientation: number\n live_core_sdk_data: {\n pull_data: {\n options: {\n default_quality: QualityInfo\n qualities: QualityInfo[]\n }\n stream_data: string\n }\n }\n extra: {\n height: number\n width: number\n fps: number\n max_bitrate: number\n min_bitrate: number\n default_bitrate: number\n bitrate_adapt_strategy: number\n anchor_interact_profile: number\n audience_interact_profile: number\n hardware_encode: boolean\n video_profile: number\n h265_enable: boolean\n gop_sec: number\n bframe_enable: boolean\n roi: boolean\n sw_roi: boolean\n bytevc1_enable: boolean\n }\n pull_datas: unknown\n }\n mosaic_status: number\n mosaic_status_str: string\n admin_user_ids: number[]\n admin_user_ids_str: string[]\n owner: UserInfo\n room_auth: unknown\n live_room_mode: number\n stats: {\n total_user_desp: string\n like_count: number\n total_user_str: string\n user_count_str: string\n }\n has_commerce_goods: boolean\n linker_map: {}\n linker_detail: unknown\n room_view_stats: {\n is_hidden: boolean\n display_short: string\n display_middle: string\n display_long: string\n display_value: number\n display_version: number\n incremental: boolean\n display_type: number\n display_short_anchor: string\n display_middle_anchor: string\n display_long_anchor: string\n }\n scene_type_info: unknown\n toolbar_data: unknown\n room_cart: unknown\n },\n ]\n enter_room_id: string\n extra?: {\n digg_color: string\n pay_scores: string\n is_official_channel: boolean\n signature: string\n }\n user: UserInfo\n qrcode_url: string\n enter_mode: number\n room_status: number\n partition_road_map?: unknown\n similar_rooms: unknown[]\n shark_decision_conf: string\n web_stream_url?: unknown\n }\n extra: { now: number }\n status_code: number\n}\n\ntype PullURLMap = Record<string, string>\n\ninterface QualityInfo {\n name: string\n sdk_key: string\n v_codec: string\n resolution: string\n level: number\n v_bit_rate: number\n additional_content: string\n fps: number\n disable: number\n}\n\ninterface UserInfo {\n id_str: string\n sec_uid: string\n nickname: string\n avatar_thumb: {\n url_list: string[]\n }\n follow_info: { follow_status: number; follow_status_str: string }\n}\n\ninterface StreamData {\n common: unknown\n data: Record<\n string,\n {\n main: {\n flv: string\n hls: string\n cmaf: string\n dash: string\n lls: string\n tsl: string\n tile: string\n sdk_params: string\n }\n }\n >\n}\n","import fs from 'fs'\nimport path from 'path'\nimport * as R from 'ramda'\n\n/**\n * 接收 fn ,返回一个和 fn 签名一致的函数 fn'。当已经有一个 fn' 在运行时,再调用\n * fn' 会直接返回运行中 fn' 的 Promise,直到 Promise 结束 pending 状态\n */\nexport function singleton<Fn extends (...args: any) => Promise<any>>(fn: Fn): Fn {\n let latestPromise: Promise<unknown> | null = null\n\n return function (...args) {\n if (latestPromise) return latestPromise\n\n const promise = fn.apply(this, args).finally(() => {\n if (promise === latestPromise) {\n latestPromise = null\n }\n })\n\n latestPromise = promise\n return promise\n } as Fn\n}\n\n/**\n * 从数组中按照特定算法提取一些值(允许同个索引重复提取)。\n * 算法的行为类似 flex 的 space-between。\n *\n * examples:\n * ```\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 1))\n * // [1]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 3))\n * // [1, 4, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 4))\n * // [1, 3, 5, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 11))\n * // [1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7]\n * ```\n */\nexport function getValuesFromArrayLikeFlexSpaceBetween<T>(array: T[], columnCount: number): T[] {\n if (columnCount < 1) return []\n if (columnCount === 1) return [array[0]]\n\n const spacingCount = columnCount - 1\n const spacingLength = array.length / spacingCount\n\n const columns = R.range(1, columnCount + 1)\n const columnValues = columns.map((column, idx, columns) => {\n // 首个和最后的列是特殊的,因为它们不在范围内,而是在两端\n if (idx === 0) {\n return array[0]\n } else if (idx === columns.length - 1) {\n return array[array.length - 1]\n }\n\n const beforeSpacingCount = column - 1\n const colPos = beforeSpacingCount * spacingLength\n\n return array[Math.floor(colPos)]\n })\n\n return columnValues\n}\n\nexport function ensureFolderExist(fileOrFolderPath: string): void {\n const folder = path.dirname(fileOrFolderPath)\n if (!fs.existsSync(folder)) {\n fs.mkdirSync(folder, { recursive: true })\n }\n}\n\nexport function assert(assertion: unknown, msg?: string): asserts assertion {\n if (!assertion) {\n throw new Error(msg)\n }\n}\n\nexport function assertStringType(data: unknown, msg?: string): asserts data is string {\n assert(typeof data === 'string', msg)\n}\n\nexport function assertNumberType(data: unknown, msg?: string): asserts data is number {\n assert(typeof data === 'number', msg)\n}\n\nexport function assertObjectType(data: unknown, msg?: string): asserts data is object {\n assert(typeof data === 'object', msg)\n}\n\nexport function replaceExtName(filePath: string, newExtName: string) {\n return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName)\n}\n"],"mappings":"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","names":["import_path","import_manager","columns","path","fs","path","axios","R","mitt","prop","path"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/stream.ts","../src/douyin_api.ts","../src/utils.ts"],"sourcesContent":["import path from 'path'\nimport mitt from 'mitt'\nimport {\n Recorder,\n RecorderCreateOpts,\n RecorderProvider,\n createFFMPEGBuilder,\n RecordHandle,\n defaultFromJSON,\n defaultToJSON,\n genRecorderUUID,\n genRecordUUID,\n createRecordExtraDataController,\n Comment,\n GiveGift,\n} from '@autorecord/manager'\nimport { getInfo, getStream } from './stream'\nimport { getAuthCookie, setAuthCookie } from './douyin_api'\nimport { assertStringType, ensureFolderExist, replaceExtName, singleton } from './utils'\n\nfunction createRecorder(opts: RecorderCreateOpts): Recorder {\n // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过\n // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。\n const recorder: Recorder = {\n id: opts.id ?? genRecorderUUID(),\n extra: opts.extra ?? {},\n ...mitt(),\n ...opts,\n\n availableStreams: [],\n availableSources: [],\n state: 'idle',\n\n getChannelURL() {\n return `https://live.douyin.com/${this.channelId}`\n },\n checkLiveStatusAndRecord: singleton(checkLiveStatusAndRecord),\n\n toJSON() {\n return defaultToJSON(provider, this)\n },\n }\n\n const recorderWithSupportUpdatedEvent = new Proxy(recorder, {\n set(obj, prop, value) {\n Reflect.set(obj, prop, value)\n\n if (typeof prop === 'string') {\n obj.emit('Updated', [prop])\n }\n\n return true\n },\n })\n\n return recorderWithSupportUpdatedEvent\n}\n\nconst ffmpegOutputOptions: string[] = ['-c', 'copy', '-movflags', 'frag_keyframe', '-min_frag_duration', '60000000']\nconst checkLiveStatusAndRecord: Recorder['checkLiveStatusAndRecord'] = async function ({ getSavePath }) {\n if (this.recordHandle != null) return this.recordHandle\n\n const { living, owner, title, roomId } = await getInfo(this.channelId)\n if (!living) return null\n\n this.state = 'recording'\n let res\n // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方\n try {\n res = await getStream({\n channelId: this.channelId,\n quality: this.quality,\n streamPriorities: this.streamPriorities,\n sourcePriorities: this.sourcePriorities,\n })\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n const { currentStream: stream, sources: availableSources, streams: availableStreams } = res\n this.availableStreams = availableStreams.map((s) => s.desc)\n this.availableSources = availableSources.map((s) => s.name)\n this.usedStream = stream.name\n this.usedSource = stream.source\n // TODO: emit update event\n\n const savePath = getSavePath({ owner, title })\n const extraDataSavePath = replaceExtName(savePath, '.json')\n const recordSavePath = savePath\n try {\n // TODO: 这个 ensure 或许应该放在 createRecordExtraDataController 里实现?\n ensureFolderExist(extraDataSavePath)\n ensureFolderExist(recordSavePath)\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n\n // TODO: 之后可能要结合 disableRecordMeta 之类的来确认是否要创建文件。\n const extraDataController = createRecordExtraDataController(extraDataSavePath)\n extraDataController.setMeta({ title })\n\n // TODO: 弹幕录制\n\n let isEnded = false\n const onEnd = (...args: unknown[]) => {\n if (isEnded) return\n isEnded = true\n this.emit('DebugLog', {\n type: 'common',\n text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,\n })\n const reason = args[0] instanceof Error ? args[0].message : String(args[0])\n this.recordHandle?.stop(reason)\n }\n\n const isInvalidStream = createInvalidStreamChecker()\n const timeoutChecker = createTimeoutChecker(() => onEnd('ffmpeg timeout'), 10e3)\n const cookie = getAuthCookie()\n const headersValue = cookie\n ? `Referer: https://live.douyin.com/\\r\\nCookie: ${cookie}`\n : 'Referer: https://live.douyin.com/'\n\n const command = createFFMPEGBuilder(stream.url)\n .inputOptions(\n '-user_agent',\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',\n '-headers',\n headersValue,\n /**\n * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。\n * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。\n *\n * Refs:\n * https://github.com/Sunoo/homebridge-camera-ffmpeg/issues/462#issuecomment-617723949\n * https://stackoverflow.com/a/49273163/21858805\n */\n '-probesize',\n (64 * 1024).toString(),\n )\n .outputOptions(ffmpegOutputOptions)\n .output(recordSavePath)\n .on('error', onEnd)\n .on('end', () => onEnd('finished'))\n .on('stderr', (stderrLine) => {\n assertStringType(stderrLine)\n this.emit('DebugLog', { type: 'ffmpeg', text: stderrLine })\n\n if (isInvalidStream(stderrLine)) {\n onEnd('invalid stream')\n }\n })\n .on('stderr', timeoutChecker.update)\n const ffmpegArgs = command._getArguments()\n extraDataController.setMeta({\n recordStartTimestamp: Date.now(),\n ffmpegArgs,\n })\n command.run()\n\n // TODO: 需要一个机制防止空录制,比如检查文件的大小变化、ffmpeg 的输出、直播状态等\n\n const stop = singleton<RecordHandle['stop']>(async (reason?: string) => {\n if (!this.recordHandle) return\n this.state = 'stopping-record'\n // TODO: emit update event\n\n timeoutChecker.stop()\n\n // 如果给 SIGKILL 信号会非正常退出,SIGINT 可以被 ffmpeg 正常处理。\n // TODO: fluent-ffmpeg 好像没处理好这个 SIGINT 导致的退出信息,会抛一个错。\n command.kill('SIGINT')\n // TODO: 这里可能会有内存泄露,因为事件还没清,之后再检查下看看。\n // client?.close()\n extraDataController.setMeta({ recordStopTimestamp: Date.now() })\n extraDataController.flush()\n\n this.usedStream = undefined\n this.usedSource = undefined\n // TODO: other codes\n // TODO: emit update event\n\n this.emit('RecordStop', { recordHandle: this.recordHandle, reason })\n this.recordHandle = undefined\n this.state = 'idle'\n })\n\n this.recordHandle = {\n id: genRecordUUID(),\n stream: stream.name,\n source: stream.source,\n url: stream.url,\n ffmpegArgs,\n savePath: recordSavePath,\n stop,\n }\n this.emit('RecordStart', this.recordHandle)\n\n return this.recordHandle\n}\n\nfunction createTimeoutChecker(\n onTimeout: () => void,\n time: number,\n): {\n update: () => void\n stop: () => void\n} {\n let timer: NodeJS.Timeout | null = null\n let stopped: boolean = false\n\n const update = () => {\n if (stopped) return\n if (timer != null) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n onTimeout()\n }, time)\n }\n\n update()\n\n return {\n update,\n stop() {\n stopped = true\n if (timer != null) clearTimeout(timer)\n timer = null\n },\n }\n}\n\nfunction createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean {\n let prevFrame = 0\n let frameUnchangedCount = 0\n\n return (ffmpegLogLine) => {\n const streamInfo = ffmpegLogLine.match(\n /frame=\\s*(\\d+) fps=.*? q=.*? size=\\s*(\\d+)kB time=.*? bitrate=.*? speed=.*?/,\n )\n if (streamInfo != null) {\n const [, frameText] = streamInfo\n const frame = Number(frameText)\n\n if (frame === prevFrame) {\n if (++frameUnchangedCount >= 10) {\n return true\n }\n } else {\n prevFrame = frame\n frameUnchangedCount = 0\n }\n\n return false\n }\n\n if (ffmpegLogLine.includes('HTTP error 404 Not Found')) {\n return true\n }\n\n return false\n }\n}\n\nexport const provider: RecorderProvider<{}> = {\n id: 'DouYin',\n name: '抖音',\n siteURL: 'https://live.douyin.com/',\n\n matchURL(channelURL) {\n // TODO: 暂时不支持 v.douyin.com\n return /https?:\\/\\/live\\.douyin\\.com\\//.test(channelURL)\n },\n\n async resolveChannelInfoFromURL(channelURL) {\n if (!this.matchURL(channelURL)) return null\n\n const id = path.basename(new URL(channelURL).pathname)\n const info = await getInfo(id)\n\n return {\n id: info.roomId,\n title: info.title,\n owner: info.owner,\n }\n },\n\n createRecorder(opts) {\n return createRecorder({ providerId: provider.id, ...opts })\n },\n\n fromJSON(recorder) {\n return defaultFromJSON(this, recorder)\n },\n\n setFFMPEGOutputArgs(args) {\n ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args)\n },\n\n authFields: [\n {\n key: 'cookie',\n label: 'Cookie',\n type: 'textarea',\n required: false,\n placeholder: 'sessionid_ss=xxx; ttwid=xxx; ...',\n description: '从浏览器获取抖音登录 Cookie,用于获取更高画质直播流或解决访问限制',\n },\n ],\n\n authFlow: {\n loginURL: 'https://live.douyin.com/',\n checkLoginResult({ cookies }) {\n const douyinCookies = cookies.filter((c) => c.domain === '.douyin.com' || c.domain === 'live.douyin.com')\n const hasSession = douyinCookies.some((c) => c.name === 'sessionid_ss')\n if (!hasSession) return { success: false }\n\n const cookieString = douyinCookies.map((c) => `${c.name}=${c.value}`).join('; ')\n return { success: true, authConfig: { cookie: cookieString } }\n },\n timeout: 300_000,\n },\n\n setAuth(config) {\n setAuthCookie(config.cookie || undefined)\n },\n\n async checkAuth() {\n const cookie = getAuthCookie()\n if (!cookie) return { isAuthenticated: false }\n\n // 检查 cookie 中是否包含关键的登录标识\n const hasSession = /sessionid_ss=/.test(cookie)\n if (!hasSession) return { isAuthenticated: false, description: 'Cookie 中缺少 sessionid_ss' }\n\n return { isAuthenticated: true, description: '已设置登录 Cookie' }\n },\n}\n","import { Qualities, Recorder } from '@autorecord/manager'\nimport { getRoomInfo, SourceProfile, StreamProfile } from './douyin_api'\nimport * as R from 'ramda'\nimport { getValuesFromArrayLikeFlexSpaceBetween } from './utils'\n\nexport async function getInfo(channelId: string): Promise<{\n living: boolean\n owner: string\n title: string\n roomId: string\n}> {\n const info = await getRoomInfo(channelId)\n\n return {\n living: info.living,\n owner: info.owner,\n title: info.title,\n roomId: info.roomId,\n }\n}\n\nexport async function getStream(\n opts: Pick<Recorder, 'channelId' | 'quality' | 'streamPriorities' | 'sourcePriorities'> & { rejectCache?: boolean },\n) {\n const info = await getRoomInfo(opts.channelId)\n if (!info.living) {\n throw new Error('It must be called getStream when living')\n }\n\n let expectStream: StreamProfile\n const streamsWithPriority = sortAndFilterStreamsByPriority(info.streams, opts.streamPriorities)\n if (streamsWithPriority.length > 0) {\n // 通过优先级来选择对应流\n expectStream = streamsWithPriority[0]\n } else {\n // 通过设置的画质选项来选择对应流\n const flexedStreams = getValuesFromArrayLikeFlexSpaceBetween(info.streams, Qualities.length)\n expectStream = flexedStreams[Qualities.indexOf(opts.quality)]\n }\n\n let expectSource: SourceProfile | null = null\n const sourcesWithPriority = sortAndFilterSourcesByPriority(info.sources, opts.sourcePriorities)\n if (sourcesWithPriority.length > 0) {\n expectSource = sourcesWithPriority[0]\n } else {\n expectSource = info.sources[0]\n }\n\n return {\n ...info,\n currentStream: {\n name: expectStream.desc,\n source: expectSource.name,\n url: expectSource.streamMap[expectStream.key].main.flv,\n },\n }\n}\n\n/**\n * 按提供的流优先级去给流列表排序,并过滤掉不在优先级配置中的流\n */\nfunction sortAndFilterStreamsByPriority(\n streams: StreamProfile[],\n streamPriorities: Recorder['streamPriorities'],\n): (StreamProfile & {\n priority: number\n})[] {\n if (streamPriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n streams\n .map((stream) => ({\n ...stream,\n priority: R.reverse(streamPriorities).indexOf(stream.desc),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n\n/**\n * 按提供的源优先级去给源列表排序,并过滤掉不在优先级配置中的源\n */\nfunction sortAndFilterSourcesByPriority(\n sources: SourceProfile[],\n sourcePriorities: Recorder['sourcePriorities'],\n): (SourceProfile & {\n priority: number\n})[] {\n if (sourcePriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n sources\n .map((source) => ({\n ...source,\n priority: R.reverse(sourcePriorities).indexOf(source.name),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n","import axios from 'axios'\nimport { wrapper } from 'axios-cookiejar-support'\nimport { CookieJar } from 'tough-cookie'\nimport { assert } from './utils'\n\nconst jar = new CookieJar()\nconst requester = wrapper(\n axios.create({\n timeout: 10e3,\n jar,\n // axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,这会让请求发往代理的 host。\n // 于是 set-cookie 的 domain 与请求的 host 无法匹配上,tough-cookie 在检查时会丢弃它,导致 cookie 丢失。\n // 所以这里需要主动禁用代理功能。\n proxy: false,\n }),\n)\n\n// 用户鉴权 cookie 管理(登录后的 cookie,用于获取更高画质等)\nlet authCookie: string | undefined\n\nexport function setAuthCookie(cookie: string | undefined) {\n authCookie = cookie\n}\n\nexport function getAuthCookie(): string | undefined {\n return authCookie\n}\n\n// 将用户鉴权 cookie 注入到请求中(追加到 cookie jar 自动管理的 ttwid 等 cookie 之后)\nrequester.interceptors.request.use((config) => {\n if (authCookie) {\n const existing = config.headers.Cookie\n config.headers.Cookie = existing ? `${existing}; ${authCookie}` : authCookie\n }\n return config\n})\n\nexport async function getRoomInfo(\n webRoomId: string,\n retryOnSpecialCode = true,\n): Promise<{\n living: boolean\n roomId: string\n owner: string\n title: string\n streams: StreamProfile[]\n sources: SourceProfile[]\n}> {\n // 抖音的 'webcast/room/web/enter' api 会需要 ttwid 的 cookie,这个 cookie 是由这个请求的响应头设置的,\n // 所以在这里请求一次自动设置。\n await requester.get('https://live.douyin.com/')\n\n const res = await requester.get<EnterRoomApiResp>('https://live.douyin.com/webcast/room/web/enter/', {\n params: {\n aid: 6383,\n live_id: 1,\n device_platform: 'web',\n language: 'zh-CN',\n enter_from: 'web_live',\n cookie_enabled: 'true',\n screen_width: 1920,\n screen_height: 1080,\n browser_language: 'zh-CN',\n browser_platform: 'MacIntel',\n browser_name: 'Chrome',\n browser_version: '108.0.0.0',\n web_rid: webRoomId,\n // enter_source:,\n 'Room-Enter-User-Login-Ab': 0,\n is_need_double_stream: 'false',\n },\n })\n\n // 无 cookie 时 code 为 10037\n if (res.data.status_code === 10037 && retryOnSpecialCode) {\n // resp 自动设置 cookie\n await requester.get('https://live.douyin.com/favicon.ico')\n return getRoomInfo(webRoomId, false)\n }\n\n assert(\n res.data.status_code === 0,\n `Unexpected resp, code ${res.data.status_code}, msg ${res.data.data}, id ${webRoomId}`,\n )\n\n const data = res.data.data\n const room = data.data[0]\n\n if (room?.stream_url == null) {\n return {\n living: false,\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room?.title ?? data.user.nickname,\n streams: [],\n sources: [],\n }\n }\n\n const {\n options: { qualities },\n stream_data,\n } = room.stream_url.live_core_sdk_data.pull_data\n const streamData = (JSON.parse(stream_data) as StreamData).data\n\n const streams: StreamProfile[] = qualities.map((info) => ({\n desc: info.name,\n key: info.sdk_key,\n bitRate: info.v_bit_rate,\n }))\n\n // 看起来抖音是自动切换 cdn 的,所以这里固定返回一个默认的 source。\n const sources: SourceProfile[] = [\n {\n name: '自动切换线路',\n streamMap: streamData,\n },\n ]\n\n return {\n living: data.room_status === 0,\n // 接口里不会再返回 web room id,只能直接用入参原路返回了。\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room.title,\n streams,\n sources,\n }\n}\n\nexport interface StreamProfile {\n desc: string\n key: string\n bitRate: number\n}\n\nexport interface SourceProfile {\n name: string\n streamMap: StreamData['data']\n}\n\ninterface EnterRoomApiResp {\n data: {\n data: [\n | undefined\n | {\n id_str: string\n status: number\n status_str: string\n title: string\n user_count_str: string\n cover: {\n url_list: string[]\n }\n stream_url?: {\n flv_pull_url: PullURLMap\n default_resolution: string\n hls_pull_url_map: PullURLMap\n hls_pull_url: string\n stream_orientation: number\n live_core_sdk_data: {\n pull_data: {\n options: {\n default_quality: QualityInfo\n qualities: QualityInfo[]\n }\n stream_data: string\n }\n }\n extra: {\n height: number\n width: number\n fps: number\n max_bitrate: number\n min_bitrate: number\n default_bitrate: number\n bitrate_adapt_strategy: number\n anchor_interact_profile: number\n audience_interact_profile: number\n hardware_encode: boolean\n video_profile: number\n h265_enable: boolean\n gop_sec: number\n bframe_enable: boolean\n roi: boolean\n sw_roi: boolean\n bytevc1_enable: boolean\n }\n pull_datas: unknown\n }\n mosaic_status: number\n mosaic_status_str: string\n admin_user_ids: number[]\n admin_user_ids_str: string[]\n owner: UserInfo\n room_auth: unknown\n live_room_mode: number\n stats: {\n total_user_desp: string\n like_count: number\n total_user_str: string\n user_count_str: string\n }\n has_commerce_goods: boolean\n linker_map: {}\n linker_detail: unknown\n room_view_stats: {\n is_hidden: boolean\n display_short: string\n display_middle: string\n display_long: string\n display_value: number\n display_version: number\n incremental: boolean\n display_type: number\n display_short_anchor: string\n display_middle_anchor: string\n display_long_anchor: string\n }\n scene_type_info: unknown\n toolbar_data: unknown\n room_cart: unknown\n },\n ]\n enter_room_id: string\n extra?: {\n digg_color: string\n pay_scores: string\n is_official_channel: boolean\n signature: string\n }\n user: UserInfo\n qrcode_url: string\n enter_mode: number\n room_status: number\n partition_road_map?: unknown\n similar_rooms: unknown[]\n shark_decision_conf: string\n web_stream_url?: unknown\n }\n extra: { now: number }\n status_code: number\n}\n\ntype PullURLMap = Record<string, string>\n\ninterface QualityInfo {\n name: string\n sdk_key: string\n v_codec: string\n resolution: string\n level: number\n v_bit_rate: number\n additional_content: string\n fps: number\n disable: number\n}\n\ninterface UserInfo {\n id_str: string\n sec_uid: string\n nickname: string\n avatar_thumb: {\n url_list: string[]\n }\n follow_info: { follow_status: number; follow_status_str: string }\n}\n\ninterface StreamData {\n common: unknown\n data: Record<\n string,\n {\n main: {\n flv: string\n hls: string\n cmaf: string\n dash: string\n lls: string\n tsl: string\n tile: string\n sdk_params: string\n }\n }\n >\n}\n","import fs from 'fs'\nimport path from 'path'\nimport * as R from 'ramda'\n\n/**\n * 接收 fn ,返回一个和 fn 签名一致的函数 fn'。当已经有一个 fn' 在运行时,再调用\n * fn' 会直接返回运行中 fn' 的 Promise,直到 Promise 结束 pending 状态\n */\nexport function singleton<Fn extends (...args: any) => Promise<any>>(fn: Fn): Fn {\n let latestPromise: Promise<unknown> | null = null\n\n return function (...args) {\n if (latestPromise) return latestPromise\n\n const promise = fn.apply(this, args).finally(() => {\n if (promise === latestPromise) {\n latestPromise = null\n }\n })\n\n latestPromise = promise\n return promise\n } as Fn\n}\n\n/**\n * 从数组中按照特定算法提取一些值(允许同个索引重复提取)。\n * 算法的行为类似 flex 的 space-between。\n *\n * examples:\n * ```\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 1))\n * // [1]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 3))\n * // [1, 4, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 4))\n * // [1, 3, 5, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 11))\n * // [1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7]\n * ```\n */\nexport function getValuesFromArrayLikeFlexSpaceBetween<T>(array: T[], columnCount: number): T[] {\n if (columnCount < 1) return []\n if (columnCount === 1) return [array[0]]\n\n const spacingCount = columnCount - 1\n const spacingLength = array.length / spacingCount\n\n const columns = R.range(1, columnCount + 1)\n const columnValues = columns.map((column, idx, columns) => {\n // 首个和最后的列是特殊的,因为它们不在范围内,而是在两端\n if (idx === 0) {\n return array[0]\n } else if (idx === columns.length - 1) {\n return array[array.length - 1]\n }\n\n const beforeSpacingCount = column - 1\n const colPos = beforeSpacingCount * spacingLength\n\n return array[Math.floor(colPos)]\n })\n\n return columnValues\n}\n\nexport function ensureFolderExist(fileOrFolderPath: string): void {\n const folder = path.dirname(fileOrFolderPath)\n if (!fs.existsSync(folder)) {\n fs.mkdirSync(folder, { recursive: true })\n }\n}\n\nexport function assert(assertion: unknown, msg?: string): asserts assertion {\n if (!assertion) {\n throw new Error(msg)\n }\n}\n\nexport function assertStringType(data: unknown, msg?: string): asserts data is string {\n assert(typeof data === 'string', msg)\n}\n\nexport function assertNumberType(data: unknown, msg?: string): asserts data is number {\n assert(typeof data === 'number', msg)\n}\n\nexport function assertObjectType(data: unknown, msg?: string): asserts data is object {\n assert(typeof data === 'object', msg)\n}\n\nexport function replaceExtName(filePath: string, newExtName: string) {\n return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName)\n}\n"],"mappings":"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","names":["import_path","import_manager","columns","path","fs","path","axios","R","mitt","prop","path"]}
package/lib/index.js CHANGED
@@ -83,6 +83,20 @@ var requester = wrapper(
83
83
  proxy: false
84
84
  })
85
85
  );
86
+ var authCookie;
87
+ function setAuthCookie(cookie) {
88
+ authCookie = cookie;
89
+ }
90
+ function getAuthCookie() {
91
+ return authCookie;
92
+ }
93
+ requester.interceptors.request.use((config) => {
94
+ if (authCookie) {
95
+ const existing = config.headers.Cookie;
96
+ config.headers.Cookie = existing ? `${existing}; ${authCookie}` : authCookie;
97
+ }
98
+ return config;
99
+ });
86
100
  async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
87
101
  await requester.get("https://live.douyin.com/");
88
102
  const res = await requester.get("https://live.douyin.com/webcast/room/web/enter/", {
@@ -292,9 +306,14 @@ var checkLiveStatusAndRecord = async function({ getSavePath }) {
292
306
  };
293
307
  const isInvalidStream = createInvalidStreamChecker();
294
308
  const timeoutChecker = createTimeoutChecker(() => onEnd("ffmpeg timeout"), 1e4);
309
+ const cookie = getAuthCookie();
310
+ const headersValue = cookie ? `Referer: https://live.douyin.com/\r
311
+ Cookie: ${cookie}` : "Referer: https://live.douyin.com/";
295
312
  const command = createFFMPEGBuilder(stream.url).inputOptions(
296
313
  "-user_agent",
297
314
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
315
+ "-headers",
316
+ headersValue,
298
317
  /**
299
318
  * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。
300
319
  * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。
@@ -415,6 +434,37 @@ var provider = {
415
434
  },
416
435
  setFFMPEGOutputArgs(args) {
417
436
  ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args);
437
+ },
438
+ authFields: [
439
+ {
440
+ key: "cookie",
441
+ label: "Cookie",
442
+ type: "textarea",
443
+ required: false,
444
+ placeholder: "sessionid_ss=xxx; ttwid=xxx; ...",
445
+ description: "\u4ECE\u6D4F\u89C8\u5668\u83B7\u53D6\u6296\u97F3\u767B\u5F55 Cookie\uFF0C\u7528\u4E8E\u83B7\u53D6\u66F4\u9AD8\u753B\u8D28\u76F4\u64AD\u6D41\u6216\u89E3\u51B3\u8BBF\u95EE\u9650\u5236"
446
+ }
447
+ ],
448
+ authFlow: {
449
+ loginURL: "https://live.douyin.com/",
450
+ checkLoginResult({ cookies }) {
451
+ const douyinCookies = cookies.filter((c) => c.domain === ".douyin.com" || c.domain === "live.douyin.com");
452
+ const hasSession = douyinCookies.some((c) => c.name === "sessionid_ss");
453
+ if (!hasSession) return { success: false };
454
+ const cookieString = douyinCookies.map((c) => `${c.name}=${c.value}`).join("; ");
455
+ return { success: true, authConfig: { cookie: cookieString } };
456
+ },
457
+ timeout: 3e5
458
+ },
459
+ setAuth(config) {
460
+ setAuthCookie(config.cookie || void 0);
461
+ },
462
+ async checkAuth() {
463
+ const cookie = getAuthCookie();
464
+ if (!cookie) return { isAuthenticated: false };
465
+ const hasSession = /sessionid_ss=/.test(cookie);
466
+ if (!hasSession) return { isAuthenticated: false, description: "Cookie \u4E2D\u7F3A\u5C11 sessionid_ss" };
467
+ return { isAuthenticated: true, description: "\u5DF2\u8BBE\u7F6E\u767B\u5F55 Cookie" };
418
468
  }
419
469
  };
420
470
  export {
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/stream.ts","../src/douyin_api.ts","../src/utils.ts"],"sourcesContent":["import path from 'path'\nimport mitt from 'mitt'\nimport {\n Recorder,\n RecorderCreateOpts,\n RecorderProvider,\n createFFMPEGBuilder,\n RecordHandle,\n defaultFromJSON,\n defaultToJSON,\n genRecorderUUID,\n genRecordUUID,\n createRecordExtraDataController,\n Comment,\n GiveGift,\n} from '@autorecord/manager'\nimport { getInfo, getStream } from './stream'\nimport { assertStringType, ensureFolderExist, replaceExtName, singleton } from './utils'\n\nfunction createRecorder(opts: RecorderCreateOpts): Recorder {\n // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过\n // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。\n const recorder: Recorder = {\n id: opts.id ?? genRecorderUUID(),\n extra: opts.extra ?? {},\n ...mitt(),\n ...opts,\n\n availableStreams: [],\n availableSources: [],\n state: 'idle',\n\n getChannelURL() {\n return `https://live.douyin.com/${this.channelId}`\n },\n checkLiveStatusAndRecord: singleton(checkLiveStatusAndRecord),\n\n toJSON() {\n return defaultToJSON(provider, this)\n },\n }\n\n const recorderWithSupportUpdatedEvent = new Proxy(recorder, {\n set(obj, prop, value) {\n Reflect.set(obj, prop, value)\n\n if (typeof prop === 'string') {\n obj.emit('Updated', [prop])\n }\n\n return true\n },\n })\n\n return recorderWithSupportUpdatedEvent\n}\n\nconst ffmpegOutputOptions: string[] = ['-c', 'copy', '-movflags', 'frag_keyframe', '-min_frag_duration', '60000000']\nconst checkLiveStatusAndRecord: Recorder['checkLiveStatusAndRecord'] = async function ({ getSavePath }) {\n if (this.recordHandle != null) return this.recordHandle\n\n const { living, owner, title, roomId } = await getInfo(this.channelId)\n if (!living) return null\n\n this.state = 'recording'\n let res\n // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方\n try {\n res = await getStream({\n channelId: this.channelId,\n quality: this.quality,\n streamPriorities: this.streamPriorities,\n sourcePriorities: this.sourcePriorities,\n })\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n const { currentStream: stream, sources: availableSources, streams: availableStreams } = res\n this.availableStreams = availableStreams.map((s) => s.desc)\n this.availableSources = availableSources.map((s) => s.name)\n this.usedStream = stream.name\n this.usedSource = stream.source\n // TODO: emit update event\n\n const savePath = getSavePath({ owner, title })\n const extraDataSavePath = replaceExtName(savePath, '.json')\n const recordSavePath = savePath\n try {\n // TODO: 这个 ensure 或许应该放在 createRecordExtraDataController 里实现?\n ensureFolderExist(extraDataSavePath)\n ensureFolderExist(recordSavePath)\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n\n // TODO: 之后可能要结合 disableRecordMeta 之类的来确认是否要创建文件。\n const extraDataController = createRecordExtraDataController(extraDataSavePath)\n extraDataController.setMeta({ title })\n\n // TODO: 弹幕录制\n\n let isEnded = false\n const onEnd = (...args: unknown[]) => {\n if (isEnded) return\n isEnded = true\n this.emit('DebugLog', {\n type: 'common',\n text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,\n })\n const reason = args[0] instanceof Error ? args[0].message : String(args[0])\n this.recordHandle?.stop(reason)\n }\n\n const isInvalidStream = createInvalidStreamChecker()\n const timeoutChecker = createTimeoutChecker(() => onEnd('ffmpeg timeout'), 10e3)\n const command = createFFMPEGBuilder(stream.url)\n .inputOptions(\n '-user_agent',\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',\n /**\n * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。\n * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。\n *\n * Refs:\n * https://github.com/Sunoo/homebridge-camera-ffmpeg/issues/462#issuecomment-617723949\n * https://stackoverflow.com/a/49273163/21858805\n */\n '-probesize',\n (64 * 1024).toString(),\n )\n .outputOptions(ffmpegOutputOptions)\n .output(recordSavePath)\n .on('error', onEnd)\n .on('end', () => onEnd('finished'))\n .on('stderr', (stderrLine) => {\n assertStringType(stderrLine)\n this.emit('DebugLog', { type: 'ffmpeg', text: stderrLine })\n\n if (isInvalidStream(stderrLine)) {\n onEnd('invalid stream')\n }\n })\n .on('stderr', timeoutChecker.update)\n const ffmpegArgs = command._getArguments()\n extraDataController.setMeta({\n recordStartTimestamp: Date.now(),\n ffmpegArgs,\n })\n command.run()\n\n // TODO: 需要一个机制防止空录制,比如检查文件的大小变化、ffmpeg 的输出、直播状态等\n\n const stop = singleton<RecordHandle['stop']>(async (reason?: string) => {\n if (!this.recordHandle) return\n this.state = 'stopping-record'\n // TODO: emit update event\n\n timeoutChecker.stop()\n\n // 如果给 SIGKILL 信号会非正常退出,SIGINT 可以被 ffmpeg 正常处理。\n // TODO: fluent-ffmpeg 好像没处理好这个 SIGINT 导致的退出信息,会抛一个错。\n command.kill('SIGINT')\n // TODO: 这里可能会有内存泄露,因为事件还没清,之后再检查下看看。\n // client?.close()\n extraDataController.setMeta({ recordStopTimestamp: Date.now() })\n extraDataController.flush()\n\n this.usedStream = undefined\n this.usedSource = undefined\n // TODO: other codes\n // TODO: emit update event\n\n this.emit('RecordStop', { recordHandle: this.recordHandle, reason })\n this.recordHandle = undefined\n this.state = 'idle'\n })\n\n this.recordHandle = {\n id: genRecordUUID(),\n stream: stream.name,\n source: stream.source,\n url: stream.url,\n ffmpegArgs,\n savePath: recordSavePath,\n stop,\n }\n this.emit('RecordStart', this.recordHandle)\n\n return this.recordHandle\n}\n\nfunction createTimeoutChecker(\n onTimeout: () => void,\n time: number,\n): {\n update: () => void\n stop: () => void\n} {\n let timer: NodeJS.Timeout | null = null\n let stopped: boolean = false\n\n const update = () => {\n if (stopped) return\n if (timer != null) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n onTimeout()\n }, time)\n }\n\n update()\n\n return {\n update,\n stop() {\n stopped = true\n if (timer != null) clearTimeout(timer)\n timer = null\n },\n }\n}\n\nfunction createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean {\n let prevFrame = 0\n let frameUnchangedCount = 0\n\n return (ffmpegLogLine) => {\n const streamInfo = ffmpegLogLine.match(\n /frame=\\s*(\\d+) fps=.*? q=.*? size=\\s*(\\d+)kB time=.*? bitrate=.*? speed=.*?/,\n )\n if (streamInfo != null) {\n const [, frameText] = streamInfo\n const frame = Number(frameText)\n\n if (frame === prevFrame) {\n if (++frameUnchangedCount >= 10) {\n return true\n }\n } else {\n prevFrame = frame\n frameUnchangedCount = 0\n }\n\n return false\n }\n\n if (ffmpegLogLine.includes('HTTP error 404 Not Found')) {\n return true\n }\n\n return false\n }\n}\n\nexport const provider: RecorderProvider<{}> = {\n id: 'DouYin',\n name: '抖音',\n siteURL: 'https://live.douyin.com/',\n\n matchURL(channelURL) {\n // TODO: 暂时不支持 v.douyin.com\n return /https?:\\/\\/live\\.douyin\\.com\\//.test(channelURL)\n },\n\n async resolveChannelInfoFromURL(channelURL) {\n if (!this.matchURL(channelURL)) return null\n\n const id = path.basename(new URL(channelURL).pathname)\n const info = await getInfo(id)\n\n return {\n id: info.roomId,\n title: info.title,\n owner: info.owner,\n }\n },\n\n createRecorder(opts) {\n return createRecorder({ providerId: provider.id, ...opts })\n },\n\n fromJSON(recorder) {\n return defaultFromJSON(this, recorder)\n },\n\n setFFMPEGOutputArgs(args) {\n ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args)\n },\n}\n","import { Qualities, Recorder } from '@autorecord/manager'\nimport { getRoomInfo, SourceProfile, StreamProfile } from './douyin_api'\nimport * as R from 'ramda'\nimport { getValuesFromArrayLikeFlexSpaceBetween } from './utils'\n\nexport async function getInfo(channelId: string): Promise<{\n living: boolean\n owner: string\n title: string\n roomId: string\n}> {\n const info = await getRoomInfo(channelId)\n\n return {\n living: info.living,\n owner: info.owner,\n title: info.title,\n roomId: info.roomId,\n }\n}\n\nexport async function getStream(\n opts: Pick<Recorder, 'channelId' | 'quality' | 'streamPriorities' | 'sourcePriorities'> & { rejectCache?: boolean },\n) {\n const info = await getRoomInfo(opts.channelId)\n if (!info.living) {\n throw new Error('It must be called getStream when living')\n }\n\n let expectStream: StreamProfile\n const streamsWithPriority = sortAndFilterStreamsByPriority(info.streams, opts.streamPriorities)\n if (streamsWithPriority.length > 0) {\n // 通过优先级来选择对应流\n expectStream = streamsWithPriority[0]\n } else {\n // 通过设置的画质选项来选择对应流\n const flexedStreams = getValuesFromArrayLikeFlexSpaceBetween(info.streams, Qualities.length)\n expectStream = flexedStreams[Qualities.indexOf(opts.quality)]\n }\n\n let expectSource: SourceProfile | null = null\n const sourcesWithPriority = sortAndFilterSourcesByPriority(info.sources, opts.sourcePriorities)\n if (sourcesWithPriority.length > 0) {\n expectSource = sourcesWithPriority[0]\n } else {\n expectSource = info.sources[0]\n }\n\n return {\n ...info,\n currentStream: {\n name: expectStream.desc,\n source: expectSource.name,\n url: expectSource.streamMap[expectStream.key].main.flv,\n },\n }\n}\n\n/**\n * 按提供的流优先级去给流列表排序,并过滤掉不在优先级配置中的流\n */\nfunction sortAndFilterStreamsByPriority(\n streams: StreamProfile[],\n streamPriorities: Recorder['streamPriorities'],\n): (StreamProfile & {\n priority: number\n})[] {\n if (streamPriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n streams\n .map((stream) => ({\n ...stream,\n priority: R.reverse(streamPriorities).indexOf(stream.desc),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n\n/**\n * 按提供的源优先级去给源列表排序,并过滤掉不在优先级配置中的源\n */\nfunction sortAndFilterSourcesByPriority(\n sources: SourceProfile[],\n sourcePriorities: Recorder['sourcePriorities'],\n): (SourceProfile & {\n priority: number\n})[] {\n if (sourcePriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n sources\n .map((source) => ({\n ...source,\n priority: R.reverse(sourcePriorities).indexOf(source.name),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n","import axios from 'axios'\nimport { wrapper } from 'axios-cookiejar-support'\nimport { CookieJar } from 'tough-cookie'\nimport { assert } from './utils'\n\nconst jar = new CookieJar()\nconst requester = wrapper(\n axios.create({\n timeout: 10e3,\n jar,\n // axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,这会让请求发往代理的 host。\n // 于是 set-cookie 的 domain 与请求的 host 无法匹配上,tough-cookie 在检查时会丢弃它,导致 cookie 丢失。\n // 所以这里需要主动禁用代理功能。\n proxy: false,\n }),\n)\n\nexport async function getRoomInfo(\n webRoomId: string,\n retryOnSpecialCode = true,\n): Promise<{\n living: boolean\n roomId: string\n owner: string\n title: string\n streams: StreamProfile[]\n sources: SourceProfile[]\n}> {\n // 抖音的 'webcast/room/web/enter' api 会需要 ttwid 的 cookie,这个 cookie 是由这个请求的响应头设置的,\n // 所以在这里请求一次自动设置。\n await requester.get('https://live.douyin.com/')\n\n const res = await requester.get<EnterRoomApiResp>('https://live.douyin.com/webcast/room/web/enter/', {\n params: {\n aid: 6383,\n live_id: 1,\n device_platform: 'web',\n language: 'zh-CN',\n enter_from: 'web_live',\n cookie_enabled: 'true',\n screen_width: 1920,\n screen_height: 1080,\n browser_language: 'zh-CN',\n browser_platform: 'MacIntel',\n browser_name: 'Chrome',\n browser_version: '108.0.0.0',\n web_rid: webRoomId,\n // enter_source:,\n 'Room-Enter-User-Login-Ab': 0,\n is_need_double_stream: 'false',\n },\n })\n\n // 无 cookie 时 code 为 10037\n if (res.data.status_code === 10037 && retryOnSpecialCode) {\n // resp 自动设置 cookie\n await requester.get('https://live.douyin.com/favicon.ico')\n return getRoomInfo(webRoomId, false)\n }\n\n assert(\n res.data.status_code === 0,\n `Unexpected resp, code ${res.data.status_code}, msg ${res.data.data}, id ${webRoomId}`,\n )\n\n const data = res.data.data\n const room = data.data[0]\n\n if (room?.stream_url == null) {\n return {\n living: false,\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room?.title ?? data.user.nickname,\n streams: [],\n sources: [],\n }\n }\n\n const {\n options: { qualities },\n stream_data,\n } = room.stream_url.live_core_sdk_data.pull_data\n const streamData = (JSON.parse(stream_data) as StreamData).data\n\n const streams: StreamProfile[] = qualities.map((info) => ({\n desc: info.name,\n key: info.sdk_key,\n bitRate: info.v_bit_rate,\n }))\n\n // 看起来抖音是自动切换 cdn 的,所以这里固定返回一个默认的 source。\n const sources: SourceProfile[] = [\n {\n name: '自动切换线路',\n streamMap: streamData,\n },\n ]\n\n return {\n living: data.room_status === 0,\n // 接口里不会再返回 web room id,只能直接用入参原路返回了。\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room.title,\n streams,\n sources,\n }\n}\n\nexport interface StreamProfile {\n desc: string\n key: string\n bitRate: number\n}\n\nexport interface SourceProfile {\n name: string\n streamMap: StreamData['data']\n}\n\ninterface EnterRoomApiResp {\n data: {\n data: [\n | undefined\n | {\n id_str: string\n status: number\n status_str: string\n title: string\n user_count_str: string\n cover: {\n url_list: string[]\n }\n stream_url?: {\n flv_pull_url: PullURLMap\n default_resolution: string\n hls_pull_url_map: PullURLMap\n hls_pull_url: string\n stream_orientation: number\n live_core_sdk_data: {\n pull_data: {\n options: {\n default_quality: QualityInfo\n qualities: QualityInfo[]\n }\n stream_data: string\n }\n }\n extra: {\n height: number\n width: number\n fps: number\n max_bitrate: number\n min_bitrate: number\n default_bitrate: number\n bitrate_adapt_strategy: number\n anchor_interact_profile: number\n audience_interact_profile: number\n hardware_encode: boolean\n video_profile: number\n h265_enable: boolean\n gop_sec: number\n bframe_enable: boolean\n roi: boolean\n sw_roi: boolean\n bytevc1_enable: boolean\n }\n pull_datas: unknown\n }\n mosaic_status: number\n mosaic_status_str: string\n admin_user_ids: number[]\n admin_user_ids_str: string[]\n owner: UserInfo\n room_auth: unknown\n live_room_mode: number\n stats: {\n total_user_desp: string\n like_count: number\n total_user_str: string\n user_count_str: string\n }\n has_commerce_goods: boolean\n linker_map: {}\n linker_detail: unknown\n room_view_stats: {\n is_hidden: boolean\n display_short: string\n display_middle: string\n display_long: string\n display_value: number\n display_version: number\n incremental: boolean\n display_type: number\n display_short_anchor: string\n display_middle_anchor: string\n display_long_anchor: string\n }\n scene_type_info: unknown\n toolbar_data: unknown\n room_cart: unknown\n },\n ]\n enter_room_id: string\n extra?: {\n digg_color: string\n pay_scores: string\n is_official_channel: boolean\n signature: string\n }\n user: UserInfo\n qrcode_url: string\n enter_mode: number\n room_status: number\n partition_road_map?: unknown\n similar_rooms: unknown[]\n shark_decision_conf: string\n web_stream_url?: unknown\n }\n extra: { now: number }\n status_code: number\n}\n\ntype PullURLMap = Record<string, string>\n\ninterface QualityInfo {\n name: string\n sdk_key: string\n v_codec: string\n resolution: string\n level: number\n v_bit_rate: number\n additional_content: string\n fps: number\n disable: number\n}\n\ninterface UserInfo {\n id_str: string\n sec_uid: string\n nickname: string\n avatar_thumb: {\n url_list: string[]\n }\n follow_info: { follow_status: number; follow_status_str: string }\n}\n\ninterface StreamData {\n common: unknown\n data: Record<\n string,\n {\n main: {\n flv: string\n hls: string\n cmaf: string\n dash: string\n lls: string\n tsl: string\n tile: string\n sdk_params: string\n }\n }\n >\n}\n","import fs from 'fs'\nimport path from 'path'\nimport * as R from 'ramda'\n\n/**\n * 接收 fn ,返回一个和 fn 签名一致的函数 fn'。当已经有一个 fn' 在运行时,再调用\n * fn' 会直接返回运行中 fn' 的 Promise,直到 Promise 结束 pending 状态\n */\nexport function singleton<Fn extends (...args: any) => Promise<any>>(fn: Fn): Fn {\n let latestPromise: Promise<unknown> | null = null\n\n return function (...args) {\n if (latestPromise) return latestPromise\n\n const promise = fn.apply(this, args).finally(() => {\n if (promise === latestPromise) {\n latestPromise = null\n }\n })\n\n latestPromise = promise\n return promise\n } as Fn\n}\n\n/**\n * 从数组中按照特定算法提取一些值(允许同个索引重复提取)。\n * 算法的行为类似 flex 的 space-between。\n *\n * examples:\n * ```\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 1))\n * // [1]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 3))\n * // [1, 4, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 4))\n * // [1, 3, 5, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 11))\n * // [1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7]\n * ```\n */\nexport function getValuesFromArrayLikeFlexSpaceBetween<T>(array: T[], columnCount: number): T[] {\n if (columnCount < 1) return []\n if (columnCount === 1) return [array[0]]\n\n const spacingCount = columnCount - 1\n const spacingLength = array.length / spacingCount\n\n const columns = R.range(1, columnCount + 1)\n const columnValues = columns.map((column, idx, columns) => {\n // 首个和最后的列是特殊的,因为它们不在范围内,而是在两端\n if (idx === 0) {\n return array[0]\n } else if (idx === columns.length - 1) {\n return array[array.length - 1]\n }\n\n const beforeSpacingCount = column - 1\n const colPos = beforeSpacingCount * spacingLength\n\n return array[Math.floor(colPos)]\n })\n\n return columnValues\n}\n\nexport function ensureFolderExist(fileOrFolderPath: string): void {\n const folder = path.dirname(fileOrFolderPath)\n if (!fs.existsSync(folder)) {\n fs.mkdirSync(folder, { recursive: true })\n }\n}\n\nexport function assert(assertion: unknown, msg?: string): asserts assertion {\n if (!assertion) {\n throw new Error(msg)\n }\n}\n\nexport function assertStringType(data: unknown, msg?: string): asserts data is string {\n assert(typeof data === 'string', msg)\n}\n\nexport function assertNumberType(data: unknown, msg?: string): asserts data is number {\n assert(typeof data === 'number', msg)\n}\n\nexport function assertObjectType(data: unknown, msg?: string): asserts data is object {\n assert(typeof data === 'object', msg)\n}\n\nexport function replaceExtName(filePath: string, newExtName: string) {\n return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName)\n}\n"],"mappings":"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","names":["path","columns","R","prop","path"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/stream.ts","../src/douyin_api.ts","../src/utils.ts"],"sourcesContent":["import path from 'path'\nimport mitt from 'mitt'\nimport {\n Recorder,\n RecorderCreateOpts,\n RecorderProvider,\n createFFMPEGBuilder,\n RecordHandle,\n defaultFromJSON,\n defaultToJSON,\n genRecorderUUID,\n genRecordUUID,\n createRecordExtraDataController,\n Comment,\n GiveGift,\n} from '@autorecord/manager'\nimport { getInfo, getStream } from './stream'\nimport { getAuthCookie, setAuthCookie } from './douyin_api'\nimport { assertStringType, ensureFolderExist, replaceExtName, singleton } from './utils'\n\nfunction createRecorder(opts: RecorderCreateOpts): Recorder {\n // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过\n // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。\n const recorder: Recorder = {\n id: opts.id ?? genRecorderUUID(),\n extra: opts.extra ?? {},\n ...mitt(),\n ...opts,\n\n availableStreams: [],\n availableSources: [],\n state: 'idle',\n\n getChannelURL() {\n return `https://live.douyin.com/${this.channelId}`\n },\n checkLiveStatusAndRecord: singleton(checkLiveStatusAndRecord),\n\n toJSON() {\n return defaultToJSON(provider, this)\n },\n }\n\n const recorderWithSupportUpdatedEvent = new Proxy(recorder, {\n set(obj, prop, value) {\n Reflect.set(obj, prop, value)\n\n if (typeof prop === 'string') {\n obj.emit('Updated', [prop])\n }\n\n return true\n },\n })\n\n return recorderWithSupportUpdatedEvent\n}\n\nconst ffmpegOutputOptions: string[] = ['-c', 'copy', '-movflags', 'frag_keyframe', '-min_frag_duration', '60000000']\nconst checkLiveStatusAndRecord: Recorder['checkLiveStatusAndRecord'] = async function ({ getSavePath }) {\n if (this.recordHandle != null) return this.recordHandle\n\n const { living, owner, title, roomId } = await getInfo(this.channelId)\n if (!living) return null\n\n this.state = 'recording'\n let res\n // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方\n try {\n res = await getStream({\n channelId: this.channelId,\n quality: this.quality,\n streamPriorities: this.streamPriorities,\n sourcePriorities: this.sourcePriorities,\n })\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n const { currentStream: stream, sources: availableSources, streams: availableStreams } = res\n this.availableStreams = availableStreams.map((s) => s.desc)\n this.availableSources = availableSources.map((s) => s.name)\n this.usedStream = stream.name\n this.usedSource = stream.source\n // TODO: emit update event\n\n const savePath = getSavePath({ owner, title })\n const extraDataSavePath = replaceExtName(savePath, '.json')\n const recordSavePath = savePath\n try {\n // TODO: 这个 ensure 或许应该放在 createRecordExtraDataController 里实现?\n ensureFolderExist(extraDataSavePath)\n ensureFolderExist(recordSavePath)\n } catch (err) {\n this.state = 'idle'\n throw err\n }\n\n // TODO: 之后可能要结合 disableRecordMeta 之类的来确认是否要创建文件。\n const extraDataController = createRecordExtraDataController(extraDataSavePath)\n extraDataController.setMeta({ title })\n\n // TODO: 弹幕录制\n\n let isEnded = false\n const onEnd = (...args: unknown[]) => {\n if (isEnded) return\n isEnded = true\n this.emit('DebugLog', {\n type: 'common',\n text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,\n })\n const reason = args[0] instanceof Error ? args[0].message : String(args[0])\n this.recordHandle?.stop(reason)\n }\n\n const isInvalidStream = createInvalidStreamChecker()\n const timeoutChecker = createTimeoutChecker(() => onEnd('ffmpeg timeout'), 10e3)\n const cookie = getAuthCookie()\n const headersValue = cookie\n ? `Referer: https://live.douyin.com/\\r\\nCookie: ${cookie}`\n : 'Referer: https://live.douyin.com/'\n\n const command = createFFMPEGBuilder(stream.url)\n .inputOptions(\n '-user_agent',\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',\n '-headers',\n headersValue,\n /**\n * ffmpeg 在处理抖音提供的某些直播间的流时,它会在 avformat_find_stream_info 阶段花费过多时间,这会让录制的过程推迟很久,从而触发超时。\n * 这里通过降低 avformat_find_stream_info 所需要的字节数量(默认为 5000000)来解决这个问题。\n *\n * Refs:\n * https://github.com/Sunoo/homebridge-camera-ffmpeg/issues/462#issuecomment-617723949\n * https://stackoverflow.com/a/49273163/21858805\n */\n '-probesize',\n (64 * 1024).toString(),\n )\n .outputOptions(ffmpegOutputOptions)\n .output(recordSavePath)\n .on('error', onEnd)\n .on('end', () => onEnd('finished'))\n .on('stderr', (stderrLine) => {\n assertStringType(stderrLine)\n this.emit('DebugLog', { type: 'ffmpeg', text: stderrLine })\n\n if (isInvalidStream(stderrLine)) {\n onEnd('invalid stream')\n }\n })\n .on('stderr', timeoutChecker.update)\n const ffmpegArgs = command._getArguments()\n extraDataController.setMeta({\n recordStartTimestamp: Date.now(),\n ffmpegArgs,\n })\n command.run()\n\n // TODO: 需要一个机制防止空录制,比如检查文件的大小变化、ffmpeg 的输出、直播状态等\n\n const stop = singleton<RecordHandle['stop']>(async (reason?: string) => {\n if (!this.recordHandle) return\n this.state = 'stopping-record'\n // TODO: emit update event\n\n timeoutChecker.stop()\n\n // 如果给 SIGKILL 信号会非正常退出,SIGINT 可以被 ffmpeg 正常处理。\n // TODO: fluent-ffmpeg 好像没处理好这个 SIGINT 导致的退出信息,会抛一个错。\n command.kill('SIGINT')\n // TODO: 这里可能会有内存泄露,因为事件还没清,之后再检查下看看。\n // client?.close()\n extraDataController.setMeta({ recordStopTimestamp: Date.now() })\n extraDataController.flush()\n\n this.usedStream = undefined\n this.usedSource = undefined\n // TODO: other codes\n // TODO: emit update event\n\n this.emit('RecordStop', { recordHandle: this.recordHandle, reason })\n this.recordHandle = undefined\n this.state = 'idle'\n })\n\n this.recordHandle = {\n id: genRecordUUID(),\n stream: stream.name,\n source: stream.source,\n url: stream.url,\n ffmpegArgs,\n savePath: recordSavePath,\n stop,\n }\n this.emit('RecordStart', this.recordHandle)\n\n return this.recordHandle\n}\n\nfunction createTimeoutChecker(\n onTimeout: () => void,\n time: number,\n): {\n update: () => void\n stop: () => void\n} {\n let timer: NodeJS.Timeout | null = null\n let stopped: boolean = false\n\n const update = () => {\n if (stopped) return\n if (timer != null) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n onTimeout()\n }, time)\n }\n\n update()\n\n return {\n update,\n stop() {\n stopped = true\n if (timer != null) clearTimeout(timer)\n timer = null\n },\n }\n}\n\nfunction createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean {\n let prevFrame = 0\n let frameUnchangedCount = 0\n\n return (ffmpegLogLine) => {\n const streamInfo = ffmpegLogLine.match(\n /frame=\\s*(\\d+) fps=.*? q=.*? size=\\s*(\\d+)kB time=.*? bitrate=.*? speed=.*?/,\n )\n if (streamInfo != null) {\n const [, frameText] = streamInfo\n const frame = Number(frameText)\n\n if (frame === prevFrame) {\n if (++frameUnchangedCount >= 10) {\n return true\n }\n } else {\n prevFrame = frame\n frameUnchangedCount = 0\n }\n\n return false\n }\n\n if (ffmpegLogLine.includes('HTTP error 404 Not Found')) {\n return true\n }\n\n return false\n }\n}\n\nexport const provider: RecorderProvider<{}> = {\n id: 'DouYin',\n name: '抖音',\n siteURL: 'https://live.douyin.com/',\n\n matchURL(channelURL) {\n // TODO: 暂时不支持 v.douyin.com\n return /https?:\\/\\/live\\.douyin\\.com\\//.test(channelURL)\n },\n\n async resolveChannelInfoFromURL(channelURL) {\n if (!this.matchURL(channelURL)) return null\n\n const id = path.basename(new URL(channelURL).pathname)\n const info = await getInfo(id)\n\n return {\n id: info.roomId,\n title: info.title,\n owner: info.owner,\n }\n },\n\n createRecorder(opts) {\n return createRecorder({ providerId: provider.id, ...opts })\n },\n\n fromJSON(recorder) {\n return defaultFromJSON(this, recorder)\n },\n\n setFFMPEGOutputArgs(args) {\n ffmpegOutputOptions.splice(0, ffmpegOutputOptions.length, ...args)\n },\n\n authFields: [\n {\n key: 'cookie',\n label: 'Cookie',\n type: 'textarea',\n required: false,\n placeholder: 'sessionid_ss=xxx; ttwid=xxx; ...',\n description: '从浏览器获取抖音登录 Cookie,用于获取更高画质直播流或解决访问限制',\n },\n ],\n\n authFlow: {\n loginURL: 'https://live.douyin.com/',\n checkLoginResult({ cookies }) {\n const douyinCookies = cookies.filter((c) => c.domain === '.douyin.com' || c.domain === 'live.douyin.com')\n const hasSession = douyinCookies.some((c) => c.name === 'sessionid_ss')\n if (!hasSession) return { success: false }\n\n const cookieString = douyinCookies.map((c) => `${c.name}=${c.value}`).join('; ')\n return { success: true, authConfig: { cookie: cookieString } }\n },\n timeout: 300_000,\n },\n\n setAuth(config) {\n setAuthCookie(config.cookie || undefined)\n },\n\n async checkAuth() {\n const cookie = getAuthCookie()\n if (!cookie) return { isAuthenticated: false }\n\n // 检查 cookie 中是否包含关键的登录标识\n const hasSession = /sessionid_ss=/.test(cookie)\n if (!hasSession) return { isAuthenticated: false, description: 'Cookie 中缺少 sessionid_ss' }\n\n return { isAuthenticated: true, description: '已设置登录 Cookie' }\n },\n}\n","import { Qualities, Recorder } from '@autorecord/manager'\nimport { getRoomInfo, SourceProfile, StreamProfile } from './douyin_api'\nimport * as R from 'ramda'\nimport { getValuesFromArrayLikeFlexSpaceBetween } from './utils'\n\nexport async function getInfo(channelId: string): Promise<{\n living: boolean\n owner: string\n title: string\n roomId: string\n}> {\n const info = await getRoomInfo(channelId)\n\n return {\n living: info.living,\n owner: info.owner,\n title: info.title,\n roomId: info.roomId,\n }\n}\n\nexport async function getStream(\n opts: Pick<Recorder, 'channelId' | 'quality' | 'streamPriorities' | 'sourcePriorities'> & { rejectCache?: boolean },\n) {\n const info = await getRoomInfo(opts.channelId)\n if (!info.living) {\n throw new Error('It must be called getStream when living')\n }\n\n let expectStream: StreamProfile\n const streamsWithPriority = sortAndFilterStreamsByPriority(info.streams, opts.streamPriorities)\n if (streamsWithPriority.length > 0) {\n // 通过优先级来选择对应流\n expectStream = streamsWithPriority[0]\n } else {\n // 通过设置的画质选项来选择对应流\n const flexedStreams = getValuesFromArrayLikeFlexSpaceBetween(info.streams, Qualities.length)\n expectStream = flexedStreams[Qualities.indexOf(opts.quality)]\n }\n\n let expectSource: SourceProfile | null = null\n const sourcesWithPriority = sortAndFilterSourcesByPriority(info.sources, opts.sourcePriorities)\n if (sourcesWithPriority.length > 0) {\n expectSource = sourcesWithPriority[0]\n } else {\n expectSource = info.sources[0]\n }\n\n return {\n ...info,\n currentStream: {\n name: expectStream.desc,\n source: expectSource.name,\n url: expectSource.streamMap[expectStream.key].main.flv,\n },\n }\n}\n\n/**\n * 按提供的流优先级去给流列表排序,并过滤掉不在优先级配置中的流\n */\nfunction sortAndFilterStreamsByPriority(\n streams: StreamProfile[],\n streamPriorities: Recorder['streamPriorities'],\n): (StreamProfile & {\n priority: number\n})[] {\n if (streamPriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n streams\n .map((stream) => ({\n ...stream,\n priority: R.reverse(streamPriorities).indexOf(stream.desc),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n\n/**\n * 按提供的源优先级去给源列表排序,并过滤掉不在优先级配置中的源\n */\nfunction sortAndFilterSourcesByPriority(\n sources: SourceProfile[],\n sourcePriorities: Recorder['sourcePriorities'],\n): (SourceProfile & {\n priority: number\n})[] {\n if (sourcePriorities.length === 0) return []\n\n return R.sortBy(\n R.prop('priority'),\n // 分配优先级属性,数字越大优先级越高\n sources\n .map((source) => ({\n ...source,\n priority: R.reverse(sourcePriorities).indexOf(source.name),\n }))\n .filter(({ priority }) => priority !== -1),\n )\n}\n","import axios from 'axios'\nimport { wrapper } from 'axios-cookiejar-support'\nimport { CookieJar } from 'tough-cookie'\nimport { assert } from './utils'\n\nconst jar = new CookieJar()\nconst requester = wrapper(\n axios.create({\n timeout: 10e3,\n jar,\n // axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,这会让请求发往代理的 host。\n // 于是 set-cookie 的 domain 与请求的 host 无法匹配上,tough-cookie 在检查时会丢弃它,导致 cookie 丢失。\n // 所以这里需要主动禁用代理功能。\n proxy: false,\n }),\n)\n\n// 用户鉴权 cookie 管理(登录后的 cookie,用于获取更高画质等)\nlet authCookie: string | undefined\n\nexport function setAuthCookie(cookie: string | undefined) {\n authCookie = cookie\n}\n\nexport function getAuthCookie(): string | undefined {\n return authCookie\n}\n\n// 将用户鉴权 cookie 注入到请求中(追加到 cookie jar 自动管理的 ttwid 等 cookie 之后)\nrequester.interceptors.request.use((config) => {\n if (authCookie) {\n const existing = config.headers.Cookie\n config.headers.Cookie = existing ? `${existing}; ${authCookie}` : authCookie\n }\n return config\n})\n\nexport async function getRoomInfo(\n webRoomId: string,\n retryOnSpecialCode = true,\n): Promise<{\n living: boolean\n roomId: string\n owner: string\n title: string\n streams: StreamProfile[]\n sources: SourceProfile[]\n}> {\n // 抖音的 'webcast/room/web/enter' api 会需要 ttwid 的 cookie,这个 cookie 是由这个请求的响应头设置的,\n // 所以在这里请求一次自动设置。\n await requester.get('https://live.douyin.com/')\n\n const res = await requester.get<EnterRoomApiResp>('https://live.douyin.com/webcast/room/web/enter/', {\n params: {\n aid: 6383,\n live_id: 1,\n device_platform: 'web',\n language: 'zh-CN',\n enter_from: 'web_live',\n cookie_enabled: 'true',\n screen_width: 1920,\n screen_height: 1080,\n browser_language: 'zh-CN',\n browser_platform: 'MacIntel',\n browser_name: 'Chrome',\n browser_version: '108.0.0.0',\n web_rid: webRoomId,\n // enter_source:,\n 'Room-Enter-User-Login-Ab': 0,\n is_need_double_stream: 'false',\n },\n })\n\n // 无 cookie 时 code 为 10037\n if (res.data.status_code === 10037 && retryOnSpecialCode) {\n // resp 自动设置 cookie\n await requester.get('https://live.douyin.com/favicon.ico')\n return getRoomInfo(webRoomId, false)\n }\n\n assert(\n res.data.status_code === 0,\n `Unexpected resp, code ${res.data.status_code}, msg ${res.data.data}, id ${webRoomId}`,\n )\n\n const data = res.data.data\n const room = data.data[0]\n\n if (room?.stream_url == null) {\n return {\n living: false,\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room?.title ?? data.user.nickname,\n streams: [],\n sources: [],\n }\n }\n\n const {\n options: { qualities },\n stream_data,\n } = room.stream_url.live_core_sdk_data.pull_data\n const streamData = (JSON.parse(stream_data) as StreamData).data\n\n const streams: StreamProfile[] = qualities.map((info) => ({\n desc: info.name,\n key: info.sdk_key,\n bitRate: info.v_bit_rate,\n }))\n\n // 看起来抖音是自动切换 cdn 的,所以这里固定返回一个默认的 source。\n const sources: SourceProfile[] = [\n {\n name: '自动切换线路',\n streamMap: streamData,\n },\n ]\n\n return {\n living: data.room_status === 0,\n // 接口里不会再返回 web room id,只能直接用入参原路返回了。\n roomId: webRoomId,\n owner: data.user.nickname,\n title: room.title,\n streams,\n sources,\n }\n}\n\nexport interface StreamProfile {\n desc: string\n key: string\n bitRate: number\n}\n\nexport interface SourceProfile {\n name: string\n streamMap: StreamData['data']\n}\n\ninterface EnterRoomApiResp {\n data: {\n data: [\n | undefined\n | {\n id_str: string\n status: number\n status_str: string\n title: string\n user_count_str: string\n cover: {\n url_list: string[]\n }\n stream_url?: {\n flv_pull_url: PullURLMap\n default_resolution: string\n hls_pull_url_map: PullURLMap\n hls_pull_url: string\n stream_orientation: number\n live_core_sdk_data: {\n pull_data: {\n options: {\n default_quality: QualityInfo\n qualities: QualityInfo[]\n }\n stream_data: string\n }\n }\n extra: {\n height: number\n width: number\n fps: number\n max_bitrate: number\n min_bitrate: number\n default_bitrate: number\n bitrate_adapt_strategy: number\n anchor_interact_profile: number\n audience_interact_profile: number\n hardware_encode: boolean\n video_profile: number\n h265_enable: boolean\n gop_sec: number\n bframe_enable: boolean\n roi: boolean\n sw_roi: boolean\n bytevc1_enable: boolean\n }\n pull_datas: unknown\n }\n mosaic_status: number\n mosaic_status_str: string\n admin_user_ids: number[]\n admin_user_ids_str: string[]\n owner: UserInfo\n room_auth: unknown\n live_room_mode: number\n stats: {\n total_user_desp: string\n like_count: number\n total_user_str: string\n user_count_str: string\n }\n has_commerce_goods: boolean\n linker_map: {}\n linker_detail: unknown\n room_view_stats: {\n is_hidden: boolean\n display_short: string\n display_middle: string\n display_long: string\n display_value: number\n display_version: number\n incremental: boolean\n display_type: number\n display_short_anchor: string\n display_middle_anchor: string\n display_long_anchor: string\n }\n scene_type_info: unknown\n toolbar_data: unknown\n room_cart: unknown\n },\n ]\n enter_room_id: string\n extra?: {\n digg_color: string\n pay_scores: string\n is_official_channel: boolean\n signature: string\n }\n user: UserInfo\n qrcode_url: string\n enter_mode: number\n room_status: number\n partition_road_map?: unknown\n similar_rooms: unknown[]\n shark_decision_conf: string\n web_stream_url?: unknown\n }\n extra: { now: number }\n status_code: number\n}\n\ntype PullURLMap = Record<string, string>\n\ninterface QualityInfo {\n name: string\n sdk_key: string\n v_codec: string\n resolution: string\n level: number\n v_bit_rate: number\n additional_content: string\n fps: number\n disable: number\n}\n\ninterface UserInfo {\n id_str: string\n sec_uid: string\n nickname: string\n avatar_thumb: {\n url_list: string[]\n }\n follow_info: { follow_status: number; follow_status_str: string }\n}\n\ninterface StreamData {\n common: unknown\n data: Record<\n string,\n {\n main: {\n flv: string\n hls: string\n cmaf: string\n dash: string\n lls: string\n tsl: string\n tile: string\n sdk_params: string\n }\n }\n >\n}\n","import fs from 'fs'\nimport path from 'path'\nimport * as R from 'ramda'\n\n/**\n * 接收 fn ,返回一个和 fn 签名一致的函数 fn'。当已经有一个 fn' 在运行时,再调用\n * fn' 会直接返回运行中 fn' 的 Promise,直到 Promise 结束 pending 状态\n */\nexport function singleton<Fn extends (...args: any) => Promise<any>>(fn: Fn): Fn {\n let latestPromise: Promise<unknown> | null = null\n\n return function (...args) {\n if (latestPromise) return latestPromise\n\n const promise = fn.apply(this, args).finally(() => {\n if (promise === latestPromise) {\n latestPromise = null\n }\n })\n\n latestPromise = promise\n return promise\n } as Fn\n}\n\n/**\n * 从数组中按照特定算法提取一些值(允许同个索引重复提取)。\n * 算法的行为类似 flex 的 space-between。\n *\n * examples:\n * ```\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 1))\n * // [1]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 3))\n * // [1, 4, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 4))\n * // [1, 3, 5, 7]\n * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 11))\n * // [1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7]\n * ```\n */\nexport function getValuesFromArrayLikeFlexSpaceBetween<T>(array: T[], columnCount: number): T[] {\n if (columnCount < 1) return []\n if (columnCount === 1) return [array[0]]\n\n const spacingCount = columnCount - 1\n const spacingLength = array.length / spacingCount\n\n const columns = R.range(1, columnCount + 1)\n const columnValues = columns.map((column, idx, columns) => {\n // 首个和最后的列是特殊的,因为它们不在范围内,而是在两端\n if (idx === 0) {\n return array[0]\n } else if (idx === columns.length - 1) {\n return array[array.length - 1]\n }\n\n const beforeSpacingCount = column - 1\n const colPos = beforeSpacingCount * spacingLength\n\n return array[Math.floor(colPos)]\n })\n\n return columnValues\n}\n\nexport function ensureFolderExist(fileOrFolderPath: string): void {\n const folder = path.dirname(fileOrFolderPath)\n if (!fs.existsSync(folder)) {\n fs.mkdirSync(folder, { recursive: true })\n }\n}\n\nexport function assert(assertion: unknown, msg?: string): asserts assertion {\n if (!assertion) {\n throw new Error(msg)\n }\n}\n\nexport function assertStringType(data: unknown, msg?: string): asserts data is string {\n assert(typeof data === 'string', msg)\n}\n\nexport function assertNumberType(data: unknown, msg?: string): asserts data is number {\n assert(typeof data === 'number', msg)\n}\n\nexport function assertObjectType(data: unknown, msg?: string): asserts data is object {\n assert(typeof data === 'object', msg)\n}\n\nexport function replaceExtName(filePath: string, newExtName: string) {\n return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName)\n}\n"],"mappings":"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","names":["path","columns","R","prop","path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autorecord/douyin-recorder",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "LAR douyin recorder implemention",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",