修改为对通用计算的支持模块
This commit is contained in:
@@ -5,7 +5,7 @@ To properly test and run this project, you will need to prepare your environment
|
|||||||
1. **Install Node.js & FFmpeg**:
|
1. **Install Node.js & FFmpeg**:
|
||||||
- Ensure Node.js (v18+) is installed.
|
- Ensure Node.js (v18+) is installed.
|
||||||
- Install **FFmpeg** on your system and make sure it is available in your PATH environment variable. The Node.js library `fluent-ffmpeg` requires it.
|
- Install **FFmpeg** on your system and make sure it is available in your PATH environment variable. The Node.js library `fluent-ffmpeg` requires it.
|
||||||
- If you plan to use Rockchip hardware encoding, make sure your FFmpeg build includes `h264_rkmpp` / `hevc_rkmpp` support and the device has the Rockchip MPP runtime available.
|
- Note: The platform supports Intel QSV, Nvidia NVENC, and VAAPI hardware encoders. Ensure your FFmpeg build includes support for `h264_qsv` / `hevc_qsv` / `h264_nvenc` / `hevc_nvenc` / `h264_vaapi` / `hevc_vaapi` if you wish to use them.
|
||||||
- The service always uses Jellyfin FFmpeg by default: `/usr/lib/jellyfin-ffmpeg/ffmpeg` and `/usr/lib/jellyfin-ffmpeg/ffprobe`. You can override them with `JELLYFIN_FFMPEG_PATH` and `JELLYFIN_FFPROBE_PATH`.
|
- The service always uses Jellyfin FFmpeg by default: `/usr/lib/jellyfin-ffmpeg/ffmpeg` and `/usr/lib/jellyfin-ffmpeg/ffprobe`. You can override them with `JELLYFIN_FFMPEG_PATH` and `JELLYFIN_FFPROBE_PATH`.
|
||||||
|
|
||||||
2. **AWS S3 / MinIO Configuration**:
|
2. **AWS S3 / MinIO Configuration**:
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const buildHlsPlaylistUrl = () => {
|
const buildHlsPlaylistUrl = () => {
|
||||||
const decoder = 'auto';
|
const decoder = 'auto';
|
||||||
const encoder = encoderSelect?.value || 'h264_rkmpp';
|
const encoder = encoderSelect?.value || 'h264_qsv';
|
||||||
let streamUrl = `/api/hls/playlist.m3u8?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&decoder=${encodeURIComponent(decoder)}&encoder=${encodeURIComponent(encoder)}`;
|
let streamUrl = `/api/hls/playlist.m3u8?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&decoder=${encodeURIComponent(decoder)}&encoder=${encodeURIComponent(encoder)}`;
|
||||||
const sessionId = localStorage.getItem('sessionId');
|
const sessionId = localStorage.getItem('sessionId');
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
@@ -543,7 +543,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (topBannerTitle) topBannerTitle.textContent = title;
|
if (topBannerTitle) topBannerTitle.textContent = title;
|
||||||
topBanner.classList.remove('hidden');
|
topBanner.classList.remove('hidden');
|
||||||
document.title = title;
|
document.title = title;
|
||||||
populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_rkmpp');
|
populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_qsv');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Config load failed:', err);
|
console.error('Config load failed:', err);
|
||||||
}
|
}
|
||||||
@@ -642,8 +642,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
clearLoginError();
|
clearLoginError();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/login', {
|
const res = await fetch('/api/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username, password })
|
body: JSON.stringify({ username, password })
|
||||||
});
|
});
|
||||||
@@ -1120,11 +1120,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const sessionId = localStorage.getItem('sessionId');
|
const sessionId = localStorage.getItem('sessionId');
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/logout', {
|
await fetch('/api/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-Session-ID': sessionId }
|
headers: { 'X-Session-ID': sessionId }
|
||||||
});
|
});
|
||||||
} catch(e){}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
clearSessionAuth();
|
clearSessionAuth();
|
||||||
location.reload();
|
location.reload();
|
||||||
@@ -1143,7 +1143,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const savedSession = localStorage.getItem('sessionId');
|
const savedSession = localStorage.getItem('sessionId');
|
||||||
const savedUsername = localStorage.getItem('username');
|
const savedUsername = localStorage.getItem('username');
|
||||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
if (themeSelector) themeSelector.value = savedTheme;
|
if (themeSelector) themeSelector.value = savedTheme;
|
||||||
|
|
||||||
|
|||||||
59
server.js
59
server.js
@@ -59,10 +59,10 @@ const logger = winston.createLogger({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log = function(...args) { logger.info(util.format(...args)); };
|
console.log = function (...args) { logger.info(util.format(...args)); };
|
||||||
console.info = function(...args) { logger.info(util.format(...args)); };
|
console.info = function (...args) { logger.info(util.format(...args)); };
|
||||||
console.warn = function(...args) { logger.warn(util.format(...args)); };
|
console.warn = function (...args) { logger.warn(util.format(...args)); };
|
||||||
console.error = function(...args) { logger.error(util.format(...args)); };
|
console.error = function (...args) { logger.error(util.format(...args)); };
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
logger.error(`Uncaught Exception: ${err.message}`, err);
|
logger.error(`Uncaught Exception: ${err.message}`, err);
|
||||||
@@ -144,8 +144,12 @@ const transcodeProcesses = new Map();
|
|||||||
const wsSubscriptions = new Map();
|
const wsSubscriptions = new Map();
|
||||||
|
|
||||||
const AVAILABLE_VIDEO_ENCODERS = [
|
const AVAILABLE_VIDEO_ENCODERS = [
|
||||||
{ value: 'h264_rkmpp', label: 'H.264(RKMPP HighSpeed)' },
|
{ value: 'h264_qsv', label: 'H.264(Intel QSV)' },
|
||||||
{ value: 'hevc_rkmpp', label: 'H.265(RKMPP HighSpeed)' },
|
{ value: 'hevc_qsv', label: 'H.265(Intel QSV)' },
|
||||||
|
{ value: 'h264_nvenc', label: 'H.264(Nvidia NVENC)' },
|
||||||
|
{ value: 'hevc_nvenc', label: 'H.265(Nvidia NVENC)' },
|
||||||
|
{ value: 'h264_vaapi', label: 'H.264(VAAPI)' },
|
||||||
|
{ value: 'hevc_vaapi', label: 'H.265(VAAPI)' },
|
||||||
{ value: 'libx264', label: 'H.264(Software Slow)' },
|
{ value: 'libx264', label: 'H.264(Software Slow)' },
|
||||||
{ value: 'libx265', label: 'H.265(Software Slow)' }
|
{ value: 'libx265', label: 'H.265(Software Slow)' }
|
||||||
];
|
];
|
||||||
@@ -196,35 +200,14 @@ const createFfmpegOptions = (encoderName) => {
|
|||||||
options.push('-preset', 'fast', '-global_quality', '23');
|
options.push('-preset', 'fast', '-global_quality', '23');
|
||||||
} else if (/_vaapi$/.test(encoderName)) {
|
} else if (/_vaapi$/.test(encoderName)) {
|
||||||
options.push('-qp', '23');
|
options.push('-qp', '23');
|
||||||
} else if (/_rkmpp$/.test(encoderName)) {
|
|
||||||
options.push('-qp_init', '23', '-pix_fmt', 'nv12');
|
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRkmppCodec = (codecName) => /_rkmpp$/.test(codecName);
|
|
||||||
const isVaapiCodec = (codecName) => /_vaapi$/.test(codecName);
|
const isVaapiCodec = (codecName) => /_vaapi$/.test(codecName);
|
||||||
const availableEncoderValues = new Set(AVAILABLE_VIDEO_ENCODERS.map((item) => item.value));
|
const availableEncoderValues = new Set(AVAILABLE_VIDEO_ENCODERS.map((item) => item.value));
|
||||||
const availableDecoderValues = new Set(AVAILABLE_VIDEO_DECODERS.map((item) => item.value));
|
const availableDecoderValues = new Set(AVAILABLE_VIDEO_DECODERS.map((item) => item.value));
|
||||||
|
|
||||||
const getRkmppDecoderName = (metadata) => {
|
|
||||||
const videoStream = (metadata?.streams || []).find((stream) => stream.codec_type === 'video');
|
|
||||||
const codecName = (videoStream?.codec_name || '').toLowerCase();
|
|
||||||
const decoderMap = {
|
|
||||||
av1: 'av1_rkmpp',
|
|
||||||
h263: 'h263_rkmpp',
|
|
||||||
h264: 'h264_rkmpp',
|
|
||||||
hevc: 'hevc_rkmpp',
|
|
||||||
mjpeg: 'mjpeg_rkmpp',
|
|
||||||
mpeg1video: 'mpeg1_rkmpp',
|
|
||||||
mpeg2video: 'mpeg2_rkmpp',
|
|
||||||
mpeg4: 'mpeg4_rkmpp',
|
|
||||||
vp8: 'vp8_rkmpp',
|
|
||||||
vp9: 'vp9_rkmpp'
|
|
||||||
};
|
|
||||||
return decoderMap[codecName] || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseFpsValue = (fpsText) => {
|
const parseFpsValue = (fpsText) => {
|
||||||
if (typeof fpsText !== 'string' || !fpsText.trim()) {
|
if (typeof fpsText !== 'string' || !fpsText.trim()) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -263,8 +246,6 @@ const getSeekFriendlyOutputOptions = (encoderName, metadata) => {
|
|||||||
options.push('-forced-idr', '1', '-force_key_frames', 'expr:gte(t,n_forced*2)');
|
options.push('-forced-idr', '1', '-force_key_frames', 'expr:gte(t,n_forced*2)');
|
||||||
} else if (/_qsv$/.test(encoderName)) {
|
} else if (/_qsv$/.test(encoderName)) {
|
||||||
options.push('-idr_interval', '1');
|
options.push('-idr_interval', '1');
|
||||||
} else if (/_rkmpp$/.test(encoderName)) {
|
|
||||||
options.push('-force_key_frames', 'expr:gte(t,n_forced*2)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
@@ -272,7 +253,7 @@ const getSeekFriendlyOutputOptions = (encoderName, metadata) => {
|
|||||||
|
|
||||||
const shouldRetryWithSoftware = (message) => {
|
const shouldRetryWithSoftware = (message) => {
|
||||||
if (!message) return false;
|
if (!message) return false;
|
||||||
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument|mpp_create|rkmpp/i.test(message);
|
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument/i.test(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const probeFile = (filePath) => {
|
const probeFile = (filePath) => {
|
||||||
@@ -499,7 +480,7 @@ app.get('/api/config', (req, res) => {
|
|||||||
title,
|
title,
|
||||||
ffmpegPath: JELLYFIN_FFMPEG_PATH,
|
ffmpegPath: JELLYFIN_FFMPEG_PATH,
|
||||||
ffprobePath: JELLYFIN_FFPROBE_PATH,
|
ffprobePath: JELLYFIN_FFPROBE_PATH,
|
||||||
defaultVideoEncoder: 'h264_rkmpp',
|
defaultVideoEncoder: 'h264_qsv',
|
||||||
defaultVideoDecoder: 'auto',
|
defaultVideoDecoder: 'auto',
|
||||||
videoEncoders: AVAILABLE_VIDEO_ENCODERS,
|
videoEncoders: AVAILABLE_VIDEO_ENCODERS,
|
||||||
videoDecoders: AVAILABLE_VIDEO_DECODERS
|
videoDecoders: AVAILABLE_VIDEO_DECODERS
|
||||||
@@ -671,7 +652,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
|
|||||||
if (i === totalSegments - 1 && duration % HLS_SEGMENT_TIME !== 0) {
|
if (i === totalSegments - 1 && duration % HLS_SEGMENT_TIME !== 0) {
|
||||||
segDur = (duration % HLS_SEGMENT_TIME) || HLS_SEGMENT_TIME;
|
segDur = (duration % HLS_SEGMENT_TIME) || HLS_SEGMENT_TIME;
|
||||||
}
|
}
|
||||||
m3u8 += `#EXTINF:${segDur.toFixed(6)},\nsegment.ts?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}&seg=${i}&encoder=${req.query.encoder || 'h264_rkmpp'}&decoder=${req.query.decoder || 'auto'}\n`;
|
m3u8 += `#EXTINF:${segDur.toFixed(6)},\nsegment.ts?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}&seg=${i}&encoder=${req.query.encoder || 'h264_qsv'}&decoder=${req.query.decoder || 'auto'}\n`;
|
||||||
}
|
}
|
||||||
m3u8 += `#EXT-X-ENDLIST\n`;
|
m3u8 += `#EXT-X-ENDLIST\n`;
|
||||||
|
|
||||||
@@ -703,7 +684,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
|||||||
const bucket = req.query.bucket;
|
const bucket = req.query.bucket;
|
||||||
const key = req.query.key;
|
const key = req.query.key;
|
||||||
const seg = parseInt(req.query.seg || '0');
|
const seg = parseInt(req.query.seg || '0');
|
||||||
const requestedEncoder = req.query.encoder || 'h264_rkmpp';
|
const requestedEncoder = req.query.encoder || 'h264_qsv';
|
||||||
const requestedDecoder = req.query.decoder || 'auto';
|
const requestedDecoder = req.query.decoder || 'auto';
|
||||||
|
|
||||||
if (!bucket || !key || isNaN(seg)) return res.status(400).send('Bad Request');
|
if (!bucket || !key || isNaN(seg)) return res.status(400).send('Bad Request');
|
||||||
@@ -751,7 +732,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
|||||||
let sourceMetadata = null;
|
let sourceMetadata = null;
|
||||||
try { sourceMetadata = await probeFile(tmpInputPath); } catch (e) { }
|
try { sourceMetadata = await probeFile(tmpInputPath); } catch (e) { }
|
||||||
|
|
||||||
const encoderName = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp';
|
const encoderName = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_qsv';
|
||||||
const decoderName = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
|
const decoderName = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
|
||||||
|
|
||||||
const m3u8Path = path.join(hlsDir, `temp.m3u8`);
|
const m3u8Path = path.join(hlsDir, `temp.m3u8`);
|
||||||
@@ -765,7 +746,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
|||||||
if (isVaapiCodec(encoderName)) {
|
if (isVaapiCodec(encoderName)) {
|
||||||
ffmpegCommand.inputOptions(['-vaapi_device', '/dev/dri/renderD128']).videoFilters('format=nv12,hwupload');
|
ffmpegCommand.inputOptions(['-vaapi_device', '/dev/dri/renderD128']).videoFilters('format=nv12,hwupload');
|
||||||
}
|
}
|
||||||
const resolvedDecoderName = decoderName === 'auto' && isRkmppCodec(encoderName) ? getRkmppDecoderName(sourceMetadata) : decoderName;
|
const resolvedDecoderName = decoderName;
|
||||||
if (resolvedDecoderName && resolvedDecoderName !== 'auto') ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);
|
if (resolvedDecoderName && resolvedDecoderName !== 'auto') ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);
|
||||||
|
|
||||||
const segmentFilename = path.join(hlsDir, `segment_%d.ts`);
|
const segmentFilename = path.join(hlsDir, `segment_%d.ts`);
|
||||||
@@ -841,7 +822,7 @@ app.get('/api/stream', async (req, res) => {
|
|||||||
const bucket = req.query.bucket;
|
const bucket = req.query.bucket;
|
||||||
const key = req.query.key;
|
const key = req.query.key;
|
||||||
const requestedDecoder = typeof req.query.decoder === 'string' ? req.query.decoder.trim() : 'auto';
|
const requestedDecoder = typeof req.query.decoder === 'string' ? req.query.decoder.trim() : 'auto';
|
||||||
const requestedEncoder = typeof req.query.encoder === 'string' ? req.query.encoder.trim() : 'h264_rkmpp';
|
const requestedEncoder = typeof req.query.encoder === 'string' ? req.query.encoder.trim() : 'h264_qsv';
|
||||||
const startSeconds = parseFloat(req.query.ss) || 0;
|
const startSeconds = parseFloat(req.query.ss) || 0;
|
||||||
const streamSessionId = typeof req.query.streamSessionId === 'string' && req.query.streamSessionId.trim()
|
const streamSessionId = typeof req.query.streamSessionId === 'string' && req.query.streamSessionId.trim()
|
||||||
? req.query.streamSessionId.trim()
|
? req.query.streamSessionId.trim()
|
||||||
@@ -854,7 +835,7 @@ app.get('/api/stream', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Video key is required' });
|
return res.status(400).json({ error: 'Video key is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoEncoder = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp';
|
const videoEncoder = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_qsv';
|
||||||
const requestedVideoDecoder = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
|
const requestedVideoDecoder = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
|
||||||
|
|
||||||
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
||||||
@@ -995,9 +976,7 @@ app.get('/api/stream', async (req, res) => {
|
|||||||
.videoFilters('format=nv12,hwupload');
|
.videoFilters('format=nv12,hwupload');
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedDecoderName = decoderName === 'auto' && isRkmppCodec(encoderName)
|
const resolvedDecoderName = decoderName;
|
||||||
? getRkmppDecoderName(sourceMetadata)
|
|
||||||
: decoderName;
|
|
||||||
|
|
||||||
if (resolvedDecoderName && resolvedDecoderName !== 'auto') {
|
if (resolvedDecoderName && resolvedDecoderName !== 'auto') {
|
||||||
ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);
|
ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);
|
||||||
|
|||||||
Reference in New Issue
Block a user