From 2ca8958a1e07c304a2406caa501f3dc5fdc88c73 Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sat, 4 Apr 2026 13:02:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BA=E5=AF=B9=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E8=AE=A1=E7=AE=97=E7=9A=84=E6=94=AF=E6=8C=81=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- public/js/main.js | 18 +++++++-------- server.js | 59 +++++++++++++++-------------------------------- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 0f13302..9697548 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ To properly test and run this project, you will need to prepare your environment 1. **Install Node.js & FFmpeg**: - 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. - - 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`. 2. **AWS S3 / MinIO Configuration**: diff --git a/public/js/main.js b/public/js/main.js index c042307..6641fd9 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -140,7 +140,7 @@ document.addEventListener('DOMContentLoaded', () => { const buildHlsPlaylistUrl = () => { 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)}`; const sessionId = localStorage.getItem('sessionId'); if (sessionId) { @@ -543,7 +543,7 @@ document.addEventListener('DOMContentLoaded', () => { if (topBannerTitle) topBannerTitle.textContent = title; topBanner.classList.remove('hidden'); document.title = title; - populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_rkmpp'); + populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_qsv'); } catch (err) { console.error('Config load failed:', err); } @@ -642,8 +642,8 @@ document.addEventListener('DOMContentLoaded', () => { clearLoginError(); try { - const res = await fetch('/api/login', { - method: 'POST', + const res = await fetch('/api/login', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); @@ -1120,11 +1120,11 @@ document.addEventListener('DOMContentLoaded', () => { const sessionId = localStorage.getItem('sessionId'); if (sessionId) { try { - await fetch('/api/logout', { - method: 'POST', - headers: { 'X-Session-ID': sessionId } + await fetch('/api/logout', { + method: 'POST', + headers: { 'X-Session-ID': sessionId } }); - } catch(e){} + } catch (e) { } } clearSessionAuth(); location.reload(); @@ -1143,7 +1143,7 @@ document.addEventListener('DOMContentLoaded', () => { const savedSession = localStorage.getItem('sessionId'); const savedUsername = localStorage.getItem('username'); const savedTheme = localStorage.getItem('theme') || 'light'; - + document.documentElement.setAttribute('data-theme', savedTheme); if (themeSelector) themeSelector.value = savedTheme; diff --git a/server.js b/server.js index a4ef5d0..bd51b13 100644 --- a/server.js +++ b/server.js @@ -59,10 +59,10 @@ const logger = winston.createLogger({ ] }); -console.log = 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.error = function(...args) { logger.error(util.format(...args)); }; +console.log = 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.error = function (...args) { logger.error(util.format(...args)); }; process.on('uncaughtException', (err) => { logger.error(`Uncaught Exception: ${err.message}`, err); @@ -144,8 +144,12 @@ const transcodeProcesses = new Map(); const wsSubscriptions = new Map(); const AVAILABLE_VIDEO_ENCODERS = [ - { value: 'h264_rkmpp', label: 'H.264(RKMPP HighSpeed)' }, - { value: 'hevc_rkmpp', label: 'H.265(RKMPP HighSpeed)' }, + { value: 'h264_qsv', label: 'H.264(Intel QSV)' }, + { 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: 'libx265', label: 'H.265(Software Slow)' } ]; @@ -196,35 +200,14 @@ const createFfmpegOptions = (encoderName) => { options.push('-preset', 'fast', '-global_quality', '23'); } else if (/_vaapi$/.test(encoderName)) { options.push('-qp', '23'); - } else if (/_rkmpp$/.test(encoderName)) { - options.push('-qp_init', '23', '-pix_fmt', 'nv12'); } return options; }; -const isRkmppCodec = (codecName) => /_rkmpp$/.test(codecName); const isVaapiCodec = (codecName) => /_vaapi$/.test(codecName); const availableEncoderValues = new Set(AVAILABLE_VIDEO_ENCODERS.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) => { if (typeof fpsText !== 'string' || !fpsText.trim()) { return 0; @@ -263,8 +246,6 @@ const getSeekFriendlyOutputOptions = (encoderName, metadata) => { options.push('-forced-idr', '1', '-force_key_frames', 'expr:gte(t,n_forced*2)'); } else if (/_qsv$/.test(encoderName)) { options.push('-idr_interval', '1'); - } else if (/_rkmpp$/.test(encoderName)) { - options.push('-force_key_frames', 'expr:gte(t,n_forced*2)'); } return options; @@ -272,7 +253,7 @@ const getSeekFriendlyOutputOptions = (encoderName, metadata) => { const shouldRetryWithSoftware = (message) => { 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) => { @@ -499,7 +480,7 @@ app.get('/api/config', (req, res) => { title, ffmpegPath: JELLYFIN_FFMPEG_PATH, ffprobePath: JELLYFIN_FFPROBE_PATH, - defaultVideoEncoder: 'h264_rkmpp', + defaultVideoEncoder: 'h264_qsv', defaultVideoDecoder: 'auto', videoEncoders: AVAILABLE_VIDEO_ENCODERS, 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) { 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`; @@ -703,7 +684,7 @@ app.get('/api/hls/segment.ts', async (req, res) => { const bucket = req.query.bucket; const key = req.query.key; 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'; 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; 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 m3u8Path = path.join(hlsDir, `temp.m3u8`); @@ -765,7 +746,7 @@ app.get('/api/hls/segment.ts', async (req, res) => { if (isVaapiCodec(encoderName)) { 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]); 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 key = req.query.key; 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 streamSessionId = typeof req.query.streamSessionId === 'string' && 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' }); } - const videoEncoder = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp'; + const videoEncoder = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_qsv'; const requestedVideoDecoder = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto'; 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'); } - const resolvedDecoderName = decoderName === 'auto' && isRkmppCodec(encoderName) - ? getRkmppDecoderName(sourceMetadata) - : decoderName; + const resolvedDecoderName = decoderName; if (resolvedDecoderName && resolvedDecoderName !== 'auto') { ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);