@eleven-am/transcoder
Version:
High-performance HLS transcoding library with hardware acceleration, intelligent client management, and distributed processing support for Node.js
212 lines • 9.55 kB
JavaScript
;
/*
* @eleven-am/transcoder
* Copyright (C) 2025 Roy OSSAI
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdaptiveQualitySelector = void 0;
const fp_1 = require("@eleven-am/fp");
const types_1 = require("./types");
/**
* Analyzes video content and selects optimal quality levels
* Prevents upscaling and adjusts bitrates based on content complexity
*/
class AdaptiveQualitySelector {
constructor() {
this.qualityLevels = [
{ value: types_1.VideoQualityEnum.P240, height: 426, averageBitrate: 500000, maxBitrate: 700000 },
{ value: types_1.VideoQualityEnum.P360, height: 640, averageBitrate: 800000, maxBitrate: 1000000 },
{ value: types_1.VideoQualityEnum.P480, height: 854, averageBitrate: 1200000, maxBitrate: 1500000 },
{ value: types_1.VideoQualityEnum.P720, height: 1280, averageBitrate: 2500000, maxBitrate: 3000000 },
{ value: types_1.VideoQualityEnum.P1080, height: 1920, averageBitrate: 4500000, maxBitrate: 5500000 },
{ value: types_1.VideoQualityEnum.P1440, height: 2560, averageBitrate: 9000000, maxBitrate: 11000000 },
{ value: types_1.VideoQualityEnum.P4K, height: 3840, averageBitrate: 18000000, maxBitrate: 22000000 },
{ value: types_1.VideoQualityEnum.P8K, height: 7680, averageBitrate: 50000000, maxBitrate: 60000000 },
];
// Pure helper functions
this.calculateBitrateFactor = (video) => video.bitrate / (video.width * video.height * video.frameRate);
this.adjustScoreForBitrate = (score, bitrateFactor) => bitrateFactor > 0.15 ? score + 0.2 :
bitrateFactor < 0.05 ? score - 0.2 :
score;
this.adjustScoreForFrameRate = (score, frameRate) => frameRate > 30 ? score + 0.1 :
frameRate < 25 ? score - 0.1 :
score;
this.adjustScoreForCodec = (score, codec) => (codec.includes('hevc') || codec.includes('h265')) ? score + 0.1 : score;
this.adjustScoreForDuration = (score, duration) => duration > 3600 ? score + 0.05 : score;
this.normalizeScore = (score) => Math.max(0, Math.min(1, score));
}
/**
* Select optimal quality levels based on source video characteristics
*/
selectOptimalQualities(metadata) {
return fp_1.TaskEither.of(metadata)
.map(md => md.videos[0])
.filter(video => video !== undefined, () => (0, fp_1.createNotFoundError)('No video stream found in metadata'))
.chain(video => this.analyzeComplexityTask(metadata, video)
.map(complexityScore => ({ video, complexityScore })))
.map(({ video, complexityScore }) => ({
video,
complexityScore,
applicableQualities: this.filterQualitiesBySource(video),
}))
.map(data => ({
...data,
adjustedQualities: this.adjustBitratesForComplexity(data.applicableQualities, data.complexityScore, data.video),
}))
.map(data => ({
...data,
finalQualities: this.addOriginalQualityIfNeeded(data.adjustedQualities, data.video),
}))
.map(data => ({
selectedQualities: data.finalQualities,
reason: this.generateSelectionReason(data.video, data.complexityScore, data.finalQualities),
complexityScore: data.complexityScore,
}));
}
/**
* Get recommended quality based on network conditions
*/
getRecommendedQuality(availableBandwidth, qualities) {
const calculateTargetBitrate = (bandwidth) => bandwidth * 0.7;
const sortByBitrateAsc = (qs) => [...qs].sort((a, b) => a.averageBitrate - b.averageBitrate);
const findBestFit = (sorted, target) => sorted.reverse().find(q => q.averageBitrate <= target) || sorted[0];
return fp_1.TaskEither.of({ availableBandwidth, qualities })
.filter(data => data.qualities.length > 0, () => (0, fp_1.createNotFoundError)('No qualities available'))
.map(data => ({
...data,
targetBitrate: calculateTargetBitrate(data.availableBandwidth),
}))
.map(data => ({
...data,
sorted: sortByBitrateAsc(data.qualities),
}))
.map(data => findBestFit(data.sorted, data.targetBitrate))
.filter(quality => quality !== undefined, () => (0, fp_1.createNotFoundError)('No suitable quality found'));
}
/**
* Analyze video complexity based on metadata - TaskEither version
*/
analyzeComplexityTask(metadata, video) {
return fp_1.TaskEither.of({ metadata, video })
.map(data => ({
...data,
baseScore: 0.5,
bitrateFactor: this.calculateBitrateFactor(data.video),
}))
.map(data => ({
...data,
scoreAfterBitrate: this.adjustScoreForBitrate(data.baseScore, data.bitrateFactor),
}))
.map(data => ({
...data,
scoreAfterFrameRate: this.adjustScoreForFrameRate(data.scoreAfterBitrate, data.video.frameRate),
}))
.map(data => ({
...data,
scoreAfterCodec: this.adjustScoreForCodec(data.scoreAfterFrameRate, data.video.codec),
}))
.map(data => ({
...data,
scoreAfterDuration: this.adjustScoreForDuration(data.scoreAfterCodec, data.metadata.duration),
}))
.map(data => this.normalizeScore(data.scoreAfterDuration));
}
/**
* Filter quality levels to prevent upscaling
*/
filterQualitiesBySource(video) {
return this.qualityLevels.filter((quality) => {
// Never upscale
if (quality.height > video.height) {
return false;
}
// Skip very low qualities for high-res sources
if (video.height >= 1920 && quality.height < 480) {
return false;
}
return true;
});
}
/**
* Adjust bitrates based on content complexity
*/
adjustBitratesForComplexity(qualities, complexityScore, video) {
return qualities.map((quality) => {
// Calculate scale factor based on resolution ratio
const scaleFactor = (quality.height / video.height) ** 2;
// Adjust bitrate based on complexity
let bitrateMultiplier = 1.0;
if (complexityScore < 0.3) {
// Simple content (presentations, screencasts)
bitrateMultiplier = 0.6;
}
else if (complexityScore < 0.5) {
// Medium-simple content
bitrateMultiplier = 0.8;
}
else if (complexityScore > 0.7) {
// Complex content (action, nature documentaries)
bitrateMultiplier = 1.2;
}
// Apply scale factor for lower resolutions
bitrateMultiplier *= Math.sqrt(scaleFactor);
return {
...quality,
averageBitrate: Math.round(quality.averageBitrate * bitrateMultiplier),
maxBitrate: Math.round(quality.maxBitrate * bitrateMultiplier),
};
});
}
/**
* Add original quality option for high-quality sources
*/
addOriginalQualityIfNeeded(qualities, video) {
// Add original quality for high-res, high-bitrate sources
if (video.height >= 1920 && video.bitrate > 10000000) {
return [
...qualities,
{
value: types_1.VideoQualityEnum.ORIGINAL,
height: video.height,
averageBitrate: video.bitrate,
maxBitrate: Math.round(video.bitrate * 1.2),
},
];
}
return qualities;
}
/**
* Generate human-readable reason for quality selection
*/
generateSelectionReason(video, complexityScore, selectedQualities) {
const parts = [];
parts.push(`Source: ${video.width}x${video.height} @ ${video.frameRate}fps`);
if (complexityScore < 0.3) {
parts.push('Content identified as simple (low motion/detail)');
}
else if (complexityScore > 0.7) {
parts.push('Content identified as complex (high motion/detail)');
}
const skippedUpscaling = this.qualityLevels.filter((q) => q.height > video.height).length;
if (skippedUpscaling > 0) {
parts.push(`Skipped ${skippedUpscaling} qualities to prevent upscaling`);
}
parts.push(`Selected ${selectedQualities.length} quality levels`);
return parts.join('. ');
}
}
exports.AdaptiveQualitySelector = AdaptiveQualitySelector;
//# sourceMappingURL=adaptiveQualitySelector.js.map