av1 кодек

This commit is contained in:
2025-11-11 21:07:51 +03:00
parent b843bdf897
commit 2da2b584fa
9 changed files with 274 additions and 98 deletions

File diff suppressed because one or more lines are too long

View File

@@ -10,14 +10,16 @@
* dvc-cli ./video.mp4 ./output -r 720,1080 * dvc-cli ./video.mp4 ./output -r 720,1080
*/ */
import { convertToDash, checkFFmpeg, checkNvenc, checkMP4Box, getVideoMetadata } from './index'; import { convertToDash, checkFFmpeg, checkNvenc, checkMP4Box, checkAV1Support, getVideoMetadata } from './index';
import cliProgress from 'cli-progress'; import cliProgress from 'cli-progress';
import { statSync } from 'node:fs'; import { statSync } from 'node:fs';
import type { CodecType } from './types';
// Parse arguments // Parse arguments
const args = process.argv.slice(2); const args = process.argv.slice(2);
let customProfiles: string[] | undefined; let customProfiles: string[] | undefined;
let posterTimecode: string | undefined; let posterTimecode: string | undefined;
let codecType: CodecType = 'dual'; // Default to dual codec
const positionalArgs: string[] = []; const positionalArgs: string[] = [];
// First pass: extract flags and their values // First pass: extract flags and their values
@@ -43,6 +45,15 @@ for (let i = 0; i < args.length; i++) {
} else if (args[i] === '-p' || args[i] === '--poster') { } else if (args[i] === '-p' || args[i] === '--poster') {
posterTimecode = args[i + 1]; posterTimecode = args[i + 1];
i++; // Skip next arg 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;
} else {
console.error(`❌ Invalid codec: ${codec}. Valid options: av1, h264, dual`);
process.exit(1);
}
i++; // Skip next arg
} else if (!args[i].startsWith('-')) { } else if (!args[i].startsWith('-')) {
// Positional argument // Positional argument
positionalArgs.push(args[i]); positionalArgs.push(args[i]);
@@ -54,14 +65,21 @@ const input = positionalArgs[0];
const outputDir = positionalArgs[1] || '.'; // Текущая директория по умолчанию const outputDir = positionalArgs[1] || '.'; // Текущая директория по умолчанию
if (!input) { if (!input) {
console.error('❌ Usage: dvc-cli <input-video> [output-dir] [-r resolutions] [-p poster-timecode]'); console.error('❌ Usage: dvc-cli <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode]');
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(' -p, --poster Poster timecode (e.g., 00:00:05 or 10)');
console.error('\nExamples:'); console.error('\nExamples:');
console.error(' dvc-cli video.mp4'); console.error(' dvc-cli video.mp4');
console.error(' dvc-cli video.mp4 ./output'); console.error(' dvc-cli video.mp4 ./output');
console.error(' dvc-cli video.mp4 -r 360,480,720'); console.error(' dvc-cli video.mp4 -r 360,480,720');
console.error(' dvc-cli video.mp4 -r 720@60,1080@60,2160@60'); console.error(' dvc-cli video.mp4 -c av1');
console.error(' dvc-cli video.mp4 -c h264');
console.error(' dvc-cli video.mp4 -c dual');
console.error(' dvc-cli video.mp4 -r 720@60,1080@60,2160@60 -c av1');
console.error(' dvc-cli video.mp4 -p 00:00:05'); console.error(' dvc-cli video.mp4 -p 00:00:05');
console.error(' dvc-cli video.mp4 ./output -r 720,1080 -p 10'); console.error(' dvc-cli video.mp4 ./output -r 720,1080 -c dual -p 10');
process.exit(1); process.exit(1);
} }
@@ -69,10 +87,16 @@ console.log('🔍 Checking system...\n');
const hasFFmpeg = await checkFFmpeg(); const hasFFmpeg = await checkFFmpeg();
const hasNvenc = await checkNvenc(); const hasNvenc = await checkNvenc();
const av1Support = await checkAV1Support();
const hasMP4Box = await checkMP4Box(); const hasMP4Box = await checkMP4Box();
console.log(`FFmpeg: ${hasFFmpeg ? '✅' : '❌'}`); console.log(`FFmpeg: ${hasFFmpeg ? '✅' : '❌'}`);
console.log(`NVENC: ${hasNvenc ? '✅ (GPU acceleration)' : '⚠️ (CPU only)'}`); console.log(`NVENC (H.264): ${hasNvenc ? '✅ (GPU acceleration)' : '⚠️ (CPU only)'}`);
if (av1Support.available) {
console.log(`AV1 Encoder: ✅ ${av1Support.encoder} (GPU acceleration)`);
} else {
console.log(`AV1 Encoder: ⚠️ (not available, will use CPU fallback)`);
}
console.log(`MP4Box: ${hasMP4Box ? '✅' : '❌'}\n`); console.log(`MP4Box: ${hasMP4Box ? '✅' : '❌'}\n`);
if (!hasFFmpeg) { if (!hasFFmpeg) {
@@ -85,6 +109,13 @@ if (!hasMP4Box) {
process.exit(1); process.exit(1);
} }
// Validate codec selection
if ((codecType === 'av1' || codecType === 'dual') && !av1Support.available) {
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`);
}
// Get video metadata and file size // Get video metadata and file size
console.log('📊 Analyzing video...\n'); console.log('📊 Analyzing video...\n');
const metadata = await getVideoMetadata(input); const metadata = await getVideoMetadata(input);
@@ -105,6 +136,7 @@ if (metadata.audioBitrate) {
console.log(` Audio Bitrate: ${metadata.audioBitrate} kbps`); console.log(` Audio Bitrate: ${metadata.audioBitrate} kbps`);
} }
console.log(`\n📁 Output: ${outputDir}`); console.log(`\n📁 Output: ${outputDir}`);
console.log(`🎬 Codec: ${codecType}${codecType === 'dual' ? ' (AV1 + H.264 for maximum compatibility)' : ''}`);
if (customProfiles) { if (customProfiles) {
console.log(`🎯 Custom profiles: ${customProfiles.join(', ')}`); console.log(`🎯 Custom profiles: ${customProfiles.join(', ')}`);
} }
@@ -133,6 +165,7 @@ try {
outputDir, outputDir,
customProfiles, customProfiles,
posterTimecode, posterTimecode,
codec: codecType,
segmentDuration: 2, segmentDuration: 2,
useNvenc: hasNvenc, useNvenc: hasNvenc,
generateThumbnails: true, generateThumbnails: true,
@@ -182,7 +215,8 @@ try {
console.log(` Manifest: ${result.manifestPath}`); console.log(` Manifest: ${result.manifestPath}`);
console.log(` Duration: ${result.duration.toFixed(2)}s`); console.log(` Duration: ${result.duration.toFixed(2)}s`);
console.log(` Profiles: ${result.profiles.map(p => p.name).join(', ')}`); console.log(` Profiles: ${result.profiles.map(p => p.name).join(', ')}`);
console.log(` Encoder: ${result.usedNvenc ? '⚡ NVENC (GPU)' : '🔧 libx264 (CPU)'}`); console.log(` Codec: ${result.codecType}${result.codecType === 'dual' ? ' (AV1 + H.264)' : ''}`);
console.log(` Encoder: ${result.usedNvenc ? '⚡ GPU accelerated' : '🔧 CPU'}`);
if (result.posterPath) { if (result.posterPath) {
console.log(` Poster: ${result.posterPath}`); console.log(` Poster: ${result.posterPath}`);

View File

@@ -6,12 +6,14 @@ import type {
DashConvertResult, DashConvertResult,
VideoProfile, VideoProfile,
ThumbnailConfig, ThumbnailConfig,
ConversionProgress ConversionProgress,
CodecType
} from '../types'; } from '../types';
import { import {
checkFFmpeg, checkFFmpeg,
checkMP4Box, checkMP4Box,
checkNvenc, checkNvenc,
checkAV1Support,
getVideoMetadata, getVideoMetadata,
ensureDir ensureDir
} from '../utils'; } from '../utils';
@@ -33,6 +35,7 @@ export async function convertToDash(
segmentDuration = 2, segmentDuration = 2,
profiles: userProfiles, profiles: userProfiles,
customProfiles, customProfiles,
codec = 'dual',
useNvenc, useNvenc,
generateThumbnails = true, generateThumbnails = true,
thumbnailConfig = {}, thumbnailConfig = {},
@@ -54,6 +57,7 @@ export async function convertToDash(
segmentDuration, segmentDuration,
userProfiles, userProfiles,
customProfiles, customProfiles,
codec,
useNvenc, useNvenc,
generateThumbnails, generateThumbnails,
thumbnailConfig, thumbnailConfig,
@@ -82,6 +86,7 @@ async function convertToDashInternal(
segmentDuration: number, segmentDuration: number,
userProfiles: VideoProfile[] | undefined, userProfiles: VideoProfile[] | undefined,
customProfiles: string[] | undefined, customProfiles: string[] | undefined,
codec: CodecType,
useNvenc: boolean | undefined, useNvenc: boolean | undefined,
generateThumbnails: boolean, generateThumbnails: boolean,
thumbnailConfig: ThumbnailConfig, thumbnailConfig: ThumbnailConfig,
@@ -177,61 +182,92 @@ async function convertToDashInternal(
await ensureDir(videoOutputDir); await ensureDir(videoOutputDir);
reportProgress('analyzing', 20, `Using ${willUseNvenc ? 'NVENC' : 'CPU'} encoding`, undefined); // Determine which codecs to use based on codec parameter
const codecs: Array<{ type: 'h264' | 'av1'; codec: string; preset: string }> = [];
if (codec === 'h264' || codec === 'dual') {
const h264Codec = willUseNvenc ? 'h264_nvenc' : 'libx264';
const h264Preset = willUseNvenc ? 'p4' : 'medium';
codecs.push({ type: 'h264', codec: h264Codec, preset: h264Preset });
}
if (codec === 'av1' || codec === 'dual') {
// Check for AV1 hardware encoder
const av1Support = await checkAV1Support();
const av1Codec = av1Support.available ? av1Support.encoder! : 'libsvtav1';
const av1Preset = av1Support.available ? (av1Codec === 'av1_nvenc' ? 'p4' : 'medium') : '8';
codecs.push({ type: 'av1', codec: av1Codec, preset: av1Preset });
}
const codecNames = codecs.map(c => c.type.toUpperCase()).join(' + ');
reportProgress('analyzing', 20, `Using ${codecNames} encoding (${willUseNvenc ? 'GPU' : 'CPU'})`, undefined);
// Video codec selection
const videoCodec = willUseNvenc ? 'h264_nvenc' : 'libx264';
const codecPreset = willUseNvenc ? 'p4' : 'medium';
const maxConcurrent = willUseNvenc ? 3 : 2; const maxConcurrent = willUseNvenc ? 3 : 2;
// STAGE 1: Encode profiles to MP4 (parallel - heavy work) // STAGE 1: Encode profiles to MP4 for each codec (parallel - heavy work)
reportProgress('encoding', 25, `Stage 1: Encoding ${profiles.length} profiles to MP4...`); const codecMP4Paths = new Map<'h264' | 'av1', Map<string, string>>();
const tempMP4Paths = await encodeProfilesToMP4( for (let codecIndex = 0; codecIndex < codecs.length; codecIndex++) {
input, const { type, codec: videoCodec, preset: codecPreset } = codecs[codecIndex];
tempDir, const codecProgress = codecIndex / codecs.length;
profiles, const codecProgressRange = 1 / codecs.length;
videoCodec,
codecPreset,
metadata.duration,
segmentDuration,
metadata.fps || 25, // Use detected FPS or default to 25
metadata.audioBitrate, // Source audio bitrate for smart selection
parallel,
maxConcurrent,
undefined, // optimizations - for future use
(profileName, percent) => {
const profileIndex = profiles.findIndex(p => p.name === profileName);
const baseProgress = 25 + (profileIndex / profiles.length) * 40;
const profileProgress = (percent / 100) * (40 / profiles.length);
reportProgress('encoding', baseProgress + profileProgress, `Encoding ${profileName}...`, profileName);
// Also report individual profile progress reportProgress('encoding', 25 + codecProgress * 40, `Stage 1: Encoding ${type.toUpperCase()} (${profiles.length} profiles)...`);
if (onProgress) {
onProgress({ const tempMP4Paths = await encodeProfilesToMP4(
stage: 'encoding', input,
percent: baseProgress + profileProgress, tempDir,
currentProfile: profileName, profiles,
profilePercent: percent, // Actual profile progress 0-100 videoCodec,
message: `Encoding ${profileName}...` codecPreset,
}); metadata.duration,
segmentDuration,
metadata.fps || 25,
metadata.audioBitrate,
parallel,
maxConcurrent,
type, // Pass codec type to differentiate output files
undefined, // optimizations - for future use
(profileName, percent) => {
const profileIndex = profiles.findIndex(p => p.name === profileName);
const baseProgress = 25 + codecProgress * 40;
const profileProgress = (percent / 100) * (40 * codecProgressRange / profiles.length);
reportProgress('encoding', baseProgress + profileProgress, `Encoding ${type.toUpperCase()} ${profileName}...`, `${type}-${profileName}`);
// Also report individual profile progress
if (onProgress) {
onProgress({
stage: 'encoding',
percent: baseProgress + profileProgress,
currentProfile: `${type}-${profileName}`,
profilePercent: percent,
message: `Encoding ${type.toUpperCase()} ${profileName}...`
});
}
} }
} );
);
reportProgress('encoding', 65, 'Stage 1 complete: All profiles encoded'); codecMP4Paths.set(type, tempMP4Paths);
}
reportProgress('encoding', 65, 'Stage 1 complete: All codecs and profiles encoded');
// STAGE 2: Package to DASH using MP4Box (light work, fast) // STAGE 2: Package to DASH using MP4Box (light work, fast)
reportProgress('encoding', 70, `Stage 2: Creating DASH with MP4Box...`); reportProgress('encoding', 70, `Stage 2: Creating DASH with MP4Box...`);
const manifestPath = await packageToDash( const manifestPath = await packageToDash(
tempMP4Paths, codecMP4Paths,
videoOutputDir, videoOutputDir,
profiles, profiles,
segmentDuration segmentDuration,
codec
); );
const videoPaths = Array.from(tempMP4Paths.values()); // Collect all video paths from all codecs
const videoPaths: string[] = [];
for (const mp4Paths of codecMP4Paths.values()) {
videoPaths.push(...Array.from(mp4Paths.values()));
}
reportProgress('encoding', 80, 'Stage 2 complete: DASH created'); reportProgress('encoding', 80, 'Stage 2 complete: DASH created');
@@ -293,7 +329,8 @@ async function convertToDashInternal(
posterPath, posterPath,
duration: metadata.duration, duration: metadata.duration,
profiles, profiles,
usedNvenc: willUseNvenc usedNvenc: willUseNvenc,
codecType: codec
}; };
} }

View File

@@ -16,10 +16,11 @@ export async function encodeProfileToMP4(
segmentDuration: number, segmentDuration: number,
fps: number, fps: number,
sourceAudioBitrate: number | undefined, sourceAudioBitrate: number | undefined,
codecType: 'h264' | 'av1',
optimizations?: VideoOptimizations, optimizations?: VideoOptimizations,
onProgress?: (percent: number) => void onProgress?: (percent: number) => void
): Promise<string> { ): Promise<string> {
const outputPath = join(tempDir, `video_${profile.name}.mp4`); const outputPath = join(tempDir, `video_${codecType}_${profile.name}.mp4`);
const args = [ const args = [
'-y', '-y',
@@ -27,20 +28,44 @@ export async function encodeProfileToMP4(
'-c:v', videoCodec '-c:v', videoCodec
]; ];
// Add NVENC specific options // Add codec-specific options
if (videoCodec === 'h264_nvenc') { if (videoCodec === 'h264_nvenc') {
// NVIDIA H.264
args.push('-rc:v', 'vbr'); args.push('-rc:v', 'vbr');
args.push('-preset', preset); args.push('-preset', preset);
args.push('-2pass', '0'); args.push('-2pass', '0');
} else if (videoCodec === 'av1_nvenc') {
// NVIDIA AV1
args.push('-rc:v', 'vbr');
args.push('-preset', preset);
args.push('-2pass', '0');
} else if (videoCodec === 'av1_qsv') {
// Intel QSV AV1
args.push('-preset', preset);
args.push('-global_quality', '23'); // Quality level for QSV
} else if (videoCodec === 'av1_amf') {
// AMD AMF AV1
args.push('-quality', 'balanced');
args.push('-rc', 'vbr_latency');
} else if (videoCodec === 'libsvtav1') {
// CPU-based SVT-AV1
args.push('-preset', preset); // 0-13, 8 is medium speed
args.push('-svtav1-params', 'tune=0:enable-overlays=1');
} else { } else {
// Default (libx264, libx265, etc.)
args.push('-preset', preset); args.push('-preset', preset);
} }
// Video encoding parameters // Video encoding parameters
// AV1 is ~40% more efficient than H.264 at same quality (Netflix/YouTube standard)
const bitrateMultiplier = codecType === 'av1' ? 0.6 : 1.0;
const targetBitrate = Math.round(parseInt(profile.videoBitrate) * bitrateMultiplier);
const bitrateString = `${targetBitrate}k`;
args.push( args.push(
'-b:v', profile.videoBitrate, '-b:v', bitrateString,
'-maxrate', profile.videoBitrate, '-maxrate', bitrateString,
'-bufsize', `${parseInt(profile.videoBitrate) * 2}k` '-bufsize', `${targetBitrate * 2}k`
); );
// Set GOP size for DASH segments // Set GOP size for DASH segments
@@ -105,6 +130,7 @@ export async function encodeProfilesToMP4(
sourceAudioBitrate: number | undefined, sourceAudioBitrate: number | undefined,
parallel: boolean, parallel: boolean,
maxConcurrent: number, maxConcurrent: number,
codecType: 'h264' | 'av1',
optimizations?: VideoOptimizations, optimizations?: VideoOptimizations,
onProgress?: (profileName: string, percent: number) => void onProgress?: (profileName: string, percent: number) => void
): Promise<Map<string, string>> { ): Promise<Map<string, string>> {
@@ -125,6 +151,7 @@ export async function encodeProfilesToMP4(
segmentDuration, segmentDuration,
fps, fps,
sourceAudioBitrate, sourceAudioBitrate,
codecType,
optimizations, optimizations,
(percent) => { (percent) => {
if (onProgress) { if (onProgress) {
@@ -153,6 +180,7 @@ export async function encodeProfilesToMP4(
segmentDuration, segmentDuration,
fps, fps,
sourceAudioBitrate, sourceAudioBitrate,
codecType,
optimizations, optimizations,
(percent) => { (percent) => {
if (onProgress) { if (onProgress) {

View File

@@ -1,17 +1,18 @@
import { join } from 'node:path'; import { join } from 'node:path';
import { execMP4Box } from '../utils'; import { execMP4Box } from '../utils';
import type { VideoProfile } from '../types'; import type { VideoProfile, CodecType } from '../types';
/** /**
* Package MP4 files into DASH format using MP4Box * Package MP4 files into DASH format using MP4Box
* Stage 2: Light work - just packaging, no encoding * Stage 2: Light work - just packaging, no encoding
* Creates one master MPD manifest with all profiles * Creates one master MPD manifest with all profiles and codecs
*/ */
export async function packageToDash( export async function packageToDash(
mp4Files: Map<string, string>, codecMP4Files: Map<'h264' | 'av1', Map<string, string>>,
outputDir: string, outputDir: string,
profiles: VideoProfile[], profiles: VideoProfile[],
segmentDuration: number segmentDuration: number,
codecType: CodecType
): Promise<string> { ): Promise<string> {
const manifestPath = join(outputDir, 'manifest.mpd'); const manifestPath = join(outputDir, 'manifest.mpd');
@@ -20,22 +21,32 @@ export async function packageToDash(
'-dash', String(segmentDuration * 1000), // MP4Box expects milliseconds '-dash', String(segmentDuration * 1000), // MP4Box expects milliseconds
'-frag', String(segmentDuration * 1000), '-frag', String(segmentDuration * 1000),
'-rap', // Force segments to start with random access points '-rap', // Force segments to start with random access points
'-segment-timeline', // Use SegmentTimeline for accurate segment durations
'-segment-name', '$RepresentationID$_$Number$', '-segment-name', '$RepresentationID$_$Number$',
'-out', manifestPath '-out', manifestPath
]; ];
// Add all MP4 files with their profile IDs // Add all MP4 files for each codec
for (const profile of profiles) { let firstFile = true;
const mp4Path = mp4Files.get(profile.name);
if (!mp4Path) {
throw new Error(`MP4 file not found for profile: ${profile.name}`);
}
// Add video track with representation ID for (const [codec, mp4Files] of codecMP4Files.entries()) {
args.push(`${mp4Path}#video:id=${profile.name}`); for (const profile of profiles) {
// Add audio track (shared across all profiles) const mp4Path = mp4Files.get(profile.name);
if (profile === profiles[0]) { if (!mp4Path) {
args.push(`${mp4Path}#audio:id=audio`); throw new Error(`MP4 file not found for profile: ${profile.name}, codec: ${codec}`);
}
// Representation ID includes codec: e.g., "720p-h264", "720p-av1"
const representationId = codecType === 'dual' ? `${profile.name}-${codec}` : profile.name;
// Add video track with representation ID
args.push(`${mp4Path}#video:id=${representationId}`);
// Add audio track only once (shared across all profiles and codecs)
if (firstFile) {
args.push(`${mp4Path}#audio:id=audio`);
firstFile = false;
}
} }
} }
@@ -44,10 +55,10 @@ export async function packageToDash(
// MP4Box creates files in the same directory as output MPD // MP4Box creates files in the same directory as output MPD
// Move segment files to profile subdirectories for clean structure // Move segment files to profile subdirectories for clean structure
await organizeSegments(outputDir, profiles); await organizeSegments(outputDir, profiles, codecType);
// Update MPD to reflect new file structure with subdirectories // Update MPD to reflect new file structure with subdirectories
await updateManifestPaths(manifestPath, profiles); await updateManifestPaths(manifestPath, profiles, codecType);
return manifestPath; return manifestPath;
} }
@@ -58,14 +69,27 @@ export async function packageToDash(
*/ */
async function organizeSegments( async function organizeSegments(
outputDir: string, outputDir: string,
profiles: VideoProfile[] profiles: VideoProfile[],
codecType: CodecType
): Promise<void> { ): Promise<void> {
const { readdir, rename, mkdir } = await import('node:fs/promises'); const { readdir, rename, mkdir } = await import('node:fs/promises');
// Create profile subdirectories // For dual-codec mode, create codec-specific subdirectories (e.g., "720p-h264/", "720p-av1/")
for (const profile of profiles) { // For single-codec mode, use simple profile names (e.g., "720p/")
const profileDir = join(outputDir, profile.name); const codecs: Array<'h264' | 'av1'> = [];
await mkdir(profileDir, { recursive: true }); if (codecType === 'h264' || codecType === 'dual') codecs.push('h264');
if (codecType === 'av1' || codecType === 'dual') codecs.push('av1');
const representationIds: string[] = [];
for (const codec of codecs) {
for (const profile of profiles) {
const repId = codecType === 'dual' ? `${profile.name}-${codec}` : profile.name;
representationIds.push(repId);
const profileDir = join(outputDir, repId);
await mkdir(profileDir, { recursive: true });
}
} }
// Create audio subdirectory // Create audio subdirectory
@@ -90,11 +114,11 @@ async function organizeSegments(
continue; continue;
} }
// Move video segment files to their profile directories // Move video segment files to their representation directories
for (const profile of profiles) { for (const repId of representationIds) {
if (file.startsWith(`${profile.name}_`)) { if (file.startsWith(`${repId}_`)) {
const oldPath = join(outputDir, file); const oldPath = join(outputDir, file);
const newPath = join(outputDir, profile.name, file); const newPath = join(outputDir, repId, file);
await rename(oldPath, newPath); await rename(oldPath, newPath);
break; break;
} }
@@ -107,7 +131,8 @@ async function organizeSegments(
*/ */
async function updateManifestPaths( async function updateManifestPaths(
manifestPath: string, manifestPath: string,
profiles: VideoProfile[] profiles: VideoProfile[],
codecType: CodecType
): Promise<void> { ): Promise<void> {
const { readFile, writeFile } = await import('node:fs/promises'); const { readFile, writeFile } = await import('node:fs/promises');

View File

@@ -9,7 +9,8 @@ export type {
ThumbnailConfig, ThumbnailConfig,
ConversionProgress, ConversionProgress,
VideoMetadata, VideoMetadata,
VideoOptimizations VideoOptimizations,
CodecType
} from './types'; } from './types';
// Utility exports // Utility exports
@@ -17,6 +18,7 @@ export {
checkFFmpeg, checkFFmpeg,
checkMP4Box, checkMP4Box,
checkNvenc, checkNvenc,
checkAV1Support,
getVideoMetadata, getVideoMetadata,
selectAudioBitrate selectAudioBitrate
} from './utils'; } from './utils';

View File

@@ -1,3 +1,8 @@
/**
* Video codec type for encoding
*/
export type CodecType = 'av1' | 'h264' | 'dual';
/** /**
* Configuration options for DASH conversion * Configuration options for DASH conversion
*/ */
@@ -17,6 +22,9 @@ export interface DashConvertOptions {
/** Custom resolution profiles as strings (e.g., ['360p', '480p', '720p@60']) */ /** Custom resolution profiles as strings (e.g., ['360p', '480p', '720p@60']) */
customProfiles?: string[]; customProfiles?: string[];
/** Video codec to use: 'av1', 'h264', or 'dual' for both (default: 'dual') */
codec?: CodecType;
/** Enable NVENC hardware acceleration (auto-detect if undefined) */ /** Enable NVENC hardware acceleration (auto-detect if undefined) */
useNvenc?: boolean; useNvenc?: boolean;
@@ -123,6 +131,9 @@ export interface DashConvertResult {
/** Whether NVENC was used */ /** Whether NVENC was used */
usedNvenc: boolean; usedNvenc: boolean;
/** Codec type used for encoding */
codecType: CodecType;
} }
/** /**

View File

@@ -3,6 +3,7 @@ export {
checkFFmpeg, checkFFmpeg,
checkMP4Box, checkMP4Box,
checkNvenc, checkNvenc,
checkAV1Support,
execFFmpeg, execFFmpeg,
execMP4Box execMP4Box
} from './system'; } from './system';

View File

@@ -45,6 +45,42 @@ export async function checkNvenc(): Promise<boolean> {
}); });
} }
/**
* Check if AV1 hardware encoding is available
* Supports: NVENC (RTX 40xx), QSV (Intel 11+), AMF (AMD RX 7000)
*/
export async function checkAV1Support(): Promise<{
available: boolean;
encoder?: 'av1_nvenc' | 'av1_qsv' | 'av1_amf';
}> {
return new Promise((resolve) => {
const proc = spawn('ffmpeg', ['-hide_banner', '-encoders']);
let output = '';
proc.stdout.on('data', (data) => {
output += data.toString();
});
proc.on('error', () => resolve({ available: false }));
proc.on('close', (code) => {
if (code !== 0) {
resolve({ available: false });
} else {
// Check for hardware AV1 encoders in order of preference
if (output.includes('av1_nvenc')) {
resolve({ available: true, encoder: 'av1_nvenc' });
} else if (output.includes('av1_qsv')) {
resolve({ available: true, encoder: 'av1_qsv' });
} else if (output.includes('av1_amf')) {
resolve({ available: true, encoder: 'av1_amf' });
} else {
resolve({ available: false });
}
}
});
});
}
/** /**
* Execute FFmpeg command with progress tracking * Execute FFmpeg command with progress tracking
*/ */