feat: add quality/transcoding options to live player (#6)
Manual quality profiles (source/1080p/720p/480p) in the player overflow menu plus an honest/preparatory Auto contract: adaptive when the HLS manifest exposes multiple levels, single-rendition automatic selection otherwise. - server: transcodeQualityProfiles service and qualityMode session option - player: quality menu, persisted preference, live switch with rollback - tests: npm test script and unit tests for quality profiles
This commit is contained in:
@ -5,6 +5,7 @@ const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const db = require('../db');
|
||||
const transcodeSession = require('../services/transcodeSession');
|
||||
const { QUALITY_MODES, resolveQualityMode } = require('../services/transcodeQualityProfiles');
|
||||
|
||||
/**
|
||||
* Transcode Routes
|
||||
@ -31,7 +32,8 @@ transcodeSession.startCleanupInterval();
|
||||
router.post('/session', async (req, res) => {
|
||||
const {
|
||||
url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels,
|
||||
targetVideoBitrateKbps, targetAudioBitrateKbps, allowHardwareFallback
|
||||
targetVideoBitrateKbps, targetAudioBitrateKbps, allowHardwareFallback,
|
||||
qualityMode
|
||||
} = req.body;
|
||||
|
||||
if (!url) {
|
||||
@ -43,6 +45,7 @@ router.post('/session', async (req, res) => {
|
||||
const userAgent = db.getUserAgent(settings);
|
||||
|
||||
try {
|
||||
const qualitySelection = resolveQualityMode(qualityMode, settings);
|
||||
const session = await transcodeSession.createSession(url, {
|
||||
ffmpegPath,
|
||||
userAgent,
|
||||
@ -61,7 +64,8 @@ router.post('/session', async (req, res) => {
|
||||
videoMode: videoMode, // 'copy' or 'encode'
|
||||
videoCodec: videoCodec, // 'h264', 'hevc', etc.
|
||||
audioCodec: audioCodec, // 'aac', 'ac3', etc.
|
||||
audioChannels: audioChannels // number of channels (2=stereo)
|
||||
audioChannels: audioChannels, // number of channels (2=stereo)
|
||||
...(qualitySelection?.sessionOptions || {})
|
||||
});
|
||||
|
||||
await session.start();
|
||||
@ -80,10 +84,22 @@ router.post('/session', async (req, res) => {
|
||||
status: session.status,
|
||||
requestedEncoder: session.requestedEncoder,
|
||||
effectiveEncoder: session.effectiveEncoder,
|
||||
hardwareFallbackOccurred: session.hardwareFallbackOccurred
|
||||
hardwareFallbackOccurred: session.hardwareFallbackOccurred,
|
||||
...(qualitySelection && {
|
||||
qualityMode: qualitySelection.qualityMode,
|
||||
mode: qualitySelection.mode,
|
||||
selectedProfile: qualitySelection.selectedProfile,
|
||||
availableProfiles: qualitySelection.availableProfiles
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'INVALID_QUALITY_MODE') {
|
||||
return res.status(400).json({
|
||||
error: err.message,
|
||||
availableProfiles: QUALITY_MODES
|
||||
});
|
||||
}
|
||||
console.error('[Transcode] Session creation failed:', err);
|
||||
res.status(500).json({ error: 'Failed to create session', details: err.message });
|
||||
}
|
||||
|
||||
69
server/services/transcodeQualityProfiles.js
Normal file
69
server/services/transcodeQualityProfiles.js
Normal file
@ -0,0 +1,69 @@
|
||||
const QUALITY_PROFILES = Object.freeze({
|
||||
source: Object.freeze({
|
||||
id: 'source',
|
||||
videoMode: 'copy'
|
||||
}),
|
||||
'1080p': Object.freeze({
|
||||
id: '1080p',
|
||||
videoMode: 'encode',
|
||||
maxResolution: '1080p',
|
||||
targetVideoBitrateKbps: 6000
|
||||
}),
|
||||
'720p': Object.freeze({
|
||||
id: '720p',
|
||||
videoMode: 'encode',
|
||||
maxResolution: '720p',
|
||||
targetVideoBitrateKbps: 3500
|
||||
}),
|
||||
'480p': Object.freeze({
|
||||
id: '480p',
|
||||
videoMode: 'encode',
|
||||
maxResolution: '480p',
|
||||
targetVideoBitrateKbps: 1500
|
||||
})
|
||||
});
|
||||
|
||||
const QUALITY_MODES = Object.freeze(['auto', ...Object.keys(QUALITY_PROFILES)]);
|
||||
|
||||
function getAutomaticProfile(settings = {}) {
|
||||
const configuredResolution = settings.maxResolution;
|
||||
return QUALITY_PROFILES[configuredResolution] || QUALITY_PROFILES['1080p'];
|
||||
}
|
||||
|
||||
function resolveQualityMode(qualityMode, settings = {}) {
|
||||
if (qualityMode === undefined || qualityMode === null || qualityMode === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!QUALITY_MODES.includes(qualityMode)) {
|
||||
const error = new Error(`Unsupported qualityMode: ${qualityMode}`);
|
||||
error.code = 'INVALID_QUALITY_MODE';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const profile = qualityMode === 'auto'
|
||||
? getAutomaticProfile(settings)
|
||||
: QUALITY_PROFILES[qualityMode];
|
||||
|
||||
return {
|
||||
qualityMode,
|
||||
mode: 'single',
|
||||
selectedProfile: profile.id,
|
||||
availableProfiles: QUALITY_MODES,
|
||||
sessionOptions: {
|
||||
videoMode: profile.videoMode,
|
||||
...(profile.maxResolution && { maxResolution: profile.maxResolution }),
|
||||
...(profile.targetVideoBitrateKbps && {
|
||||
targetVideoBitrateKbps: profile.targetVideoBitrateKbps
|
||||
}),
|
||||
// A manual downscale profile must not inherit a global upscaling setting.
|
||||
...(profile.videoMode === 'encode' && { upscaleEnabled: false })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
QUALITY_MODES,
|
||||
QUALITY_PROFILES,
|
||||
resolveQualityMode
|
||||
};
|
||||
Reference in New Issue
Block a user