fix: Исправить баг с масштабированием через VIDEOTOOLBOX ускоритель.
feat: добавлена возможность генерировать видео без звука -m --muted
This commit is contained in:
94
bin/cli.js
94
bin/cli.js
File diff suppressed because one or more lines are too long
@@ -31,6 +31,7 @@ let av1CQ: number | undefined;
|
||||
let av1CRF: number | undefined;
|
||||
let accelerator: HardwareAccelerationOption | undefined;
|
||||
let decoder: HardwareAccelerationOption | undefined;
|
||||
let muted = false;
|
||||
|
||||
// First pass: extract flags and their values
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -113,6 +114,8 @@ for (let i = 0; i < args.length; i++) {
|
||||
}
|
||||
decoder = acc as HardwareAccelerationOption;
|
||||
i++;
|
||||
} else if (args[i] === '-m' || args[i] === '--muted') {
|
||||
muted = true;
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
// Positional argument
|
||||
positionalArgs.push(args[i]);
|
||||
@@ -132,6 +135,7 @@ if (!input) {
|
||||
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)');
|
||||
console.error(' -m, --muted Disable audio track (no audio in output)');
|
||||
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-crf <value> H.264 CPU CRF value (0-51, lower = better, default: auto)');
|
||||
@@ -351,6 +355,7 @@ console.log(` Poster: ${posterPlanned} (will be generated)`);
|
||||
console.log(` Thumbnails: ${thumbnailsPlanned ? 'yes (with VTT)' : 'no'}`);
|
||||
console.log(` Encoder: ${acceleratorDisplay} (available: ${encoderListDisplay})`);
|
||||
console.log(` Decoder: ${decoderDisplay} (available: ${decoderListDisplay})`);
|
||||
console.log(` Audio: ${muted ? 'disabled (muted)' : 'enabled'}`);
|
||||
|
||||
// Build quality settings if any are specified
|
||||
let quality: QualitySettings | undefined;
|
||||
@@ -405,6 +410,7 @@ try {
|
||||
quality,
|
||||
generateThumbnails: true,
|
||||
generatePoster: true,
|
||||
muted,
|
||||
parallel: true,
|
||||
onProgress: (progress) => {
|
||||
const stageName = progress.stage === 'encoding' ? 'Encoding' :
|
||||
|
||||
@@ -51,6 +51,7 @@ export async function convertToDash(
|
||||
generatePoster: shouldGeneratePoster = true,
|
||||
posterTimecode = '00:00:00',
|
||||
parallel = true,
|
||||
muted = false,
|
||||
onProgress
|
||||
} = options;
|
||||
|
||||
@@ -97,6 +98,7 @@ Formats: ${formats?.join(',') || 'dash,hls'}
|
||||
shouldGeneratePoster,
|
||||
posterTimecode,
|
||||
parallel,
|
||||
muted,
|
||||
onProgress
|
||||
);
|
||||
} finally {
|
||||
@@ -137,6 +139,7 @@ async function convertToDashInternal(
|
||||
generatePosterFlag: boolean,
|
||||
posterTimecode: string,
|
||||
parallel: boolean,
|
||||
muted: boolean,
|
||||
onProgress?: (progress: ConversionProgress) => void
|
||||
): Promise<DashConvertResult> {
|
||||
|
||||
@@ -160,7 +163,12 @@ async function convertToDashInternal(
|
||||
|
||||
// Get video metadata
|
||||
const metadata = await getVideoMetadata(input);
|
||||
const hasAudio = metadata.hasAudio;
|
||||
const hasAudio = !muted && metadata.hasAudio;
|
||||
const durationSeconds = metadata.duration;
|
||||
|
||||
// Подгоняем длительность сегмента под общий хронометраж, чтобы не оставался короткий хвост
|
||||
const segmentCount = Math.max(1, Math.ceil(durationSeconds / segmentDuration));
|
||||
const effectiveSegmentDuration = durationSeconds / segmentCount;
|
||||
|
||||
// Determine hardware accelerator (auto by default)
|
||||
const preferredAccelerator: HardwareAccelerationOption =
|
||||
@@ -311,13 +319,14 @@ async function convertToDashInternal(
|
||||
videoCodec,
|
||||
codecPreset,
|
||||
metadata.duration,
|
||||
segmentDuration,
|
||||
effectiveSegmentDuration,
|
||||
metadata.audioBitrate,
|
||||
parallel,
|
||||
maxConcurrent,
|
||||
type, // Pass codec type to differentiate output files
|
||||
codecQuality, // Pass quality settings (CQ/CRF)
|
||||
undefined, // optimizations - for future use
|
||||
muted,
|
||||
selectedDecoder === 'cpu' ? undefined : selectedDecoder,
|
||||
(profileName, percent) => {
|
||||
const profileIndex = profiles.findIndex(p => p.name === profileName);
|
||||
@@ -350,7 +359,7 @@ async function convertToDashInternal(
|
||||
codecMP4Paths,
|
||||
videoOutputDir,
|
||||
profiles,
|
||||
segmentDuration,
|
||||
effectiveSegmentDuration,
|
||||
codecsSelected,
|
||||
formatsSelected,
|
||||
hasAudio
|
||||
|
||||
@@ -53,6 +53,7 @@ export async function encodeProfileToMP4(
|
||||
codecType: 'h264' | 'av1',
|
||||
qualitySettings?: CodecQualitySettings,
|
||||
optimizations?: VideoOptimizations,
|
||||
muted: boolean = false,
|
||||
decoderAccel?: HardwareAccelerator,
|
||||
onProgress?: (percent: number) => void
|
||||
): Promise<string> {
|
||||
@@ -157,12 +158,14 @@ export async function encodeProfileToMP4(
|
||||
|
||||
// Build video filter chain
|
||||
const filters: string[] = [];
|
||||
const targetWidth = profile.width;
|
||||
const targetHeight = profile.height;
|
||||
|
||||
if (decoderAccel === 'nvenc') {
|
||||
// CUDA path: keep frames on GPU
|
||||
filters.push(`scale_cuda=${profile.width}:${profile.height}`);
|
||||
// CUDA path: вписываем в профиль с сохранением исходного AR
|
||||
filters.push(`scale_cuda=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease:force_divisible_by=2`);
|
||||
} else {
|
||||
filters.push(`scale=${profile.width}:${profile.height}`);
|
||||
filters.push(`scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease:force_divisible_by=2`);
|
||||
}
|
||||
|
||||
// Apply optimizations (for future use)
|
||||
@@ -178,10 +181,16 @@ export async function encodeProfileToMP4(
|
||||
}
|
||||
}
|
||||
|
||||
// Центрируем кадр, чтобы браузеры (Firefox/videotoolbox) не игнорировали PAR
|
||||
filters.push(
|
||||
`pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2`,
|
||||
'setsar=1'
|
||||
);
|
||||
|
||||
args.push('-vf', filters.join(','));
|
||||
|
||||
if (!muted) {
|
||||
// Audio encoding
|
||||
// Select optimal bitrate based on source (don't upscale)
|
||||
const targetAudioBitrate = parseInt(profile.audioBitrate) || 256;
|
||||
const optimalAudioBitrate = selectAudioBitrate(sourceAudioBitrate, targetAudioBitrate);
|
||||
args.push('-c:a', 'aac', '-b:a', optimalAudioBitrate);
|
||||
@@ -190,6 +199,9 @@ export async function encodeProfileToMP4(
|
||||
if (optimizations?.audioNormalize) {
|
||||
args.push('-af', 'loudnorm');
|
||||
}
|
||||
} else {
|
||||
args.push('-an'); // без аудио дорожки
|
||||
}
|
||||
|
||||
// Output
|
||||
args.push('-f', 'mp4', outputPath);
|
||||
@@ -217,6 +229,7 @@ export async function encodeProfilesToMP4(
|
||||
codecType: 'h264' | 'av1',
|
||||
qualitySettings?: CodecQualitySettings,
|
||||
optimizations?: VideoOptimizations,
|
||||
muted: boolean = false,
|
||||
decoderAccel?: HardwareAccelerator,
|
||||
onProgress?: (profileName: string, percent: number) => void
|
||||
): Promise<Map<string, string>> {
|
||||
@@ -239,6 +252,7 @@ export async function encodeProfilesToMP4(
|
||||
codecType,
|
||||
qualitySettings,
|
||||
optimizations,
|
||||
muted,
|
||||
decoderAccel,
|
||||
(percent) => {
|
||||
if (onProgress) {
|
||||
@@ -269,6 +283,7 @@ export async function encodeProfilesToMP4(
|
||||
codecType,
|
||||
qualitySettings,
|
||||
optimizations,
|
||||
muted,
|
||||
decoderAccel,
|
||||
(percent) => {
|
||||
if (onProgress) {
|
||||
|
||||
@@ -86,6 +86,9 @@ export interface DashConvertOptions {
|
||||
/** Предпочитаемый аппаратный ускоритель для декодера (auto по умолчанию) */
|
||||
hardwareDecoder?: HardwareAccelerationOption;
|
||||
|
||||
/** Отключить аудиодорожку (muted). По умолчанию false. */
|
||||
muted?: boolean;
|
||||
|
||||
/** Quality settings for video encoding (CQ/CRF values) */
|
||||
quality?: QualitySettings;
|
||||
|
||||
|
||||
@@ -90,13 +90,16 @@ export function selectAudioBitrate(
|
||||
sourceAudioBitrate: number | undefined,
|
||||
targetBitrate: number = 256
|
||||
): string {
|
||||
const MIN_AUDIO_KBPS = 64; // не опускаться ниже базового качества
|
||||
|
||||
if (!sourceAudioBitrate) {
|
||||
// If we can't detect source bitrate, use target
|
||||
return `${targetBitrate}k`;
|
||||
}
|
||||
|
||||
// Use minimum of source and target (no upscaling)
|
||||
const optimalBitrate = Math.min(sourceAudioBitrate, targetBitrate);
|
||||
// Не занижаем слишком низко: clamp к минималке, но не выше целевого
|
||||
const clampedSource = Math.max(sourceAudioBitrate, MIN_AUDIO_KBPS);
|
||||
const optimalBitrate = Math.min(clampedSource, targetBitrate);
|
||||
|
||||
// Round to common bitrate values for consistency
|
||||
if (optimalBitrate <= 64) return '64k';
|
||||
|
||||
3
web-test/dash.all.min.js
vendored
Normal file
3
web-test/dash.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
|
||||
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/polyfill/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"></script> -->
|
||||
<script src="dash.all.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/polyfill/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"></script>
|
||||
<script src="https://unpkg.com/plyr@3"></script>
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
margin: 50px auto;
|
||||
max-width: 1500px;
|
||||
}
|
||||
video {
|
||||
max-height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -24,7 +27,8 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const source = 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd';
|
||||
const source = 'http://localhost:3000/test-videotoolbox/manifest.mpd';
|
||||
// const source = 'http://localhost:3000/test-nvenc/manifest.mpd';
|
||||
const dash = dashjs.MediaPlayer().create();
|
||||
const video = document.querySelector('video');
|
||||
dash.initialize(video, source, true);
|
||||
|
||||
Reference in New Issue
Block a user