@bililive-tools/bilibili-recorder
Version:
bililive-tools bilibili recorder implemention
253 lines (252 loc) • 9.4 kB
JavaScript
import { utils } from "@bililive-tools/manager";
import { getRoomInit, getRoomPlayInfo, getStatusInfoByUIDs, getRoomBaseInfo, } from "./bilibili_api.js";
import { assert } from "./utils.js";
const BiliQualities = [30000, 20000, 25000, 15000, 10000, 400, 250, 150, 80];
export async function getStrictStream(roomId, options) {
const res = await getRoomPlayInfo(roomId, options);
const streamInfo = res.playurl_info.playurl.stream
.find(({ protocol_name }) => protocol_name === options.protocol_name)
?.format.find(({ format_name }) => format_name === options.format_name)
?.codec.find(({ codec_name }) => codec_name === options.codec_name);
assert(streamInfo, "没有找到支持的流");
const expectSource = streamInfo.url_info[0];
const url = expectSource.host + streamInfo.base_url + expectSource.extra;
return url;
}
export async function getLiveStatus(channelId) {
const obj = await getRoomBaseInfo(Number(channelId));
const data = obj[Number(channelId)];
if (data) {
const startTime = new Date(data.live_time);
return {
living: data.live_status === 1 && !data.is_encrypted,
liveId: utils.md5(`${channelId}-${startTime?.getTime()}`),
owner: data.uname,
title: data.title,
};
}
const roomInit = await getRoomInit(Number(channelId));
const startTime = new Date(roomInit.live_time * 1000);
return {
living: roomInit.live_status === 1 && !roomInit.encrypted,
liveId: utils.md5(`${roomInit.room_id}-${startTime?.getTime()}`),
...roomInit,
owner: "",
title: "",
};
}
export async function getInfo(channelId) {
const roomInit = await getRoomInit(Number(channelId));
const { [roomInit.uid]: status } = await getStatusInfoByUIDs([roomInit.uid]);
const recordStartTime = new Date();
if (!status) {
// 未获取到直播间信息,可能是加密,尝试换一个接口
const data = await getRoomBaseInfo(Number(channelId));
const status = data[channelId];
const liveStartTime = new Date(status.live_time);
return {
uid: roomInit.uid,
living: roomInit.live_status === 1 && !roomInit.encrypted,
owner: status.uname,
title: status.title,
liveStartTime: liveStartTime,
avatar: "",
cover: status.cover,
roomId: roomInit.room_id,
liveId: utils.md5(`${roomInit.room_id}-${liveStartTime?.getTime()}`),
recordStartTime,
area: status.parent_area_name,
};
}
const liveStartTime = new Date(status.live_time * 1000);
return {
uid: roomInit.uid,
living: roomInit.live_status === 1 && !roomInit.encrypted,
owner: status.uname,
title: status.title,
avatar: status.face,
cover: status.cover_from_user,
roomId: roomInit.room_id,
liveStartTime: liveStartTime,
liveId: utils.md5(`${roomInit.room_id}-${liveStartTime.getTime()}`),
recordStartTime,
area: status.area_v2_parent_name,
};
}
async function getLiveInfo(roomIdOrShortId, opts) {
const res = await getRoomPlayInfo(roomIdOrShortId, opts);
// https://github.com/FFmpeg/FFmpeg/commit/b76053d8bf322b197a9d07bd27bbdad14fd5bc15
let conditons = [
{
protocol_name: "http_stream",
format_name: "flv",
codec_name: "avc",
sort: 9,
},
{
protocol_name: "http_hls",
format_name: "fmp4",
codec_name: "avc",
sort: 8,
},
{
protocol_name: "http_hls",
format_name: "ts",
codec_name: "avc",
sort: 7,
},
{
protocol_name: "http_stream",
format_name: "flv",
codec_name: "hevc",
sort: 6,
},
{
protocol_name: "http_hls",
format_name: "fmp4",
codec_name: "hevc",
sort: 5,
},
{
protocol_name: "http_hls",
format_name: "ts",
codec_name: "hevc",
sort: 4,
},
];
// 处理formatName
if (opts.formatName === "flv_only") {
conditons = conditons.filter((item) => item.format_name === "flv");
}
else if (opts.formatName === "hls_only") {
conditons = conditons.filter((item) => item.format_name === "ts");
}
else if (opts.formatName === "fmp4_only") {
conditons = conditons.filter((item) => item.format_name === "fmp4");
}
else if (opts.formatName === "hls") {
// hls优先,avc比hevc优先
conditons.forEach((item) => {
if (item.format_name === "ts") {
item.sort += 10;
}
});
conditons = conditons.sort((a, b) => b.sort - a.sort);
}
else if (opts.formatName === "fmp4") {
// fmp4优先,将format_name=fmp4的放在前面,avc比hevc优先
conditons.forEach((item) => {
if (item.format_name === "fmp4") {
item.sort += 10;
}
});
conditons = conditons.sort((a, b) => b.sort - a.sort);
}
// 处理codecName
if (opts.codecName === "avc_only") {
conditons = conditons.filter((item) => item.codec_name === "avc");
}
else if (opts.codecName === "hevc_only") {
conditons = conditons.filter((item) => item.codec_name === "hevc");
}
else if (opts.codecName === "hevc") {
conditons.forEach((item) => {
if (item.codec_name === "hevc") {
item.sort += 100;
}
});
conditons = conditons.sort((a, b) => b.sort - a.sort);
}
// console.log("conditons", opts.codecName, conditons);
let streamInfo;
let streamOptions;
// console.log("conditons", JSON.stringify(res.playurl_info.playurl.stream, null, 2));
for (const condition of conditons) {
const streamList = res.playurl_info.playurl.stream
.find(({ protocol_name }) => protocol_name === condition.protocol_name)
?.format.find(({ format_name }) => format_name === condition.format_name)
?.codec.filter(({ codec_name }) => codec_name === condition.codec_name);
if (streamList && streamList.length > 1) {
// 由于直播姬直推hevc时,指定qn,服务端仍会返回其他画质的流,这里需要指定找一下流
streamInfo = streamList.find((item) => item.current_qn === opts.qn);
}
if (!streamInfo) {
streamInfo = streamList?.[0];
}
if (streamInfo) {
streamOptions = {
...condition,
qn: streamInfo.current_qn,
};
break;
}
}
// console.log(
// "streamOptions",
// streamOptions,
// JSON.stringify(res.playurl_info.playurl.stream, null, 2),
// );
assert(streamInfo, "没有找到支持的流");
const streams = streamInfo.accept_qn.map((qn) => {
const qnDesc = res.playurl_info.playurl.g_qn_desc.find((item) => item.qn === qn);
assert(qnDesc, "Unexpected getRoomPlayInfo resp");
return qnDesc;
});
const sources = streamInfo.url_info.map((info, idx) => ({
...info,
name: idx === 0 ? "主线" : `备线 ${idx}`,
}));
const currentStreamName = res.playurl_info.playurl.g_qn_desc.find((item) => item.qn === streamInfo.current_qn)?.desc;
assert(currentStreamName, "Unexpected getRoomPlayInfo resp");
return {
...streamInfo,
streams,
sources,
name: currentStreamName,
streamOptions,
};
}
export async function getStream(opts) {
const roomId = Number(opts.channelId);
const roomInit = await getRoomInit(roomId);
if (roomInit.live_status !== 1) {
throw new Error("It must be called getStream when living");
}
const qn = BiliQualities.includes(opts.quality) ? opts.quality : 10000;
let liveInfo = await getLiveInfo(roomId, {
qn: qn,
cookie: opts.cookie,
formatName: opts.formatName,
codecName: opts.codecName,
onlyAudio: opts.onlyAudio,
});
// console.log(JSON.stringify(liveInfo, null, 2));
if (liveInfo.current_qn !== qn && opts.strictQuality) {
throw new Error("Can not get expect quality because of strictQuality");
}
if ((liveInfo?.accept_qn ?? []).length !== 0 && liveInfo.current_qn !== qn) {
// 当前流不是预期的流,需要切换。
const acceptQn = liveInfo.accept_qn[0];
liveInfo = await getLiveInfo(roomId, {
qn: acceptQn,
cookie: opts.cookie,
formatName: opts.formatName,
codecName: opts.codecName,
onlyAudio: opts.onlyAudio,
});
}
let expectSource = liveInfo.sources[0];
if (!expectSource) {
throw new Error("Can not get expect source");
}
const host = opts.customHost ? `https://${opts.customHost}` : expectSource.host;
const url = host + liveInfo.base_url + expectSource.extra;
return {
...liveInfo,
currentStream: {
name: liveInfo.name,
source: expectSource.name,
url: url,
},
};
}