moved to new repo

This commit is contained in:
wukko
2022-07-09 00:17:56 +06:00
committed by GitHub
parent 1decab4daf
commit 94acf10e9e
62 changed files with 2187 additions and 4 deletions

45
modules/stream/manage.js Normal file
View File

@@ -0,0 +1,45 @@
import NodeCache from "node-cache";
import { UUID, encrypt } from "../sub/crypto.js";
import { streamLifespan } from "../config.js";
const streamCache = new NodeCache({ stdTTL: streamLifespan, checkperiod: 120 });
export function createStream(obj) {
let streamUUID = UUID(),
exp = Math.floor(new Date().getTime()) + streamLifespan,
ghmac = encrypt(`${streamUUID},${obj.url},${obj.ip},${exp}`, obj.salt),
iphmac = encrypt(`${obj.ip}`, obj.salt);
streamCache.set(streamUUID, {
id: streamUUID,
service: obj.service,
type: obj.type,
urls: obj.urls,
filename: obj.filename,
hmac: ghmac,
ip: iphmac,
exp: exp,
isAudioOnly: obj.isAudioOnly ? true : false,
time: obj.time
});
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;
}
export function verifyStream(ip, id, hmac, exp, salt) {
try {
let streamInfo = streamCache.get(id);
if (streamInfo) {
let ghmac = encrypt(`${id},${streamInfo.url},${ip},${exp}`, salt);
if (hmac == ghmac && encrypt(`${ip}`, salt) == streamInfo.ip && ghmac == streamInfo.hmac && exp > Math.floor(new Date().getTime()) && exp == streamInfo.exp) {
return streamInfo;
} else {
return { error: 'Unauthorized', status: 401 };
}
} else {
return { error: 'this stream token does not exist', status: 400 };
}
} catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } };
}
}

View File

@@ -0,0 +1,32 @@
import { services, quality as mq } from "../config.js";
function closest(goal, array) {
return array.sort().reduce(function(prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
});
}
export default function(service, quality, maxQuality) {
if (quality == "max") {
return maxQuality
}
quality = parseInt(mq[quality])
maxQuality = parseInt(maxQuality)
if (quality >= maxQuality || quality == maxQuality) {
return maxQuality
}
if (quality < maxQuality) {
if (services[service]["quality"][quality]) {
return quality
} else {
let s = Object.keys(services[service]["quality_match"]).filter((q) => {
if (q <= quality) {
return true
}
})
return closest(quality, s)
}
}
}

27
modules/stream/stream.js Normal file
View File

@@ -0,0 +1,27 @@
import { apiJSON } from "../sub/api-helper.js";
import { verifyStream } from "./manage.js";
import { streamAudioOnly, streamDefault, streamLiveRender } from "./types.js";
export default function(res, ip, id, hmac, exp) {
try {
let streamInfo = verifyStream(ip, id, hmac, exp, process.env.streamSalt);
if (!streamInfo.error) {
if (streamInfo.isAudioOnly) {
streamAudioOnly(streamInfo, res);
} else {
switch (streamInfo.type) {
case "render":
streamLiveRender(streamInfo, res);
break;
default:
streamDefault(streamInfo, res);
break;
}
}
} else {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
}
} catch (e) {
internalError(res)
}
}

131
modules/stream/types.js Normal file
View File

@@ -0,0 +1,131 @@
import { spawn } from "child_process";
import ffmpeg from "ffmpeg-static";
import got from "got";
import { genericUserAgent } from "../config.js";
import { msToTime } from "../sub/api-helper.js";
import { internalError } from "../sub/errors.js";
import loc from "../sub/loc.js";
export async function streamDefault(streamInfo, res) {
try {
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.filename}"`);
const stream = got.get(streamInfo.urls, {
headers: {
"user-agent": genericUserAgent
},
isStream: true
});
stream.pipe(res).on('error', (err) => {
internalError(res);
throw Error("File stream pipe error.");
});
stream.on('error', (err) => {
internalError(res);
throw Error("File stream error.")
});
} catch (e) {
internalError(res);
}
}
export async function streamLiveRender(streamInfo, res) {
try {
if (streamInfo.urls.length == 2) {
let headers = {};
if (streamInfo.service == "bilibili") {
headers = { "user-agent": genericUserAgent };
}
const audio = got.get(streamInfo.urls[1], { isStream: true, headers: headers });
const video = got.get(streamInfo.urls[0], { isStream: true, headers: headers });
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', 'pipe:3',
'-i', 'pipe:4',
'-map', '0:a',
'-map', '1:v',
'-c:v', 'copy',
'-c:a', 'copy',
];
if (format == 'mp4') {
args.push('-movflags', 'frag_keyframe+empty_moov');
if (streamInfo.service == "youtube") {
args.push('-t', msToTime(streamInfo.time));
}
} else if (format == 'webm') {
args.push('-t', msToTime(streamInfo.time));
}
args.push('-f', format, 'pipe:5');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe', 'pipe', 'pipe'
],
});
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
audio.on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
video.on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[5].pipe(res);
audio.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
video.pipe(ffmpegProcess.stdio[4]).on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
} else {
res.status(400).json({ status: "error", text: loc('en', 'apiError', 'corruptedVideo') });
}
} catch (e) {
internalError(res);
}
}
export async function streamAudioOnly(streamInfo, res) {
try {
let headers = {};
if (streamInfo.service == "bilibili") {
headers = { "user-agent": genericUserAgent };
}
const audio = got.get(streamInfo.urls[0], { isStream: true, headers: headers });
const ffmpegProcess = spawn(ffmpeg, [
'-loglevel', '-8',
'-i', 'pipe:3',
'-vn',
'-c:a', 'copy',
'-f', `${streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]}`,
'pipe:4',
], {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe', 'pipe'
],
});
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
audio.on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[4].pipe(res);
audio.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
ffmpegProcess.kill();
internalError(res);
});
} catch (e) {
internalError(res);
}
}