refactor: убрать useNvenc и разделить выбор энкодера/декодера

This commit is contained in:
2026-01-20 10:38:25 +03:00
parent f550b7eb69
commit 224f14a8e0
8 changed files with 194 additions and 68 deletions

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@
* create-vod ./video.mp4 ./output -r 720,1080 * create-vod ./video.mp4 ./output -r 720,1080
*/ */
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders } from './index'; import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders, detectHardwareDecoders } from './index';
import cliProgress from 'cli-progress'; import cliProgress from 'cli-progress';
import { statSync } from 'node:fs'; import { statSync } from 'node:fs';
import { basename, extname } from 'node:path'; import { basename, extname } from 'node:path';
@@ -31,6 +31,7 @@ let h264CRF: number | undefined;
let av1CQ: number | undefined; let av1CQ: number | undefined;
let av1CRF: number | undefined; let av1CRF: number | undefined;
let accelerator: HardwareAccelerationOption | undefined; let accelerator: HardwareAccelerationOption | undefined;
let decoder: HardwareAccelerationOption | undefined;
// First pass: extract flags and their values // First pass: extract flags and their values
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
@@ -101,15 +102,24 @@ for (let i = 0; i < args.length; i++) {
process.exit(1); process.exit(1);
} }
i++; // Skip next arg i++; // Skip next arg
} else if (args[i] === '--accel' || args[i] === '--hardware') { } else if (args[i] === '--accel' || args[i] === '--hardware' || args[i] === '-e' || args[i] === '--encoder') {
const acc = args[i + 1]; const acc = args[i + 1];
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu']; const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu', 'vaapi', 'videotoolbox', 'v4l2'];
if (!allowed.includes(acc as HardwareAccelerationOption)) { if (!allowed.includes(acc as HardwareAccelerationOption)) {
console.error(`❌ Invalid accelerator: ${acc}. Valid: auto, nvenc, qsv, amf, cpu`); console.error(`❌ Invalid accelerator: ${acc}. Valid: auto, nvenc, qsv, amf, vaapi, videotoolbox, v4l2, cpu`);
process.exit(1); process.exit(1);
} }
accelerator = acc as HardwareAccelerationOption; accelerator = acc as HardwareAccelerationOption;
i++; i++;
} else if (args[i] === '-d' || args[i] === '--decoder') {
const acc = args[i + 1];
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'vaapi', 'videotoolbox', 'v4l2', 'cpu'];
if (!allowed.includes(acc as HardwareAccelerationOption)) {
console.error(`❌ Invalid decoder: ${acc}. Valid: auto, nvenc, qsv, amf, vaapi, videotoolbox, v4l2, cpu`);
process.exit(1);
}
decoder = acc as HardwareAccelerationOption;
i++;
} else if (!args[i].startsWith('-')) { } else if (!args[i].startsWith('-')) {
// Positional argument // Positional argument
positionalArgs.push(args[i]); positionalArgs.push(args[i]);
@@ -127,7 +137,8 @@ if (!input) {
console.error(' -c, --codec Video codec: av1, h264, or dual (default: dual)'); console.error(' -c, --codec Video codec: av1, h264, or dual (default: dual)');
console.error(' -f, --format Streaming format: dash, hls, or both (default: both)'); console.error(' -f, --format Streaming format: dash, hls, or both (default: both)');
console.error(' -p, --poster Poster timecode (e.g., 00:00:05 or 10)'); console.error(' -p, --poster Poster timecode (e.g., 00:00:05 or 10)');
console.error(' --accel <type> Hardware accelerator: auto|nvenc|qsv|amf|cpu (default: auto)'); console.error(' -e, --encoder <type> Hardware encoder: auto|nvenc|qsv|amf|vaapi|videotoolbox|v4l2|cpu (default: auto)');
console.error(' -d, --decoder <type> Hardware decoder: auto|nvenc|qsv|amf|vaapi|videotoolbox|v4l2|cpu (default: auto)');
console.error('\nQuality Options (override defaults):'); console.error('\nQuality Options (override defaults):');
console.error(' --h264-cq <value> H.264 GPU CQ value (0-51, lower = better, default: auto)'); console.error(' --h264-cq <value> H.264 GPU CQ value (0-51, lower = better, default: auto)');
console.error(' --h264-crf <value> H.264 CPU CRF value (0-51, lower = better, default: auto)'); console.error(' --h264-crf <value> H.264 CPU CRF value (0-51, lower = better, default: auto)');
@@ -153,6 +164,7 @@ const hasFFmpeg = await checkFFmpeg();
const hasMP4Box = await checkMP4Box(); const hasMP4Box = await checkMP4Box();
const hwEncoders = await detectHardwareEncoders(); const hwEncoders = await detectHardwareEncoders();
const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder); const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder);
const hwDecoders = await detectHardwareDecoders();
const accelPriority: Record<string, number> = { const accelPriority: Record<string, number> = {
nvenc: 100, nvenc: 100,
@@ -176,6 +188,10 @@ const accelLabel = bestAccelName
? `${bestAccelName}${accelRest.length > 0 ? ` (${accelRest.join(', ')})` : ''}` ? `${bestAccelName}${accelRest.length > 0 ? ` (${accelRest.join(', ')})` : ''}`
: '❌'; : '❌';
console.log(`Hardware: ${accelLabel}`); console.log(`Hardware: ${accelLabel}`);
if (hwDecoders.length > 0) {
const decList = Array.from(new Set(hwDecoders.map((d) => d.accelerator.toUpperCase())));
console.log(`Decoders: ${decList.join(', ')}`);
}
console.log(''); console.log('');
if (!hasFFmpeg) { if (!hasFFmpeg) {
@@ -267,7 +283,13 @@ const manifestDesc =
const thumbnailsPlanned = true; const thumbnailsPlanned = true;
const posterPlanned = posterTimecode || '00:00:00'; const posterPlanned = posterTimecode || '00:00:00';
const codecDisplay = codecType === 'dual' ? 'dual (AV1 + H.264)' : codecType; const codecDisplay = codecType === 'dual' ? 'dual (AV1 + H.264)' : codecType;
const codecNote = codecType === 'h264' && accelRest && accelRest.length >= 0 && !hasAv1Hardware ? ' (AV1 disabled: no HW)' : ''; const codecNote = codecType === 'h264' && !hasAv1Hardware ? ' (AV1 disabled: no HW)' : '';
const plannedAccel = accelerator ? accelerator.toUpperCase() : (bestAccelName || 'CPU');
const plannedDecoder = decoder ? decoder.toUpperCase() : (hwDecoders[0]?.accelerator.toUpperCase() || 'CPU');
const acceleratorDisplay = plannedAccel === 'AUTO' ? (bestAccelName || 'CPU') : plannedAccel;
const decoderDisplay = plannedDecoder === 'AUTO'
? (hwDecoders[0]?.accelerator.toUpperCase() || 'CPU')
: plannedDecoder;
console.log('\n📦 Parameters:'); console.log('\n📦 Parameters:');
console.log(` Input: ${input}`); console.log(` Input: ${input}`);
@@ -277,7 +299,8 @@ console.log(` Profiles: ${displayProfiles.join(', ')}`);
console.log(` Manifests: ${manifestDesc}`); console.log(` Manifests: ${manifestDesc}`);
console.log(` Poster: ${posterPlanned} (will be generated)`); console.log(` Poster: ${posterPlanned} (will be generated)`);
console.log(` Thumbnails: ${thumbnailsPlanned ? 'yes (with VTT)' : 'no'}`); console.log(` Thumbnails: ${thumbnailsPlanned ? 'yes (with VTT)' : 'no'}`);
console.log(` Accelerator: ${bestAccel ? bestAccel.accelerator.toUpperCase() : 'CPU'}`); console.log(` Accelerator: ${acceleratorDisplay}`);
console.log(` Decoder: ${decoderDisplay}`);
// Build quality settings if any are specified // Build quality settings if any are specified
let quality: QualitySettings | undefined; let quality: QualitySettings | undefined;
@@ -316,6 +339,7 @@ const bars: Record<string, any> = {};
let overallBar: any = null; let overallBar: any = null;
try { try {
const startedAt = Date.now();
const result = await convertToDash({ const result = await convertToDash({
input, input,
outputDir, outputDir,
@@ -325,6 +349,7 @@ try {
format: formatType, format: formatType,
segmentDuration: 2, segmentDuration: 2,
hardwareAccelerator: accelerator, hardwareAccelerator: accelerator,
hardwareDecoder: decoder,
quality, quality,
generateThumbnails: true, generateThumbnails: true,
generatePoster: true, generatePoster: true,
@@ -368,7 +393,9 @@ try {
multibar.stop(); multibar.stop();
console.log('\n✅ Conversion completed successfully!\n'); const elapsedMs = Date.now() - startedAt;
const elapsedSec = (elapsedMs / 1000).toFixed(2);
console.log(`\n✅ Conversion completed successfully! (${elapsedSec}s)\n`);
} catch (error) { } catch (error) {
multibar.stop(); multibar.stop();

View File

@@ -11,7 +11,8 @@ import type {
StreamingFormat, StreamingFormat,
HardwareAccelerationOption, HardwareAccelerationOption,
HardwareAccelerator, HardwareAccelerator,
HardwareEncoderInfo HardwareEncoderInfo,
HardwareDecoderInfo
} from '../types'; } from '../types';
import { import {
checkFFmpeg, checkFFmpeg,
@@ -19,7 +20,8 @@ import {
getVideoMetadata, getVideoMetadata,
ensureDir, ensureDir,
setLogFile, setLogFile,
detectHardwareEncoders detectHardwareEncoders,
detectHardwareDecoders
} from '../utils'; } from '../utils';
import { selectProfiles, createProfilesFromStrings } from '../config/profiles'; import { selectProfiles, createProfilesFromStrings } from '../config/profiles';
import { generateThumbnailSprite, generatePoster } from './thumbnails'; import { generateThumbnailSprite, generatePoster } from './thumbnails';
@@ -41,7 +43,7 @@ export async function convertToDash(
customProfiles, customProfiles,
codec = 'dual', codec = 'dual',
format = 'both', format = 'both',
useNvenc, hardwareDecoder,
hardwareAccelerator, hardwareAccelerator,
quality, quality,
generateThumbnails = true, generateThumbnails = true,
@@ -87,8 +89,8 @@ Format: ${format}
customProfiles, customProfiles,
codec, codec,
format, format,
useNvenc,
hardwareAccelerator, hardwareAccelerator,
hardwareDecoder,
quality, quality,
generateThumbnails, generateThumbnails,
thumbnailConfig, thumbnailConfig,
@@ -127,8 +129,8 @@ async function convertToDashInternal(
customProfiles: string[] | undefined, customProfiles: string[] | undefined,
codec: CodecType, codec: CodecType,
format: StreamingFormat, format: StreamingFormat,
useNvenc: boolean | undefined,
hardwareAccelerator: HardwareAccelerationOption | undefined, hardwareAccelerator: HardwareAccelerationOption | undefined,
hardwareDecoder: HardwareAccelerationOption | undefined,
quality: DashConvertOptions['quality'], quality: DashConvertOptions['quality'],
generateThumbnails: boolean, generateThumbnails: boolean,
thumbnailConfig: ThumbnailConfig, thumbnailConfig: ThumbnailConfig,
@@ -164,13 +166,10 @@ async function convertToDashInternal(
const preferredAccelerator: HardwareAccelerationOption = const preferredAccelerator: HardwareAccelerationOption =
hardwareAccelerator && hardwareAccelerator !== 'auto' hardwareAccelerator && hardwareAccelerator !== 'auto'
? hardwareAccelerator ? hardwareAccelerator
: useNvenc === true : 'auto';
? 'nvenc'
: useNvenc === false
? 'cpu'
: 'auto';
const hardwareEncoders = await detectHardwareEncoders(); const hardwareEncoders = await detectHardwareEncoders();
const hardwareDecoders = await detectHardwareDecoders();
const { selected, h264Encoder, av1Encoder, warnings: accelWarnings } = selectHardwareEncoders( const { selected, h264Encoder, av1Encoder, warnings: accelWarnings } = selectHardwareEncoders(
hardwareEncoders, hardwareEncoders,
@@ -184,6 +183,11 @@ async function convertToDashInternal(
} }
} }
const { selected: selectedDecoder } = selectHardwareDecoders(
hardwareDecoders,
hardwareDecoder || 'auto'
);
const av1HardwareAvailable = hardwareEncoders.some(info => info.av1Encoder); const av1HardwareAvailable = hardwareEncoders.some(info => info.av1Encoder);
let effectiveCodec: CodecType = codec; let effectiveCodec: CodecType = codec;
@@ -275,7 +279,7 @@ async function convertToDashInternal(
const codecNames = codecs.map(c => c.type.toUpperCase()).join(' + '); const codecNames = codecs.map(c => c.type.toUpperCase()).join(' + ');
const accelLabel = selected === 'cpu' ? 'CPU' : selected.toUpperCase(); const accelLabel = selected === 'cpu' ? 'CPU' : selected.toUpperCase();
reportProgress('analyzing', 20, `Using ${codecNames} encoding (${accelLabel})`, undefined); reportProgress('analyzing', 20, `Using ${codecNames} encoding (${accelLabel}, decoder ${selectedDecoder.toUpperCase()})`, undefined);
const maxConcurrent = selected === 'cpu' ? 2 : 3; const maxConcurrent = selected === 'cpu' ? 2 : 3;
@@ -306,6 +310,7 @@ async function convertToDashInternal(
type, // Pass codec type to differentiate output files type, // Pass codec type to differentiate output files
codecQuality, // Pass quality settings (CQ/CRF) codecQuality, // Pass quality settings (CQ/CRF)
undefined, // optimizations - for future use undefined, // optimizations - for future use
selectedDecoder === 'cpu' ? undefined : selectedDecoder,
(profileName, percent) => { (profileName, percent) => {
const profileIndex = profiles.findIndex(p => p.name === profileName); const profileIndex = profiles.findIndex(p => p.name === profileName);
const baseProgress = 25 + codecProgress * 40; const baseProgress = 25 + codecProgress * 40;
@@ -412,6 +417,7 @@ async function convertToDashInternal(
profiles, profiles,
usedNvenc: codecs.some(c => c.codec.includes('nvenc')), usedNvenc: codecs.some(c => c.codec.includes('nvenc')),
selectedAccelerator: selected, selectedAccelerator: selected,
selectedDecoder,
codecType: effectiveCodec, codecType: effectiveCodec,
format format
}; };
@@ -518,6 +524,36 @@ function selectHardwareEncoders(
}; };
} }
function selectHardwareDecoders(
available: HardwareDecoderInfo[],
preferred: HardwareAccelerationOption
): {
selected: HardwareAccelerator;
} {
const supportedForAuto = new Set<HardwareAccelerator>(['nvenc', 'qsv', 'vaapi', 'videotoolbox', 'v4l2']);
const pick = (acc: HardwareAccelerator) => available.find(info => info.accelerator === acc);
if (preferred !== 'auto') {
if (preferred === 'cpu') {
return { selected: 'cpu' };
}
const item = pick(preferred);
return { selected: item ? item.accelerator : 'cpu' };
}
const pool = available.filter(info => supportedForAuto.has(info.accelerator));
if (pool.length === 0) {
return { selected: 'cpu' };
}
const best = pool.sort(
(a, b) => (ACCEL_PRIORITY[b.accelerator] || 0) - (ACCEL_PRIORITY[a.accelerator] || 0)
)[0];
return { selected: best.accelerator };
}
function resolvePresetForEncoder(encoder: string, codecType: 'h264' | 'av1'): string { function resolvePresetForEncoder(encoder: string, codecType: 'h264' | 'av1'): string {
if (encoder.includes('nvenc')) return 'p4'; if (encoder.includes('nvenc')) return 'p4';
if (encoder.includes('qsv')) return 'medium'; if (encoder.includes('qsv')) return 'medium';

View File

@@ -1,6 +1,6 @@
import { join } from 'node:path'; import { join } from 'node:path';
import { execFFmpeg, selectAudioBitrate } from '../utils'; import { execFFmpeg, selectAudioBitrate } from '../utils';
import type { VideoProfile, VideoOptimizations, CodecQualitySettings } from '../types'; import type { VideoProfile, VideoOptimizations, CodecQualitySettings, HardwareAccelerator } from '../types';
/** /**
* Get default CQ/CRF value based on resolution and codec * Get default CQ/CRF value based on resolution and codec
@@ -53,15 +53,29 @@ export async function encodeProfileToMP4(
codecType: 'h264' | 'av1', codecType: 'h264' | 'av1',
qualitySettings?: CodecQualitySettings, qualitySettings?: CodecQualitySettings,
optimizations?: VideoOptimizations, optimizations?: VideoOptimizations,
decoderAccel?: HardwareAccelerator,
onProgress?: (percent: number) => void onProgress?: (percent: number) => void
): Promise<string> { ): Promise<string> {
const outputPath = join(tempDir, `video_${codecType}_${profile.name}.mp4`); const outputPath = join(tempDir, `video_${codecType}_${profile.name}.mp4`);
const args = [ const args = ['-y'];
'-y',
'-i', input, // Hardware decode (optional)
'-c:v', videoCodec if (decoderAccel) {
]; if (decoderAccel === 'nvenc') {
args.push('-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda');
} else if (decoderAccel === 'qsv') {
args.push('-hwaccel', 'qsv');
} else if (decoderAccel === 'vaapi') {
args.push('-hwaccel', 'vaapi');
} else if (decoderAccel === 'videotoolbox') {
args.push('-hwaccel', 'videotoolbox');
} else if (decoderAccel === 'v4l2') {
args.push('-hwaccel', 'v4l2');
}
}
args.push('-i', input, '-c:v', videoCodec);
// Determine if using GPU or CPU encoder // Determine if using GPU or CPU encoder
const isGPU = videoCodec.includes('nvenc') || videoCodec.includes('qsv') || videoCodec.includes('amf') || videoCodec.includes('vaapi') || videoCodec.includes('videotoolbox') || videoCodec.includes('v4l2'); const isGPU = videoCodec.includes('nvenc') || videoCodec.includes('qsv') || videoCodec.includes('amf') || videoCodec.includes('vaapi') || videoCodec.includes('videotoolbox') || videoCodec.includes('v4l2');
@@ -196,6 +210,7 @@ export async function encodeProfilesToMP4(
codecType: 'h264' | 'av1', codecType: 'h264' | 'av1',
qualitySettings?: CodecQualitySettings, qualitySettings?: CodecQualitySettings,
optimizations?: VideoOptimizations, optimizations?: VideoOptimizations,
decoderAccel?: HardwareAccelerator,
onProgress?: (profileName: string, percent: number) => void onProgress?: (profileName: string, percent: number) => void
): Promise<Map<string, string>> { ): Promise<Map<string, string>> {
const mp4Files = new Map<string, string>(); const mp4Files = new Map<string, string>();
@@ -217,6 +232,7 @@ export async function encodeProfilesToMP4(
codecType, codecType,
qualitySettings, qualitySettings,
optimizations, optimizations,
decoderAccel,
(percent) => { (percent) => {
if (onProgress) { if (onProgress) {
onProgress(profile.name, percent); onProgress(profile.name, percent);
@@ -246,6 +262,7 @@ export async function encodeProfilesToMP4(
codecType, codecType,
qualitySettings, qualitySettings,
optimizations, optimizations,
decoderAccel,
(percent) => { (percent) => {
if (onProgress) { if (onProgress) {
onProgress(profile.name, percent); onProgress(profile.name, percent);

View File

@@ -13,7 +13,8 @@ export type {
CodecType, CodecType,
HardwareAccelerator, HardwareAccelerator,
HardwareAccelerationOption, HardwareAccelerationOption,
HardwareEncoderInfo HardwareEncoderInfo,
HardwareDecoderInfo
} from './types'; } from './types';
// Utility exports // Utility exports
@@ -24,7 +25,8 @@ export {
checkAV1Support, checkAV1Support,
getVideoMetadata, getVideoMetadata,
selectAudioBitrate, selectAudioBitrate,
detectHardwareEncoders detectHardwareEncoders,
detectHardwareDecoders
} from './utils'; } from './utils';
// Profile exports // Profile exports

View File

@@ -27,6 +27,13 @@ export interface HardwareEncoderInfo {
av1Encoder?: string; av1Encoder?: string;
} }
/**
* Набор доступных декодеров/accel
*/
export interface HardwareDecoderInfo {
accelerator: HardwareAccelerator;
}
/** /**
* Quality settings for a codec * Quality settings for a codec
*/ */
@@ -73,12 +80,11 @@ export interface DashConvertOptions {
/** Streaming format to generate: 'dash', 'hls', or 'both' (default: 'both') */ /** Streaming format to generate: 'dash', 'hls', or 'both' (default: 'both') */
format?: StreamingFormat; format?: StreamingFormat;
/** Enable NVENC hardware acceleration (auto-detect if undefined) — устарело, используйте hardwareAccelerator */
useNvenc?: boolean;
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */ /** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
hardwareAccelerator?: HardwareAccelerationOption; hardwareAccelerator?: HardwareAccelerationOption;
/** Предпочитаемый аппаратный ускоритель для декодера (auto по умолчанию) */
hardwareDecoder?: HardwareAccelerationOption;
/** Quality settings for video encoding (CQ/CRF values) */ /** Quality settings for video encoding (CQ/CRF values) */
quality?: QualitySettings; quality?: QualitySettings;
@@ -195,6 +201,8 @@ export interface DashConvertResult {
/** Выбранный аппаратный ускоритель */ /** Выбранный аппаратный ускоритель */
selectedAccelerator: HardwareAccelerator; selectedAccelerator: HardwareAccelerator;
/** Выбранный аппаратный декодер */
selectedDecoder: HardwareAccelerator;
/** Codec type used for encoding */ /** Codec type used for encoding */
codecType: CodecType; codecType: CodecType;

View File

@@ -5,6 +5,7 @@ export {
checkNvenc, checkNvenc,
checkAV1Support, checkAV1Support,
detectHardwareEncoders, detectHardwareEncoders,
detectHardwareDecoders,
execFFmpeg, execFFmpeg,
execMP4Box, execMP4Box,
setLogFile setLogFile

View File

@@ -1,6 +1,6 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { appendFile } from 'node:fs/promises'; import { appendFile } from 'node:fs/promises';
import type { HardwareAccelerator, HardwareEncoderInfo } from '../types'; import type { HardwareAccelerator, HardwareDecoderInfo, HardwareEncoderInfo } from '../types';
// Global variable for log file path // Global variable for log file path
let currentLogFile: string | null = null; let currentLogFile: string | null = null;
@@ -159,6 +159,40 @@ export async function detectHardwareEncoders(): Promise<HardwareEncoderInfo[]> {
return detected; return detected;
} }
/**
* Получить список доступных аппаратных декодеров (по выводу ffmpeg -hwaccels)
*/
export async function detectHardwareDecoders(): Promise<HardwareDecoderInfo[]> {
const output: string = await new Promise((resolve) => {
const proc = spawn('ffmpeg', ['-hide_banner', '-hwaccels']);
let data = '';
proc.stdout.on('data', (chunk) => data += chunk.toString());
proc.on('error', () => resolve(''));
proc.on('close', () => resolve(data));
});
const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
const decoders: HardwareDecoderInfo[] = [];
const map: Record<string, HardwareAccelerator> = {
cuda: 'nvenc',
qsv: 'qsv',
vaapi: 'vaapi',
videotoolbox: 'videotoolbox',
v4l2m2m: 'v4l2',
dxva2: 'amf'
};
for (const line of lines) {
const acc = map[line];
if (acc) {
decoders.push({ accelerator: acc });
}
}
return decoders;
}
/** /**
* Execute FFmpeg command with progress tracking * Execute FFmpeg command with progress tracking
*/ */