uyem
Version:
WebRTC client-server SFU application
459 lines • 19.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const child_process_1 = require("child_process");
const date_fns_1 = require("date-fns");
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
const isDev = process.env.FFMPEG_DEV === 'true';
if (isDev) {
process.env.LOG_LEVEL = '1';
process.env.NODE_ENV = 'development';
}
// eslint-disable-next-line import/first
const lib_1 = require("./lib");
// eslint-disable-next-line import/first
const constants_1 = require("./constants");
// eslint-disable-next-line import/first
const interfaces_1 = require("../types/interfaces");
class FFmpeg {
dirPath;
time = 0;
chunks;
episodes = [];
roomId;
border = 5;
videoWidth = 1280;
videoHeight = 720;
mapLength = 6;
delimiter = '_';
forceOption = '-y';
inputOption = '-i';
filterComplexOption = '-filter_complex';
mapOption = '-map';
eol = ';';
backgroundInput = '0:v';
backgroundImagePath;
// eslint-disable-next-line class-methods-use-this
vstack = ({ inputs }) => `vstack=inputs=${inputs}`;
// eslint-disable-next-line class-methods-use-this
hstack = ({ inputs }) => `hstack=inputs=${inputs}`;
// eslint-disable-next-line class-methods-use-this
amerge = ({ count }) => `amerge=inputs=${count}`;
overlay = 'overlay=(W-w)/2:(H-h)/2';
// eslint-disable-next-line class-methods-use-this
pad = ({ x, y }) => `format=rgba,pad=width=iw+${x}:height=ih+${y}:x=iw/2:y=ih/2:color=#00000000`;
// eslint-disable-next-line class-methods-use-this
concat = ({ n, v, a }) => `concat=n=${n}:v=${v}:a=${a}`;
// eslint-disable-next-line class-methods-use-this
trim = ({ start, duration }) => `trim=start=${start}:duration=${duration},setpts=PTS-STARTPTS`;
// eslint-disable-next-line class-methods-use-this
atrim = ({ start, duration }) => `atrim=start=${start}:duration=${duration},asetpts=PTS-STARTPTS`;
// eslint-disable-next-line class-methods-use-this
scale = ({ w, h }) => `scale=w=${w}:h=${h}:force_original_aspect_ratio=decrease`;
// eslint-disable-next-line class-methods-use-this
color = ({ w, h }) => `color=c=black:s=${w}x${h}`;
constructor({ dirPath, dir, roomId, backgroundImagePath, }) {
this.dirPath = dirPath;
this.backgroundImagePath = backgroundImagePath;
this.roomId = roomId;
this.chunks = (0, interfaces_1.createVideoChunks)({ dir, dirPath, indexShift: backgroundImagePath !== null });
}
getFilterComplexArgument({ args, value, map, }) {
return `${args}${value}${map}${this.eol}`;
}
async createVideo({ loading }) {
const inputArgs = this.createInputArguments();
const filterComplexArgs = this.createFilterComplexArguments();
const args = inputArgs.concat(filterComplexArgs);
const videosDirPath = this.getVideosDirPath();
const roomDir = (0, lib_1.getRoomDirPath)({ videosDirPath, roomId: this.roomId });
if (!fs_1.default.existsSync(roomDir)) {
fs_1.default.mkdirSync(roomDir);
}
const name = this.getVideoName({ videosDirPath });
const src = path_1.default.resolve(roomDir, `./${name}`);
args.push(src);
// process.exit(0);
const errorCode = await this.runFFmpegCommand(args, loading);
return {
errorCode,
name,
time: this.time,
};
}
getVideoName = ({ videosDirPath }) => `${this.dirPath
.replace(videosDirPath, '')
.replace(new RegExp(`^${this.roomId}${this.delimiter}`), '')}${interfaces_1.EXT_WEBM}`;
getVideosDirPath = () => this.dirPath.replace(/[a-z0-9A-Z-_]+$/, '');
createInputArguments() {
let args = [this.forceOption];
if (this.backgroundImagePath) {
args = args.concat([this.inputOption, this.backgroundImagePath]);
}
this.chunks.forEach((item) => {
args.push(this.inputOption);
args.push(item.fullPath);
});
return args;
}
// eslint-disable-next-line class-methods-use-this
createMapArg(map) {
return `[${map}]`;
}
getArg({ chunk, dest }) {
const map = dest === 'a' ? chunk.mapA : chunk.map;
return map !== '' ? this.createMapArg(map) : this.createMapArg(`${chunk.index}:${dest}`);
}
// eslint-disable-next-line class-methods-use-this
joinFilterComplexArgs(args) {
return `"${args.join('').replace(/;$/, '')}"`;
}
createFilterComplexArguments() {
const args = [];
const _episodes = (0, interfaces_1.createEpisodes)({ chunks: this.chunks });
let withAudio = false;
this.episodes = _episodes.map((episode, index) => {
console.log(episode);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const episodeCopy = { ...episode };
// Set start and duration
let chunks = episode.chunks.map((chunk) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chunkCopy = { ...chunk };
if (chunk.video) {
chunkCopy.video = true;
if (!episode.video) {
episodeCopy.video = true;
}
}
if (chunk.audio) {
chunkCopy.audio = true;
withAudio = true;
if (!episode.audio) {
episodeCopy.audio = true;
}
}
if (chunk.start !== episode.start || chunk.end !== episode.end) {
const start = index === 0 ? 0 : Math.abs(chunk.start - episode.start);
const duration = episode.end - episode.start;
if (chunk.video) {
chunkCopy.map = (0, lib_1.createRandHash)(this.mapLength);
args.push(this.getFilterComplexArgument({
args: this.createMapArg(`${chunk.index}:v`),
value: this.trim({ start, duration }),
map: this.createMapArg(chunkCopy.map),
}));
}
if (chunk.audio) {
chunkCopy.mapA = (0, lib_1.createRandHash)(this.mapLength);
args.push(this.getFilterComplexArgument({
args: this.createMapArg(`${chunk.index}:a`),
value: this.atrim({ start, duration }),
map: this.createMapArg(chunkCopy.mapA),
}));
}
}
else {
chunkCopy.map = '';
chunkCopy.mapA = '';
}
return chunkCopy;
});
// Set audio channels
const mapA = (0, lib_1.createRandHash)(this.mapLength);
let arg = '';
let audioCount = 0;
chunks = chunks.map((chunk) => {
const chunkCopy = { ...chunk };
if (chunk.audio) {
audioCount++;
arg += this.getArg({ chunk, dest: 'a' });
episodeCopy.mapA = mapA;
return chunkCopy;
}
return chunk;
});
if (audioCount !== 0) {
args.push(this.getFilterComplexArgument({
args: arg,
value: this.amerge({ count: audioCount }),
map: this.createMapArg(mapA),
}));
}
// Set video paddings
const { videoCount } = (0, interfaces_1.getCountVideos)(episode.chunks);
const { x, y, shiftX, shiftY } = (0, interfaces_1.getVideoShifts)({
videoCount,
chunks,
videoHeight: this.videoHeight,
videoWidth: this.videoWidth,
border: this.border,
});
chunks = chunks.map((chunk) => {
if (chunk.video) {
const chunkCopy = { ...chunk };
// Scale if not included in size
let map = (0, lib_1.createRandHash)(this.mapLength);
if (shiftX > shiftY) {
const newWidth = chunk.width - shiftX;
args.push(this.getFilterComplexArgument({
args: this.getArg({ chunk: chunkCopy, dest: 'v' }),
value: this.scale({ w: newWidth, h: -1 }),
map: this.createMapArg(map),
}));
chunkCopy.map = map;
}
else if (shiftY) {
const newHeight = chunk.height - shiftY;
args.push(this.getFilterComplexArgument({
args: this.getArg({ chunk: chunkCopy, dest: 'v' }),
value: this.scale({ w: -1, h: newHeight }),
map: this.createMapArg(map),
}));
chunkCopy.map = map;
}
map = (0, lib_1.createRandHash)(this.mapLength);
args.push(this.getFilterComplexArgument({
args: this.getArg({ chunk: chunkCopy, dest: 'v' }),
value: this.pad({ x, y }),
map: this.createMapArg(map),
}));
chunkCopy.map = map;
return chunkCopy;
}
return chunk;
});
// Set video stacks
let map = (0, lib_1.createRandHash)(this.mapLength);
if (videoCount === 2 || videoCount === 3) {
arg = '';
chunks = chunks.map((chunk) => {
if (chunk.video) {
const chunkCopy = { ...chunk };
arg += this.getArg({ chunk, dest: 'v' });
chunkCopy.map = map;
return chunkCopy;
}
return chunk;
});
args.push(this.getFilterComplexArgument({
args: arg,
value: this.hstack({ inputs: videoCount }),
map: this.createMapArg(map),
}));
}
else if (videoCount === 4) {
arg = '';
let i = 0;
chunks = chunks.map((chunk) => {
if (chunk.video) {
i++;
if (i <= 2) {
const chunkCopy = { ...chunk };
arg += this.getArg({ chunk, dest: 'v' });
chunkCopy.map = map;
return chunkCopy;
}
}
return chunk;
});
args.push(this.getFilterComplexArgument({
args: arg,
value: this.hstack({ inputs: 2 }),
map: this.createMapArg(map),
}));
map = (0, lib_1.createRandHash)(this.mapLength);
arg = '';
i = 0;
chunks = chunks.map((chunk) => {
if (chunk.video) {
i++;
if (i > 2) {
const chunkCopy = { ...chunk };
arg += this.getArg({ chunk, dest: 'v' });
chunkCopy.map = map;
return chunkCopy;
}
}
return chunk;
});
args.push(this.getFilterComplexArgument({
args: arg,
value: this.hstack({ inputs: 2 }),
map: this.createMapArg(map),
}));
map = (0, lib_1.createRandHash)(this.mapLength);
episodeCopy.chunks = chunks;
const uMaps = this.getUniqueMaps(episodeCopy);
arg = '';
uMaps.forEach((uMap) => {
arg += this.createMapArg(uMap);
});
chunks = chunks.map((chunk) => {
const chunkCopy = { ...chunk };
chunkCopy.map = map;
return chunkCopy;
});
args.push(this.getFilterComplexArgument({
args: arg,
value: this.vstack({ inputs: 2 }),
map: this.createMapArg(map),
}));
}
episodeCopy.chunks = chunks;
return episodeCopy;
});
// Set overlay
this.episodes = this.episodes.map((episode) => {
const episdeCopy = { ...episode };
const uMaps = this.getUniqueMaps(episode);
const map = (0, lib_1.createRandHash)(this.mapLength);
const emptyMap = (0, lib_1.createRandHash)(this.mapLength);
const isEmpty = uMaps.length === 1 && uMaps[0] === '';
if (isEmpty) {
args.push(this.getFilterComplexArgument({
args: this.createMapArg(`${episode.chunks[0].index}:v`),
value: this.scale({ w: constants_1.RECORD_WIDTH_DEFAULT, h: constants_1.RECORD_HEIGHT_DEFAULT }),
map: this.createMapArg(emptyMap),
}));
}
const trimMap = (0, lib_1.createRandHash)(this.mapLength);
if (!this.backgroundImagePath) {
const colorMap = (0, lib_1.createRandHash)(this.mapLength);
args.push(this.getFilterComplexArgument({
args: '',
value: this.color({ w: this.videoWidth, h: this.videoHeight }),
map: this.createMapArg(colorMap),
}));
const duration = episode.end - episode.start;
args.push(this.getFilterComplexArgument({
args: this.createMapArg(colorMap),
value: this.trim({ start: episode.start, duration }),
map: this.createMapArg(trimMap),
}));
}
uMaps.forEach((uMap) => {
args.push(this.getFilterComplexArgument({
args: `${this.createMapArg(this.backgroundImagePath ? this.backgroundInput : trimMap)}${this.createMapArg(isEmpty ? emptyMap : uMap)}`,
value: this.overlay,
map: this.createMapArg(map),
}));
});
episdeCopy.map = map;
return episdeCopy;
});
// Set concat
const concatMap = (0, lib_1.createRandHash)(this.mapLength);
const concatMapA = (0, lib_1.createRandHash)(this.mapLength);
let arg = '';
this.episodes = this.episodes.map((episode) => {
const episodeCopy = { ...episode };
arg += `${this.createMapArg(episode.map)}${episode.mapA ? this.createMapArg(episode.mapA) : ''}`;
episodeCopy.map = concatMap;
episodeCopy.mapA = concatMapA;
return episodeCopy;
});
args.push(this.getFilterComplexArgument({
args: arg,
value: this.concat({
n: this.episodes.length,
v: 1,
a: withAudio ? 1 : 0,
}),
map: `${this.createMapArg(concatMap)}${withAudio ? this.createMapArg(concatMapA) : ''}`,
}));
const _args = [this.filterComplexOption, this.joinFilterComplexArgs(args)];
return _args.concat(this.getMap(withAudio));
}
// eslint-disable-next-line class-methods-use-this
getUniqueMaps(episode) {
const uMaps = [];
episode.chunks.forEach((_item) => {
const { map } = _item;
if (uMaps.indexOf(map) === -1) {
uMaps.push(map);
}
});
return uMaps;
}
getMap(withAudio) {
const maps = [];
this.episodes.forEach((item) => {
if (item.map) {
const map = `"${this.createMapArg(item.map)}"`;
if (maps.indexOf(map) === -1) {
maps.push(this.mapOption);
maps.push(map);
}
}
if (item.mapA && withAudio) {
const mapA = `"${this.createMapArg(item.mapA)}"`;
if (maps.indexOf(mapA) === -1) {
maps.push(this.mapOption);
maps.push(mapA);
}
}
});
return maps;
}
parseTime(data) {
const time = data.match(/time=\d{2}:\d{2}:\d{2}/);
let result = null;
if (time) {
const _time = time[0].replace('time=', '');
const t = _time.split(':');
const d = (0, date_fns_1.differenceInSeconds)(new Date(0, 0, 0, parseInt(t[0], 10), parseInt(t[1], 10), parseInt(t[2], 10)), new Date(0, 0, 0, 0, 0, 0));
const procents = Math.ceil(d / ((this.time - 1) / 100));
result = procents < 100 ? procents : 100;
}
return result;
}
async runFFmpegCommand(args, loading) {
return new Promise((resolve) => {
const command = `${ffmpeg_static_1.default} ${args.join(' ')}`;
(0, lib_1.log)('info', 'Run command', command);
const fC = (0, child_process_1.exec)(command, { env: process.env }, (error) => {
if (error) {
(0, lib_1.log)('error', 'FFmpeg command error', error);
resolve(error.code || 0);
}
});
fC.stdout?.on('data', (d) => {
(0, lib_1.log)('log', 'stdout', d);
});
fC.stderr?.on('data', (d) => {
(0, lib_1.log)('info', 'stderr', d);
const time = this.parseTime(d);
if (time) {
loading(time);
}
});
fC.on('exit', (code) => {
(0, lib_1.log)('info', 'FFmpeg command exit with code', code);
resolve(code || 0);
});
});
}
}
exports.default = FFmpeg;
if (isDev) {
const roomId = '1673340519949';
const dirPath = '/home/kol/Projects/werift-sfu-react/packages/server/rec/videos/1673340519949_1673950253377';
new FFmpeg({
dirPath,
dir: fs_1.default.readdirSync(dirPath),
roomId,
backgroundImagePath: null,
//backgroundImagePath: '/home/kol/Projects/werift-sfu-react/tmp/1png.png',
}).createVideo({
loading: (time) => {
// eslint-disable-next-line no-console
console.log(time);
},
});
}
//# sourceMappingURL=ffmpeg.js.map