feat: add Plex-style adaptive transcoding with bitrate limits and hardware fallback

This commit is contained in:
blomios
2026-09-29 23:31:05 +02:00
parent 2912384257
commit d018ac078c
7 changed files with 258 additions and 66 deletions

View File

@ -87,6 +87,22 @@ function analyzeProbeResult(probeResult, url) {
const videoCodec = videoStream?.codec_name?.toLowerCase() || 'unknown';
const audioCodec = audioStream?.codec_name?.toLowerCase() || 'unknown';
const container = format.format_name?.toLowerCase() || 'unknown';
const toPositiveNumber = (value) => {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
};
const toKbps = (value) => {
const bitrate = toPositiveNumber(value);
return bitrate === null ? null : Math.round(bitrate / 1000);
};
const durationSeconds = toPositiveNumber(format.duration) ?? toPositiveNumber(videoStream?.duration) ?? toPositiveNumber(audioStream?.duration);
const videoBitrateKbps = toKbps(videoStream?.bit_rate);
const audioBitrateKbps = toKbps(audioStream?.bit_rate);
const overallBitrateKbps = toKbps(format.bit_rate);
const probeConfidence = durationSeconds !== null && videoBitrateKbps !== null &&
(!audioStream || audioBitrateKbps !== null) && overallBitrateKbps !== null
? 'complete'
: 'partial';
// Check codec compatibility
const videoOk = BROWSER_VIDEO_CODECS.some(c => videoCodec.includes(c));
@ -131,6 +147,11 @@ function analyzeProbeResult(probeResult, url) {
width: videoStream?.width || 0,
height: videoStream?.height || 0,
audioChannels: audioStream?.channels || 0, // For Smart Audio Copy
durationSeconds,
videoBitrateKbps,
audioBitrateKbps,
overallBitrateKbps,
probeConfidence,
container: container,
compatible: compatible,
needsRemux: needsRemux,
@ -157,7 +178,12 @@ router.get('/', async (req, res) => {
container: 'unknown',
compatible: false,
needsRemux: false,
needsTranscode: true
needsTranscode: true,
durationSeconds: null,
videoBitrateKbps: null,
audioBitrateKbps: null,
overallBitrateKbps: null,
probeConfidence: 'partial'
});
}
@ -193,6 +219,11 @@ router.get('/', async (req, res) => {
compatible: false,
needsRemux: false,
needsTranscode: true,
durationSeconds: null,
videoBitrateKbps: null,
audioBitrateKbps: null,
overallBitrateKbps: null,
probeConfidence: 'partial',
error: err.message
});
}

View File

@ -29,7 +29,10 @@ transcodeSession.startCleanupInterval();
* Body: { url: string, seekOffset?: number }
*/
router.post('/session', async (req, res) => {
const { url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels } = req.body;
const {
url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels,
targetVideoBitrateKbps, targetAudioBitrateKbps, allowHardwareFallback
} = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
@ -47,6 +50,9 @@ router.post('/session', async (req, res) => {
hwEncoder: settings.hwEncoder || 'software',
maxResolution: settings.maxResolution || '1080p',
quality: settings.quality || 'medium',
targetVideoBitrateKbps: targetVideoBitrateKbps ?? settings.targetTranscodeBitrateKbps ?? 6000,
targetAudioBitrateKbps: targetAudioBitrateKbps ?? 192,
allowHardwareFallback: allowHardwareFallback ?? settings.allowHardwareFallback ?? true,
audioMixPreset: settings.audioMixPreset || 'auto', // Audio downmix preset
// Upscaling options
upscaleEnabled: settings.upscaleEnabled || false,
@ -71,7 +77,10 @@ router.post('/session', async (req, res) => {
res.json({
sessionId: session.id,
playlistUrl: `/api/transcode/${session.id}/stream.m3u8`,
status: session.status
status: session.status,
requestedEncoder: session.requestedEncoder,
effectiveEncoder: session.effectiveEncoder,
hardwareFallbackOccurred: session.hardwareFallbackOccurred
});
} catch (err) {
@ -270,4 +279,3 @@ router.get('/', async (req, res) => {
});
module.exports = router;