添加RKMPP
This commit is contained in:
91
server.js
91
server.js
@@ -15,7 +15,15 @@ const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const HOST = process.env.HOST || process.env.LISTEN_ADDRESS || '0.0.0.0';
|
||||
const server = http.createServer(app);
|
||||
const RKMPP_FFMPEG_PATH = process.env.RKMPP_FFMPEG_PATH || '/usr/lib/jellyfin-ffmpeg/ffmpeg';
|
||||
const JELLYFIN_FFMPEG_PATH = process.env.JELLYFIN_FFMPEG_PATH || '/usr/lib/jellyfin-ffmpeg/ffmpeg';
|
||||
const JELLYFIN_FFPROBE_PATH = process.env.JELLYFIN_FFPROBE_PATH || '/usr/lib/jellyfin-ffmpeg/ffprobe';
|
||||
|
||||
if (typeof ffmpeg.setFfmpegPath === 'function') {
|
||||
ffmpeg.setFfmpegPath(JELLYFIN_FFMPEG_PATH);
|
||||
}
|
||||
if (typeof ffmpeg.setFfprobePath === 'function') {
|
||||
ffmpeg.setFfprobePath(JELLYFIN_FFPROBE_PATH);
|
||||
}
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@@ -63,6 +71,34 @@ const progressMap = {};
|
||||
const transcodeProcesses = new Map();
|
||||
const wsSubscriptions = new Map();
|
||||
|
||||
const AVAILABLE_VIDEO_ENCODERS = [
|
||||
{ value: 'libx264', label: 'libx264 (Software H.264)' },
|
||||
{ value: 'libx265', label: 'libx265 (Software H.265)' },
|
||||
{ value: 'h264_nvenc', label: 'h264_nvenc (NVIDIA H.264)' },
|
||||
{ value: 'hevc_nvenc', label: 'hevc_nvenc (NVIDIA HEVC)' },
|
||||
{ value: 'h264_qsv', label: 'h264_qsv (Intel QSV H.264)' },
|
||||
{ value: 'hevc_qsv', label: 'hevc_qsv (Intel QSV HEVC)' },
|
||||
{ value: 'h264_vaapi', label: 'h264_vaapi (VAAPI H.264)' },
|
||||
{ value: 'hevc_vaapi', label: 'hevc_vaapi (VAAPI HEVC)' },
|
||||
{ value: 'h264_rkmpp', label: 'h264_rkmpp (RKMPP H.264)' },
|
||||
{ value: 'hevc_rkmpp', label: 'hevc_rkmpp (RKMPP HEVC)' },
|
||||
{ value: 'mjpeg_rkmpp', label: 'mjpeg_rkmpp (RKMPP MJPEG)' }
|
||||
];
|
||||
|
||||
const AVAILABLE_VIDEO_DECODERS = [
|
||||
{ value: 'auto', label: 'Auto Select Decoder' },
|
||||
{ value: 'av1_rkmpp', label: 'av1_rkmpp (RKMPP AV1)' },
|
||||
{ value: 'h263_rkmpp', label: 'h263_rkmpp (RKMPP H.263)' },
|
||||
{ value: 'h264_rkmpp', label: 'h264_rkmpp (RKMPP H.264)' },
|
||||
{ value: 'hevc_rkmpp', label: 'hevc_rkmpp (RKMPP HEVC)' },
|
||||
{ value: 'mjpeg_rkmpp', label: 'mjpeg_rkmpp (RKMPP MJPEG)' },
|
||||
{ value: 'mpeg1_rkmpp', label: 'mpeg1_rkmpp (RKMPP MPEG-1)' },
|
||||
{ value: 'mpeg2_rkmpp', label: 'mpeg2_rkmpp (RKMPP MPEG-2)' },
|
||||
{ value: 'mpeg4_rkmpp', label: 'mpeg4_rkmpp (RKMPP MPEG-4)' },
|
||||
{ value: 'vp8_rkmpp', label: 'vp8_rkmpp (RKMPP VP8)' },
|
||||
{ value: 'vp9_rkmpp', label: 'vp9_rkmpp (RKMPP VP9)' }
|
||||
];
|
||||
|
||||
const getProgressKey = (key) => key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')).join('/');
|
||||
|
||||
const createStreamSessionId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
@@ -112,6 +148,9 @@ const createFfmpegOptions = (encoderName) => {
|
||||
};
|
||||
|
||||
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');
|
||||
@@ -360,7 +399,15 @@ app.get('/api/videos', async (req, res) => {
|
||||
|
||||
app.get('/api/config', (req, res) => {
|
||||
const title = process.env.APP_TITLE || 'S3 Media Transcoder';
|
||||
res.json({ title });
|
||||
res.json({
|
||||
title,
|
||||
ffmpegPath: JELLYFIN_FFMPEG_PATH,
|
||||
ffprobePath: JELLYFIN_FFPROBE_PATH,
|
||||
defaultVideoEncoder: 'h264_rkmpp',
|
||||
defaultVideoDecoder: 'auto',
|
||||
videoEncoders: AVAILABLE_VIDEO_ENCODERS,
|
||||
videoDecoders: AVAILABLE_VIDEO_DECODERS
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/clear-download-cache', (req, res) => {
|
||||
@@ -402,8 +449,8 @@ app.post('/api/stop-transcode', (req, res) => {
|
||||
app.get('/api/stream', async (req, res) => {
|
||||
const bucket = req.query.bucket;
|
||||
const key = req.query.key;
|
||||
const codec = req.query.codec;
|
||||
const encoder = req.query.encoder;
|
||||
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 startSeconds = parseFloat(req.query.ss) || 0;
|
||||
const streamSessionId = typeof req.query.streamSessionId === 'string' && req.query.streamSessionId.trim()
|
||||
? req.query.streamSessionId.trim()
|
||||
@@ -416,17 +463,8 @@ app.get('/api/stream', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Video key is required' });
|
||||
}
|
||||
|
||||
const safeCodec = codec === 'h265' ? 'h265' : 'h264';
|
||||
const safeEncoder = ['nvidia', 'intel', 'vaapi', 'rkmpp', 'neon'].includes(encoder) ? encoder : 'software';
|
||||
const codecMap = {
|
||||
software: { h264: 'libx264', h265: 'libx265' },
|
||||
neon: { h264: 'libx264', h265: 'libx265' },
|
||||
nvidia: { h264: 'h264_nvenc', h265: 'hevc_nvenc' },
|
||||
intel: { h264: 'h264_qsv', h265: 'hevc_qsv' },
|
||||
vaapi: { h264: 'h264_vaapi', h265: 'hevc_vaapi' },
|
||||
rkmpp: { h264: 'h264_rkmpp', h265: 'hevc_rkmpp' }
|
||||
};
|
||||
const videoCodec = codecMap[safeEncoder][safeCodec];
|
||||
const videoEncoder = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp';
|
||||
const requestedVideoDecoder = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
|
||||
|
||||
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
||||
const progressKey = safeKeySegments.join('/');
|
||||
@@ -543,7 +581,7 @@ app.get('/api/stream', async (req, res) => {
|
||||
|
||||
let ffmpegCommand = null;
|
||||
|
||||
const startStream = (encoderName) => {
|
||||
const startStream = (encoderName, decoderName) => {
|
||||
const streamingOptions = createFfmpegOptions(encoderName).concat(getSeekFriendlyOutputOptions(encoderName, sourceMetadata));
|
||||
ffmpegCommand = ffmpeg()
|
||||
.input(tmpInputPath)
|
||||
@@ -552,10 +590,6 @@ app.get('/api/stream', async (req, res) => {
|
||||
.outputOptions(streamingOptions)
|
||||
.format('mp4');
|
||||
|
||||
if (isRkmppCodec(encoderName) && typeof ffmpegCommand.setFfmpegPath === 'function') {
|
||||
ffmpegCommand.setFfmpegPath(RKMPP_FFMPEG_PATH);
|
||||
}
|
||||
|
||||
if (startSeconds > 0) {
|
||||
if (typeof ffmpegCommand.seekInput === 'function') {
|
||||
ffmpegCommand.seekInput(startSeconds);
|
||||
@@ -564,15 +598,18 @@ app.get('/api/stream', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (/_vaapi$/.test(encoderName)) {
|
||||
if (isVaapiCodec(encoderName)) {
|
||||
ffmpegCommand
|
||||
.inputOptions(['-vaapi_device', '/dev/dri/renderD128'])
|
||||
.videoFilters('format=nv12,hwupload');
|
||||
} else if (isRkmppCodec(encoderName)) {
|
||||
const rkmppDecoder = getRkmppDecoderName(sourceMetadata);
|
||||
if (rkmppDecoder) {
|
||||
ffmpegCommand.inputOptions(['-c:v', rkmppDecoder]);
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedDecoderName = decoderName === 'auto' && isRkmppCodec(encoderName)
|
||||
? getRkmppDecoderName(sourceMetadata)
|
||||
: decoderName;
|
||||
|
||||
if (resolvedDecoderName && resolvedDecoderName !== 'auto') {
|
||||
ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);
|
||||
}
|
||||
|
||||
transcodeProcesses.set(progressKey, { command: ffmpegCommand, streamSessionId });
|
||||
@@ -664,7 +701,7 @@ app.get('/api/stream', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
startStream(videoCodec);
|
||||
startStream(videoEncoder, requestedVideoDecoder);
|
||||
} catch (error) {
|
||||
console.error('Error in stream:', error);
|
||||
if (!res.headersSent) {
|
||||
|
||||
Reference in New Issue
Block a user