fp/packages/capture/src/record.js

118 lines
3.7 KiB
JavaScript

import { join } from 'path';
import { spawn } from 'child_process';
import fs from 'node:fs';
export const getFilename = (appContext, roomName) => {
const name = `${roomName}_${new Date().toISOString()}.ts`
return join(appContext.env.FUTUREPORN_WORKDIR, 'recordings', name);
}
export const assertDirectory = (directoryPath) => {
if (fs.statSync(directoryPath, { throwIfNoEntry: false }) === undefined) fs.mkdirSync(directoryPath);
}
export const checkFFmpeg = async (appContext) => {
return new Promise((resolve, reject) => {
const childProcess = spawn('ffmpeg', ['-version']);
childProcess.on('error', (err) => {
appContext.logger.log({
level: 'error',
message: `ffmpeg -version failed, which likely means ffmpeg is not installed or not on $PATH`,
});
throw new Error('ffmpeg is missing')
});
childProcess.on('exit', (code) => {
if (code !== 0) reject(`'ffmpeg -version' exited with code ${code}`)
if (code === 0) {
appContext.logger.log({ level: 'info', message: `ffmpeg PRESENT.` });
resolve()
}
});
})
};
export const assertDependencyDirectory = (appContext) => {
// Extract the directory path from the filename
const directoryPath = join(appContext.env.FUTUREPORN_WORKDIR, 'recordings');
console.log(`asserting ${directoryPath} exists`)
// Check if the directory exists, and create it if it doesn't
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, { recursive: true });
console.log(`Created directory: ${directoryPath}`);
}
}
export const record = async (appContext, playlistUrl, roomName) => {
if (appContext === undefined) throw new Error('appContext undef');
if (playlistUrl === undefined) throw new Error('playlistUrl undef');
if (roomName === undefined) throw new Error('roomName undef');
const filename = getFilename(appContext, roomName);
console.log(`downloading to ${filename}`)
// example: `ffmpeg -headers "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
// -i ${chunkPlaylist}
// -c:v copy
// -c:a copy
// -movflags faststart
// -y
// -f mpegts
// ./my-recording.ts`
const ffmpegProcess = spawn('ffmpeg', [
'-headers', `"User-Agent: ${appContext.env.DOWNLOADER_UA}"`,
'-i', playlistUrl,
'-c:v', 'copy',
'-c:a', 'copy',
'-movflags', 'faststart',
'-y',
'-f', 'mpegts',
filename
], {
stdio: 'inherit'
});
return new Promise((resolve, reject) => {
ffmpegProcess.once('exit', (code) => {
resolve(code)
})
})
// ffmpegProcess.on('data', (data) => {
// console.log(data.toString());
// });
// Optional: Handle other events such as 'error', 'close', etc.
// @todo this needs to be handled outside this function
// otherwise this function is not testable
// ffmpegProcess.on('exit', (code, signal) => {
// // Retry the download using exponential backoff if the process exits for any reason
// console.log(`ffmpeg exited with code ${code} and signal ${signal}`)
// retryDownload(appContext, playlistUrl, roomName);
// });
// return ffmpegProcess;
}
const calculateExponentialBackoffDelay = (attemptNumber) => {
return Math.pow(2, attemptNumber) * 1000;
};
const retryDownload = (appContext, playlistUrl, roomName, attemptNumber = 1, maxAttempts = 3) => {
const delay = calculateExponentialBackoffDelay(attemptNumber);
appContext.logger.log({ level: 'debug', message: `Retrying download in ${delay / 1000} seconds...` });
setTimeout(() => {
console.log('Retrying download...');
record(appContext, playlistUrl, roomName, attemptNumber + 1);
}, delay);
};