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:
blomios
2026-09-30 12:01:08 +02:00
parent f715203c4a
commit 4488914b2d
5 changed files with 427 additions and 17 deletions

View File

@ -0,0 +1,82 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
QUALITY_MODES,
resolveQualityMode
} = require('../server/services/transcodeQualityProfiles');
const { TranscodeSession } = require('../server/services/transcodeSession');
test('legacy payloads do not receive profile overrides', () => {
assert.equal(resolveQualityMode(undefined, { maxResolution: '720p' }), null);
});
test('manual quality profiles map to encoding height and bitrate', () => {
const expected = {
'1080p': { maxResolution: '1080p', targetVideoBitrateKbps: 6000 },
'720p': { maxResolution: '720p', targetVideoBitrateKbps: 3500 },
'480p': { maxResolution: '480p', targetVideoBitrateKbps: 1500 }
};
for (const [qualityMode, profile] of Object.entries(expected)) {
const selection = resolveQualityMode(qualityMode);
assert.equal(selection.mode, 'single');
assert.equal(selection.selectedProfile, qualityMode);
assert.deepEqual(selection.availableProfiles, QUALITY_MODES);
assert.deepEqual(selection.sessionOptions, {
videoMode: 'encode',
...profile,
upscaleEnabled: false
});
}
});
test('profile options produce the expected FFmpeg scale and bitrate arguments', () => {
for (const qualityMode of ['1080p', '720p', '480p']) {
const selection = resolveQualityMode(qualityMode);
const session = new TranscodeSession('https://example.test/live.ts', {
hwEncoder: 'software',
audioMixPreset: 'passthrough',
...selection.sessionOptions
});
const args = session.buildFFmpegArgs('software');
const bitrate = selection.sessionOptions.targetVideoBitrateKbps;
assert.equal(args[args.indexOf('-vf') + 1], `scale=-2:${qualityMode.replace('p', '')}`);
assert.equal(args[args.indexOf('-b:v') + 1], `${bitrate}k`);
assert.equal(args[args.indexOf('-maxrate') + 1], `${bitrate}k`);
assert.equal(args[args.indexOf('-bufsize') + 1], `${bitrate * 2}k`);
}
});
test('source profile preserves the original video stream', () => {
const selection = resolveQualityMode('source');
assert.equal(selection.selectedProfile, 'source');
assert.deepEqual(selection.sessionOptions, { videoMode: 'copy' });
});
test('auto selects one configured rendition and reports single-rendition mode', () => {
const selection = resolveQualityMode('auto', { maxResolution: '720p' });
assert.equal(selection.qualityMode, 'auto');
assert.equal(selection.mode, 'single');
assert.equal(selection.selectedProfile, '720p');
assert.deepEqual(selection.sessionOptions, {
videoMode: 'encode',
maxResolution: '720p',
targetVideoBitrateKbps: 3500,
upscaleEnabled: false
});
});
test('auto falls back to 1080p for an unsupported global resolution', () => {
assert.equal(resolveQualityMode('auto', { maxResolution: '4k' }).selectedProfile, '1080p');
});
test('unsupported quality modes are rejected', () => {
assert.throws(
() => resolveQualityMode('adaptive'),
error => error.code === 'INVALID_QUALITY_MODE'
);
});