fix: Исправление выбора энкодер/декодер
This commit is contained in:
163
src/cli.ts
163
src/cli.ts
@@ -10,19 +10,19 @@
|
||||
* create-vod ./video.mp4 ./output -r 720,1080
|
||||
*/
|
||||
|
||||
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders, detectHardwareDecoders } from './index';
|
||||
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders, detectHardwareDecoders, testEncoder, testDecoder } from './index';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { statSync } from 'node:fs';
|
||||
import { basename, extname } from 'node:path';
|
||||
import type { CodecType, StreamingFormat, QualitySettings, HardwareAccelerationOption } from './types';
|
||||
import type { CodecChoice, StreamingFormatChoice, QualitySettings, HardwareAccelerationOption, HardwareAccelerator } from './types';
|
||||
import { selectProfiles, createProfilesFromStrings } from './config/profiles';
|
||||
|
||||
// Parse arguments
|
||||
const args = process.argv.slice(2);
|
||||
let customProfiles: string[] | undefined;
|
||||
let posterTimecode: string | undefined;
|
||||
let codecType: CodecType = 'dual'; // Default to dual codec
|
||||
let formatType: StreamingFormat = 'both'; // Default to both formats (DASH + HLS)
|
||||
let codecChoice: CodecChoice = 'auto'; // h264 + AV1 if HW
|
||||
let formatChoice: StreamingFormatChoice = 'auto'; // DASH + HLS
|
||||
const positionalArgs: string[] = [];
|
||||
|
||||
// Quality settings
|
||||
@@ -58,19 +58,19 @@ for (let i = 0; i < args.length; i++) {
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '-c' || args[i] === '--codec') {
|
||||
const codec = args[i + 1];
|
||||
if (codec === 'av1' || codec === 'h264' || codec === 'dual') {
|
||||
codecType = codec;
|
||||
if (codec === 'av1' || codec === 'h264') {
|
||||
codecChoice = codec;
|
||||
} else {
|
||||
console.error(`❌ Invalid codec: ${codec}. Valid options: av1, h264, dual`);
|
||||
console.error(`❌ Invalid codec: ${codec}. Valid options: av1, h264`);
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '-f' || args[i] === '--format') {
|
||||
const format = args[i + 1];
|
||||
if (format === 'dash' || format === 'hls' || format === 'both') {
|
||||
formatType = format;
|
||||
if (format === 'dash' || format === 'hls') {
|
||||
formatChoice = format;
|
||||
} else {
|
||||
console.error(`❌ Invalid format: ${format}. Valid options: dash, hls, both`);
|
||||
console.error(`❌ Invalid format: ${format}. Valid options: dash, hls`);
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip next arg
|
||||
@@ -102,7 +102,7 @@ for (let i = 0; i < args.length; i++) {
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '--accel' || args[i] === '--hardware' || args[i] === '-e' || args[i] === '--encoder') {
|
||||
} else if (args[i] === '-e' || args[i] === '--encoder') {
|
||||
const acc = args[i + 1];
|
||||
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu', 'vaapi', 'videotoolbox', 'v4l2'];
|
||||
if (!allowed.includes(acc as HardwareAccelerationOption)) {
|
||||
@@ -134,8 +134,8 @@ if (!input) {
|
||||
console.error('❌ Usage: create-vod <input-video> [output-dir] [options]');
|
||||
console.error('\nOptions:');
|
||||
console.error(' -r, --resolutions Video resolutions (e.g., 360,480,720 or 720@60,1080@60)');
|
||||
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(' -c, --codec Video codec: av1 or h264 (default: auto = h264 + AV1 if HW)');
|
||||
console.error(' -f, --format Streaming format: dash or hls (default: auto = dash + hls)');
|
||||
console.error(' -p, --poster Poster timecode (e.g., 00:00:05 or 10)');
|
||||
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)');
|
||||
@@ -149,12 +149,11 @@ if (!input) {
|
||||
console.error(' create-vod video.mp4 ./output');
|
||||
console.error(' create-vod video.mp4 -r 360,480,720');
|
||||
console.error(' create-vod video.mp4 -c av1 --av1-cq 40');
|
||||
console.error(' create-vod video.mp4 -c dual --h264-cq 30 --av1-cq 39');
|
||||
console.error(' create-vod video.mp4 -c h264 --h264-cq 30');
|
||||
console.error(' create-vod video.mp4 -f hls');
|
||||
console.error(' create-vod video.mp4 -c dual -f both');
|
||||
console.error(' create-vod video.mp4 -r 720@60,1080@60,2160@60 -c av1 -f dash');
|
||||
console.error(' create-vod video.mp4 -p 00:00:05');
|
||||
console.error(' create-vod video.mp4 ./output -r 720,1080 -c dual -f both -p 10 --h264-cq 28 --av1-cq 37');
|
||||
console.error(' create-vod video.mp4 ./output -r 720,1080 -p 10 --h264-cq 28');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -163,8 +162,8 @@ console.log('🔍 Checking system...\n');
|
||||
const hasFFmpeg = await checkFFmpeg();
|
||||
const hasMP4Box = await checkMP4Box();
|
||||
const hwEncoders = await detectHardwareEncoders();
|
||||
const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder);
|
||||
const hwDecoders = await detectHardwareDecoders();
|
||||
const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder);
|
||||
|
||||
const accelPriority: Record<string, number> = {
|
||||
nvenc: 100,
|
||||
@@ -172,31 +171,77 @@ const accelPriority: Record<string, number> = {
|
||||
amf: 80,
|
||||
vaapi: 70,
|
||||
videotoolbox: 65,
|
||||
v4l2: 60
|
||||
v4l2: 60,
|
||||
cpu: 1
|
||||
};
|
||||
|
||||
const bestAccel = hwEncoders
|
||||
const encoderMap: Record<string, string> = {
|
||||
nvenc: 'h264_nvenc',
|
||||
qsv: 'h264_qsv',
|
||||
amf: 'h264_amf',
|
||||
vaapi: 'h264_vaapi',
|
||||
videotoolbox: 'h264_videotoolbox',
|
||||
v4l2: 'h264_v4l2m2m',
|
||||
cpu: 'libx264'
|
||||
};
|
||||
|
||||
const encoderCandidates = Array.from(new Set([...hwEncoders.map(e => e.accelerator), 'cpu']));
|
||||
const decoderCandidates = Array.from(new Set([...hwDecoders.map(d => d.accelerator), 'cpu']));
|
||||
|
||||
async function filterEncoders() {
|
||||
const result: HardwareAccelerationOption[] = [];
|
||||
for (const acc of encoderCandidates) {
|
||||
if (acc === 'amf') {
|
||||
continue;
|
||||
}
|
||||
const encoderName = encoderMap[acc] || 'libx264';
|
||||
const ok = await testEncoder(encoderName);
|
||||
if (ok) result.push(acc as HardwareAccelerationOption);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function filterDecoders() {
|
||||
const result: HardwareAccelerationOption[] = [];
|
||||
for (const acc of decoderCandidates) {
|
||||
if (acc === 'cpu') {
|
||||
result.push('cpu');
|
||||
continue;
|
||||
}
|
||||
const ok = await testDecoder(acc as HardwareAccelerator, input);
|
||||
if (ok) result.push(acc as HardwareAccelerationOption);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const availableEncoders = await filterEncoders();
|
||||
const availableDecoders = await filterDecoders();
|
||||
|
||||
const bestAccel = availableEncoders
|
||||
.slice()
|
||||
.sort((a, b) => (accelPriority[b.accelerator] || 0) - (accelPriority[a.accelerator] || 0))[0];
|
||||
.sort((a, b) => (accelPriority[b] || 0) - (accelPriority[a] || 0))[0];
|
||||
|
||||
const bestDecoder = availableDecoders
|
||||
.slice()
|
||||
.sort((a, b) => (accelPriority[b] || 0) - (accelPriority[a] || 0))[0];
|
||||
|
||||
console.log(`FFmpeg: ${hasFFmpeg ? '✅' : '❌'}`);
|
||||
console.log(`MP4Box: ${hasMP4Box ? '✅' : '❌'}`);
|
||||
const accelList = Array.from(new Set(hwEncoders.map(e => e.accelerator.toUpperCase())));
|
||||
const bestAccelName = bestAccel ? bestAccel.accelerator.toUpperCase() : undefined;
|
||||
const accelRest = accelList.filter(name => name !== bestAccelName);
|
||||
const accelList = Array.from(new Set(availableEncoders.map(e => e.toUpperCase())));
|
||||
const decList = Array.from(new Set(availableDecoders.map(d => d.toUpperCase())));
|
||||
|
||||
const encoderSelectedPlanned = accelerator
|
||||
? accelerator.toUpperCase()
|
||||
: (bestAccelName || 'CPU');
|
||||
: ((bestAccel && bestAccel.toUpperCase()) || 'CPU');
|
||||
const encoderAll = accelList.length > 0 ? accelList : ['CPU'];
|
||||
|
||||
const decList = Array.from(new Set(hwDecoders.map((d) => d.accelerator.toUpperCase())));
|
||||
const decoderSelectedPlanned = decoder
|
||||
? decoder.toUpperCase()
|
||||
: (decList[0] || 'CPU');
|
||||
: ((bestDecoder && bestDecoder.toUpperCase()) || 'CPU');
|
||||
const decoderAll = decList.length > 0 ? decList : ['CPU'];
|
||||
|
||||
console.log(`Encoder: ${encoderSelectedPlanned === 'AUTO' ? (bestAccelName || 'CPU') : encoderSelectedPlanned} (${encoderAll.join(', ')})`);
|
||||
console.log(`Decoder: ${decoderSelectedPlanned === 'AUTO' ? (decList[0] || 'CPU') : decoderSelectedPlanned} (${decoderAll.join(', ')})`);
|
||||
console.log(`Encoder: ${encoderSelectedPlanned === 'AUTO' ? ((bestAccel && bestAccel.toUpperCase()) || 'CPU') : encoderSelectedPlanned} (${encoderAll.join(', ')})`);
|
||||
console.log(`Decoder: ${decoderSelectedPlanned === 'AUTO' ? ((bestDecoder && bestDecoder.toUpperCase()) || 'CPU') : decoderSelectedPlanned} (${decoderAll.join(', ')})`);
|
||||
console.log('');
|
||||
|
||||
if (!hasFFmpeg) {
|
||||
@@ -209,22 +254,28 @@ if (!hasMP4Box) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate codec selection
|
||||
if ((codecType === 'av1' || codecType === 'dual') && !hasAv1Hardware) {
|
||||
if (codecType === 'av1') {
|
||||
console.error(`⚠️ Warning: AV1 encoding requested but no hardware AV1 encoder found.`);
|
||||
console.error(` CPU-based AV1 encoding (libsvtav1) will be VERY slow.`);
|
||||
console.error(` Consider using --codec h264 for faster encoding.\n`);
|
||||
} else if (codecType === 'dual') {
|
||||
console.warn(`⚠️ AV1 hardware encoder not detected. Using H.264 only (disable AV1).`);
|
||||
codecType = 'h264';
|
||||
}
|
||||
// Resolve codec selection
|
||||
let includeH264 = codecChoice === 'h264' || codecChoice === 'auto';
|
||||
let includeAv1 = codecChoice === 'av1' || codecChoice === 'auto';
|
||||
|
||||
if (includeAv1 && !hasAv1Hardware && codecChoice === 'auto') {
|
||||
includeAv1 = false;
|
||||
}
|
||||
|
||||
if (codecChoice === 'av1' && !hasAv1Hardware) {
|
||||
console.error(`⚠️ Warning: AV1 encoding requested but no hardware AV1 encoder found.`);
|
||||
console.error(` CPU-based AV1 encoding (libsvtav1) will be VERY slow.`);
|
||||
console.error(` Consider using --codec h264 for faster encoding.\n`);
|
||||
}
|
||||
|
||||
// Resolve formats
|
||||
const wantDash = formatChoice === 'dash' || formatChoice === 'auto';
|
||||
const wantHls = formatChoice === 'hls' || formatChoice === 'auto';
|
||||
|
||||
// Validate HLS requires H.264
|
||||
if ((formatType === 'hls' || formatType === 'both') && codecType === 'av1') {
|
||||
if (wantHls && !includeH264) {
|
||||
console.error(`❌ Error: HLS format requires H.264 codec for Safari/iOS compatibility.`);
|
||||
console.error(` Please use --codec h264 or --codec dual with --format hls\n`);
|
||||
console.error(` Please use --codec h264 or omit --codec to keep H.264.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -281,27 +332,31 @@ if (customProfiles && customProfiles.length > 0) {
|
||||
displayProfiles = autoProfiles.map(p => p.name);
|
||||
}
|
||||
|
||||
const manifestDesc =
|
||||
formatType === 'both' ? 'DASH (manifest.mpd), HLS (master.m3u8)' :
|
||||
formatType === 'dash' ? 'DASH (manifest.mpd)' : 'HLS (master.m3u8)';
|
||||
const manifestDesc = [
|
||||
wantDash ? 'DASH (manifest.mpd)' : null,
|
||||
wantHls ? 'HLS (master.m3u8)' : null
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
const thumbnailsPlanned = true;
|
||||
const posterPlanned = posterTimecode || '00:00:00';
|
||||
const codecDisplay = codecType === 'dual' ? 'dual (AV1 + H.264)' : codecType;
|
||||
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;
|
||||
const codecListDisplay = [
|
||||
includeH264 ? 'h264' : null,
|
||||
includeAv1 ? 'av1' : null
|
||||
].filter(Boolean).join(', ');
|
||||
const codecNote = (!includeAv1 && codecChoice === 'auto' && !hasAv1Hardware) ? ' (AV1 disabled: no HW)' : '';
|
||||
const bestAccelName = (bestAccel && bestAccel.toUpperCase()) || 'CPU';
|
||||
const bestDecoderName = (bestDecoder && bestDecoder.toUpperCase()) || 'CPU';
|
||||
const plannedAccel = accelerator ? accelerator.toUpperCase() : bestAccelName;
|
||||
const plannedDecoder = decoder ? decoder.toUpperCase() : bestDecoderName;
|
||||
const acceleratorDisplay = plannedAccel === 'AUTO' ? bestAccelName : plannedAccel;
|
||||
const decoderDisplay = plannedDecoder === 'AUTO' ? bestDecoderName : plannedDecoder;
|
||||
const encoderListDisplay = encoderAll.join(', ');
|
||||
const decoderListDisplay = decoderAll.join(', ');
|
||||
|
||||
console.log('\n📦 Parameters:');
|
||||
console.log(` Input: ${input}`);
|
||||
console.log(` Output: ${outputDir}`);
|
||||
console.log(` Codec: ${codecDisplay}${codecNote}`);
|
||||
console.log(` Codec: ${codecListDisplay}${codecNote}`);
|
||||
console.log(` Profiles: ${displayProfiles.join(', ')}`);
|
||||
console.log(` Manifests: ${manifestDesc}`);
|
||||
console.log(` Poster: ${posterPlanned} (will be generated)`);
|
||||
@@ -352,8 +407,8 @@ try {
|
||||
outputDir,
|
||||
customProfiles,
|
||||
posterTimecode,
|
||||
codec: codecType,
|
||||
format: formatType,
|
||||
codec: codecChoice,
|
||||
format: formatChoice,
|
||||
segmentDuration: 2,
|
||||
hardwareAccelerator: accelerator,
|
||||
hardwareDecoder: decoder,
|
||||
|
||||
Reference in New Issue
Block a user