Files
dvc-cli/src/core/converter.ts

230 lines
6.5 KiB
TypeScript
Raw Normal View History

2025-11-08 19:41:20 +03:00
import { join, basename, extname } from 'node:path';
import { randomUUID } from 'node:crypto';
import { rm } from 'node:fs/promises';
import type {
DashConvertOptions,
DashConvertResult,
VideoProfile,
ThumbnailConfig,
ConversionProgress
2025-11-09 01:28:42 +03:00
} from '../types';
2025-11-08 19:41:20 +03:00
import {
checkFFmpeg,
checkMP4Box,
checkNvenc,
getVideoMetadata,
ensureDir
2025-11-09 01:28:42 +03:00
} from '../utils';
import { selectProfiles } from '../config/profiles';
2025-11-08 19:41:20 +03:00
import { generateThumbnailSprite } from './thumbnails';
import { encodeProfilesToMP4 } from './encoding';
import { packageToDash } from './packaging';
/**
* Convert video to DASH format with NVENC acceleration
* Two-stage approach: FFmpeg encoding MP4Box packaging
*/
export async function convertToDash(
options: DashConvertOptions
): Promise<DashConvertResult> {
const {
input,
outputDir,
segmentDuration = 2,
profiles: userProfiles,
useNvenc,
generateThumbnails = true,
thumbnailConfig = {},
parallel = true,
onProgress
} = options;
// Create unique temp directory
const tempDir = join('/tmp', `dash-converter-${randomUUID()}`);
await ensureDir(tempDir);
try {
return await convertToDashInternal(
input,
outputDir,
tempDir,
segmentDuration,
userProfiles,
useNvenc,
generateThumbnails,
thumbnailConfig,
parallel,
onProgress
);
} finally {
// Cleanup temp directory
try {
await rm(tempDir, { recursive: true, force: true });
} catch (err) {
console.warn(`Warning: Failed to cleanup temp directory: ${tempDir}`);
}
}
}
/**
* Internal conversion logic
*/
async function convertToDashInternal(
input: string,
outputDir: string,
tempDir: string,
segmentDuration: number,
userProfiles: VideoProfile[] | undefined,
useNvenc: boolean | undefined,
generateThumbnails: boolean,
thumbnailConfig: ThumbnailConfig,
parallel: boolean,
onProgress?: (progress: ConversionProgress) => void
): Promise<DashConvertResult> {
// Validate dependencies
if (!await checkFFmpeg()) {
throw new Error('FFmpeg is not installed or not in PATH');
}
if (!await checkMP4Box()) {
throw new Error('MP4Box is not installed or not in PATH. Install gpac package.');
}
// Report progress
const reportProgress = (stage: ConversionProgress['stage'], percent: number, message?: string, currentProfile?: string) => {
if (onProgress) {
onProgress({ stage, percent, message, currentProfile });
}
};
reportProgress('analyzing', 0, 'Analyzing input video...');
// Get video metadata
const metadata = await getVideoMetadata(input);
// Check NVENC availability
const nvencAvailable = useNvenc !== false ? await checkNvenc() : false;
const willUseNvenc = useNvenc === true ? true : (useNvenc === false ? false : nvencAvailable);
if (useNvenc === true && !nvencAvailable) {
throw new Error('NVENC requested but not available. Check NVIDIA drivers and GPU support.');
}
// Select profiles
const profiles = userProfiles || selectProfiles(metadata.width, metadata.height);
if (profiles.length === 0) {
throw new Error('No suitable profiles found for input video resolution');
}
// Create video name directory
const inputBasename = basename(input, extname(input));
const videoOutputDir = join(outputDir, inputBasename);
await ensureDir(videoOutputDir);
reportProgress('analyzing', 20, `Using ${willUseNvenc ? 'NVENC' : 'CPU'} encoding`, undefined);
// Video codec selection
const videoCodec = willUseNvenc ? 'h264_nvenc' : 'libx264';
const codecPreset = willUseNvenc ? 'p4' : 'medium';
const maxConcurrent = willUseNvenc ? 3 : 2;
// STAGE 1: Encode profiles to MP4 (parallel - heavy work)
reportProgress('encoding', 25, `Stage 1: Encoding ${profiles.length} profiles to MP4...`);
const tempMP4Paths = await encodeProfilesToMP4(
input,
tempDir,
profiles,
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
if (onProgress) {
onProgress({
stage: 'encoding',
percent: baseProgress + profileProgress,
currentProfile: profileName,
profilePercent: percent, // Actual profile progress 0-100
message: `Encoding ${profileName}...`
});
}
}
);
reportProgress('encoding', 65, 'Stage 1 complete: All profiles encoded');
// STAGE 2: Package to DASH using MP4Box (light work, fast)
reportProgress('encoding', 70, `Stage 2: Creating DASH with MP4Box...`);
const manifestPath = await packageToDash(
tempMP4Paths,
videoOutputDir,
profiles,
segmentDuration
);
const videoPaths = Array.from(tempMP4Paths.values());
reportProgress('encoding', 80, 'Stage 2 complete: DASH created');
// Generate thumbnails
let thumbnailSpritePath: string | undefined;
let thumbnailVttPath: string | undefined;
if (generateThumbnails) {
reportProgress('thumbnails', 80, 'Generating thumbnail sprites...');
const thumbConfig: Required<ThumbnailConfig> = {
width: thumbnailConfig.width || 160,
height: thumbnailConfig.height || 90,
interval: thumbnailConfig.interval || 1, // 1 секунда по умолчанию
columns: thumbnailConfig.columns || 10
};
const thumbResult = await generateThumbnailSprite(
input,
videoOutputDir,
metadata.duration,
thumbConfig
);
thumbnailSpritePath = thumbResult.spritePath;
thumbnailVttPath = thumbResult.vttPath;
reportProgress('thumbnails', 90, 'Thumbnails generated');
}
// Generate MPD manifest
reportProgress('manifest', 95, 'Finalizing manifest...');
// Note: manifestPath is already created by MP4Box in packageToDash
// No need for separate generateManifest function
reportProgress('complete', 100, 'Conversion complete!');
return {
manifestPath,
videoPaths,
thumbnailSpritePath,
thumbnailVttPath,
duration: metadata.duration,
profiles,
usedNvenc: willUseNvenc
};
}