Save worktree changes
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
function createRtmpStreamService(options) {
|
||||
const mediaDir = options && options.mediaDir ? options.mediaDir : null;
|
||||
const ffmpegPath = options && options.ffmpegPath ? options.ffmpegPath : 'ffmpeg';
|
||||
const sessions = new Map();
|
||||
|
||||
if (!mediaDir) {
|
||||
throw new Error('mediaDir is required');
|
||||
}
|
||||
|
||||
const cacheRoot = path.join(mediaDir, 'rtmp-cache');
|
||||
|
||||
function normalizeSourceUrl(value) {
|
||||
const sourceUrl = String(value || '').trim();
|
||||
if (!sourceUrl || !/^rtmps?:\/\//i.test(sourceUrl)) {
|
||||
const error = new Error('A valid RTMP URL is required.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
function getSessionKey(sourceUrl, disableAudio) {
|
||||
return crypto.createHash('sha1').update(String(sourceUrl)).update('\0').update(disableAudio ? '1' : '0').digest('hex');
|
||||
}
|
||||
|
||||
async function ensureDirectory(dirPath) {
|
||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
async function waitForFile(filePath, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
try {
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
if (stat.isFile() && stat.size > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (_error) {
|
||||
// keep waiting
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function buildPlaylistUrl(key) {
|
||||
return '/api/rtmp/streams/' + encodeURIComponent(key) + '/index.m3u8';
|
||||
}
|
||||
|
||||
async function ensureSession(sourceUrl, disableAudio) {
|
||||
const normalizedSource = normalizeSourceUrl(sourceUrl);
|
||||
const normalizedDisableAudio = Boolean(disableAudio);
|
||||
const key = getSessionKey(normalizedSource, normalizedDisableAudio);
|
||||
|
||||
if (sessions.has(key)) {
|
||||
return sessions.get(key);
|
||||
}
|
||||
|
||||
const directory = path.join(cacheRoot, key);
|
||||
const manifestPath = path.join(directory, 'index.m3u8');
|
||||
|
||||
await ensureDirectory(directory);
|
||||
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-loglevel', 'warning',
|
||||
'-nostdin',
|
||||
'-i', normalizedSource,
|
||||
'-fflags', '+genpts',
|
||||
'-f', 'hls',
|
||||
'-hls_time', '1',
|
||||
'-hls_init_time', '1',
|
||||
'-hls_list_size', '8',
|
||||
'-hls_flags', 'delete_segments+append_list+omit_endlist+independent_segments+program_date_time',
|
||||
'-hls_segment_filename', path.join(directory, 'segment-%05d.ts')
|
||||
];
|
||||
|
||||
if (normalizedDisableAudio) {
|
||||
args.push('-an');
|
||||
args.push('-c:v', 'copy');
|
||||
} else {
|
||||
args.push('-c', 'copy');
|
||||
}
|
||||
|
||||
args.push(manifestPath);
|
||||
|
||||
const child = spawn(ffmpegPath, args, {
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
});
|
||||
|
||||
child.stderr.on('data', function (chunk) {
|
||||
const message = String(chunk || '').trim();
|
||||
if (message) {
|
||||
console.error('[rtmp]', message);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('exit', function (code, signal) {
|
||||
const session = sessions.get(key);
|
||||
if (session && session.process === child) {
|
||||
session.process = null;
|
||||
session.exitCode = code;
|
||||
session.exitSignal = signal;
|
||||
}
|
||||
});
|
||||
|
||||
const session = {
|
||||
key: key,
|
||||
sourceUrl: normalizedSource,
|
||||
disableAudio: normalizedDisableAudio,
|
||||
directory: directory,
|
||||
manifestPath: manifestPath,
|
||||
playlistUrl: buildPlaylistUrl(key),
|
||||
process: child,
|
||||
ready: waitForFile(manifestPath, 5000)
|
||||
};
|
||||
|
||||
sessions.set(key, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async function getPlaylistUrl(sourceUrl, disableAudio) {
|
||||
const session = await ensureSession(sourceUrl, disableAudio);
|
||||
await session.ready.catch(function () {
|
||||
return false;
|
||||
});
|
||||
return session.playlistUrl + '?t=' + Date.now();
|
||||
}
|
||||
|
||||
function getSessionByKey(key) {
|
||||
return sessions.get(String(key || '').trim()) || null;
|
||||
}
|
||||
|
||||
async function getManifestFilePath(key) {
|
||||
const session = getSessionByKey(key);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
await session.ready.catch(function () {
|
||||
return false;
|
||||
});
|
||||
return session.manifestPath;
|
||||
}
|
||||
|
||||
async function getSegmentFilePath(key, fileName) {
|
||||
const session = getSessionByKey(key);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
const segmentName = path.basename(String(fileName || '').trim());
|
||||
if (!segmentName || segmentName === 'index.m3u8') {
|
||||
return null;
|
||||
}
|
||||
return path.join(session.directory, segmentName);
|
||||
}
|
||||
|
||||
return {
|
||||
ensureSession: ensureSession,
|
||||
getPlaylistUrl: getPlaylistUrl,
|
||||
getSessionByKey: getSessionByKey,
|
||||
getManifestFilePath: getManifestFilePath,
|
||||
getSegmentFilePath: getSegmentFilePath
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRtmpStreamService: createRtmpStreamService
|
||||
};
|
||||
Reference in New Issue
Block a user