api/stream: split types.js into proxy.js and ffmpeg.js
This commit is contained in:
212
api/src/stream/ffmpeg.js
Normal file
212
api/src/stream/ffmpeg.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import ffmpeg from "ffmpeg-static";
|
||||
import { spawn } from "child_process";
|
||||
import { create as contentDisposition } from "content-disposition-header";
|
||||
|
||||
import { env } from "../config.js";
|
||||
import { destroyInternalStream } from "./manage.js";
|
||||
import { hlsExceptions } from "../processing/service-config.js";
|
||||
import { closeResponse, pipe, estimateTunnelLength, estimateAudioMultiplier } from "./shared.js";
|
||||
|
||||
const metadataTags = [
|
||||
"album",
|
||||
"composer",
|
||||
"genre",
|
||||
"copyright",
|
||||
"title",
|
||||
"artist",
|
||||
"album_artist",
|
||||
"track",
|
||||
"date",
|
||||
"sublanguage"
|
||||
];
|
||||
|
||||
const convertMetadataToFFmpeg = (metadata) => {
|
||||
const args = [];
|
||||
|
||||
for (const [ name, value ] of Object.entries(metadata)) {
|
||||
if (metadataTags.includes(name)) {
|
||||
if (name === "sublanguage") {
|
||||
args.push('-metadata:s:s:0', `language=${value}`);
|
||||
continue;
|
||||
}
|
||||
args.push('-metadata', `${name}=${value.replace(/[\u0000-\u0009]/g, "")}`); // skipcq: JS-0004
|
||||
} else {
|
||||
throw `${name} metadata tag is not supported.`;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
const killProcess = (p) => {
|
||||
p?.kill('SIGTERM'); // ask the process to terminate itself gracefully
|
||||
|
||||
setTimeout(() => {
|
||||
if (p?.exitCode === null)
|
||||
p?.kill('SIGKILL'); // brutally murder the process if it didn't quit
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const getCommand = (args) => {
|
||||
if (typeof env.processingPriority === 'number' && !isNaN(env.processingPriority)) {
|
||||
return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]]
|
||||
}
|
||||
return [ffmpeg, args]
|
||||
}
|
||||
|
||||
const render = async (res, streamInfo, ffargs, multiplier) => {
|
||||
let process;
|
||||
const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls];
|
||||
const shutdown = () => (
|
||||
killProcess(process),
|
||||
closeResponse(res),
|
||||
urls.map(destroyInternalStream)
|
||||
);
|
||||
|
||||
try {
|
||||
// if the streamInfo.urls is an array but doesn't have 2 urls,
|
||||
// then something went wrong
|
||||
if (Array.isArray(streamInfo.urls) && streamInfo.urls.length !== 2) {
|
||||
return shutdown();
|
||||
}
|
||||
|
||||
const args = [
|
||||
'-loglevel', '-8',
|
||||
...ffargs,
|
||||
];
|
||||
|
||||
process = spawn(...getCommand(args), {
|
||||
windowsHide: true,
|
||||
stdio: [
|
||||
'inherit', 'inherit', 'inherit',
|
||||
'pipe'
|
||||
],
|
||||
});
|
||||
|
||||
const [,,, muxOutput] = process.stdio;
|
||||
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename));
|
||||
res.setHeader('Estimated-Content-Length', await estimateTunnelLength(streamInfo, multiplier));
|
||||
|
||||
pipe(muxOutput, res, shutdown);
|
||||
|
||||
process.on('close', shutdown);
|
||||
res.on('finish', shutdown);
|
||||
} catch {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
const remux = async (streamInfo, res) => {
|
||||
const format = streamInfo.filename.split('.').pop();
|
||||
const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls];
|
||||
const args = [...urls.flatMap(url => ['-i', url])];
|
||||
|
||||
if (streamInfo.subtitles) {
|
||||
args.push(
|
||||
'-i', streamInfo.subtitles,
|
||||
'-map', `${urls.length}:s`,
|
||||
'-c:s', format === 'mp4' ? 'mov_text' : 'webvtt',
|
||||
);
|
||||
}
|
||||
|
||||
if (urls.length === 2) {
|
||||
args.push(
|
||||
'-map', '0:v',
|
||||
'-map', '1:a',
|
||||
'-c:v', 'copy',
|
||||
'-c:a', 'copy'
|
||||
);
|
||||
} else {
|
||||
args.push('-c:v', 'copy');
|
||||
|
||||
if (streamInfo.type === 'mute') {
|
||||
args.push('-an');
|
||||
} else {
|
||||
args.push('-c:a', 'copy');
|
||||
}
|
||||
}
|
||||
|
||||
if (format === 'mp4') {
|
||||
args.push('-movflags', 'faststart+frag_keyframe+empty_moov');
|
||||
}
|
||||
|
||||
if (streamInfo.type !== 'mute' && streamInfo.isHLS && hlsExceptions.includes(streamInfo.service)) {
|
||||
if (streamInfo.service === 'youtube' && format === 'webm') {
|
||||
args.push('-c:a', 'libopus');
|
||||
} else {
|
||||
args.push('-c:a', 'aac', '-bsf:a', 'aac_adtstoasc');
|
||||
}
|
||||
}
|
||||
|
||||
if (streamInfo.metadata) {
|
||||
args.push(...convertMetadataToFFmpeg(streamInfo.metadata));
|
||||
}
|
||||
|
||||
args.push('-f', format === 'mkv' ? 'matroska' : format, 'pipe:3');
|
||||
|
||||
await render(res, streamInfo, args);
|
||||
}
|
||||
|
||||
const convertAudio = async (streamInfo, res) => {
|
||||
const args = [
|
||||
'-i', streamInfo.urls,
|
||||
'-vn',
|
||||
...(streamInfo.audioCopy ? ['-c:a', 'copy'] : ['-b:a', `${streamInfo.audioBitrate}k`]),
|
||||
];
|
||||
|
||||
if (streamInfo.audioFormat === 'mp3' && streamInfo.audioBitrate === '8') {
|
||||
args.push('-ar', '12000');
|
||||
}
|
||||
|
||||
if (streamInfo.audioFormat === 'opus') {
|
||||
args.push('-vbr', 'off');
|
||||
}
|
||||
|
||||
if (streamInfo.audioFormat === 'mp4a') {
|
||||
args.push('-movflags', 'frag_keyframe+empty_moov');
|
||||
}
|
||||
|
||||
if (streamInfo.metadata) {
|
||||
args.push(...convertMetadataToFFmpeg(streamInfo.metadata));
|
||||
}
|
||||
|
||||
args.push(
|
||||
'-f',
|
||||
streamInfo.audioFormat === 'm4a' ? 'ipod' : streamInfo.audioFormat,
|
||||
'pipe:3'
|
||||
);
|
||||
|
||||
await render(
|
||||
res,
|
||||
streamInfo,
|
||||
args,
|
||||
estimateAudioMultiplier(streamInfo) * 1.1,
|
||||
);
|
||||
}
|
||||
|
||||
const convertGif = async (streamInfo, res) => {
|
||||
const args = [
|
||||
'-i', streamInfo.urls,
|
||||
|
||||
'-vf',
|
||||
'scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse',
|
||||
'-loop', '0',
|
||||
|
||||
'-f', "gif", 'pipe:3',
|
||||
];
|
||||
|
||||
await render(
|
||||
res,
|
||||
streamInfo,
|
||||
args,
|
||||
60,
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
remux,
|
||||
convertAudio,
|
||||
convertGif,
|
||||
}
|
||||
Reference in New Issue
Block a user