Compare commits

..

13 Commits

Author SHA1 Message Date
b40ae34387 0.1.15 2026-01-22 11:44:03 +03:00
84231d705f style: Update README 2026-01-22 11:44:00 +03:00
2c8d9d1e9e 0.1.14 2026-01-22 11:39:57 +03:00
41fe1a7370 style: Update README 2026-01-22 11:39:51 +03:00
55fb1f640a 0.1.13 2026-01-22 11:14:47 +03:00
4293b6735a chore: update files 2026-01-22 11:14:21 +03:00
248fe15b62 0.1.12 2026-01-22 11:09:08 +03:00
81add91669 style: update Readme 2026-01-22 11:08:26 +03:00
b6c191290c 0.1.11 2026-01-22 10:44:41 +03:00
5ab30eee4c build cli 2026-01-22 10:44:39 +03:00
187697eca6 fix: Замена кодека при GPU скейлинге на nv12
fix: Исправлена проблема с звуком и телепортами по частям видео
2026-01-22 10:43:54 +03:00
346eb697cf 0.1.10 2026-01-22 09:52:12 +03:00
b8f9f0e046 fix: Исправить баг с масштабированием через VIDEOTOOLBOX ускоритель.
feat: добавлена возможность генерировать видео без звука -m --muted
2026-01-22 09:51:32 +03:00
11 changed files with 183 additions and 103 deletions

View File

@@ -28,20 +28,29 @@ brew install ffmpeg gpac
Before running, make sure `FFmpeg` and `MP4Box` are installed (see Install). Before running, make sure `FFmpeg` and `MP4Box` are installed (see Install).
```bash ```bash
# Run via npx (no install)
npx @gromlab/create-vod video.mp4 npx @gromlab/create-vod video.mp4
# Or install globally
npm install -g @gromlab/create-vod
create-vod video.mp4
``` ```
**Output:** A folder `video/` in the current directory with segments under `{profile}-{codec}/`, DASH/HLS manifests in the root, poster, and thumbnail sprite/VTT (both DASH and HLS are always generated). **Output:** In the current directory you'll get:
```
video/
├── manifest.mpd # DASH manifest
├── master.m3u8 # HLS master playlist
├── poster.jpg # Poster frame
├── thumbnails.{jpg,vtt} # Sprite + VTT cues
├── audio/ # Audio init + segments (AAC)
├── 1080p/ # H.264 1080p init + segments
├── 720p/ # H.264 720p
├── 480p/ # H.264 480p
├── 360p/ # H.264 360p
├── 1080p-av1/ # AV1 1080p (if av1 selected)
└── ... # Other profiles/codecs as {profile}-{codec}
```
## CLI Usage ## CLI Usage
```bash ```bash
create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode] [-e encoder] [-d decoder] create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode] [-e encoder] [-d decoder] [-m]
``` ```
### Main arguments ### Main arguments
@@ -60,33 +69,37 @@ create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-tim
| `-p, --poster` | Poster timecode | `HH:MM:SS` or seconds | `00:00:00` | `-p 00:00:05` or `-p 10` | | `-p, --poster` | Poster timecode | `HH:MM:SS` or seconds | `00:00:00` | `-p 00:00:05` or `-p 10` |
| `-e, --encoder` | Video encoder | `auto`, `nvenc`, `qsv`, `amf`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-e nvenc` | | `-e, --encoder` | Video encoder | `auto`, `nvenc`, `qsv`, `amf`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-e nvenc` |
| `-d, --decoder` | Video decoder (hwaccel) | `auto`, `nvenc`, `qsv`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-d cpu` | | `-d, --decoder` | Video decoder (hwaccel) | `auto`, `nvenc`, `qsv`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-d cpu` |
| `-m, --muted` | Disable audio track | flag | off | `-m` |
### Examples ### Examples
```bash ```bash
# Default (DASH + HLS, auto profiles) # Default (DASH + HLS, auto profiles)
create-vod video.mp4 npx @gromlab/create-vod video.mp4
# Custom output directory # Custom output directory
create-vod video.mp4 ./output npx @gromlab/create-vod video.mp4 ./output
# Selected resolutions # Selected resolutions
create-vod video.mp4 -r 720,1080,1440 npx @gromlab/create-vod video.mp4 -r 720,1080,1440
# High FPS # High FPS
create-vod video.mp4 -r 720@60,1080@60 npx @gromlab/create-vod video.mp4 -r 720@60,1080@60
# Poster from 5th second # Poster from 5th second
create-vod video.mp4 -p 5 npx @gromlab/create-vod video.mp4 -p 5
# Force CPU encode/decode # Force CPU encode/decode
create-vod video.mp4 -c h264 -e cpu -d cpu npx @gromlab/create-vod video.mp4 -c h264 -e cpu -d cpu
# Force GPU encode + CPU decode # Force GPU encode + CPU decode
create-vod video.mp4 -c h264 -e nvenc -d cpu npx @gromlab/create-vod video.mp4 -c h264 -e nvenc -d cpu
# Combined parameters # Combined parameters
create-vod video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10 npx @gromlab/create-vod video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10
# No audio
npx @gromlab/create-vod video.mp4 -m
``` ```
### Supported resolutions ### Supported resolutions

View File

@@ -27,20 +27,30 @@ brew install ffmpeg gpac
## Быстрый старт ## Быстрый старт
Перед запуском убедитесь, что в системе установлены `FFmpeg` и `MP4Box` (см. Install). Перед запуском убедитесь, что в системе установлены `FFmpeg` и `MP4Box` (см. Install).
```bash ```bash
# Использование через npx (без установки)
npx @gromlab/create-vod video.mp4 npx @gromlab/create-vod video.mp4
# Или глобальная установка
npm install -g @gromlab/create-vod
create-vod video.mp4
``` ```
**Результат:** В текущей директории будет создана папка `video/` с сегментами в папках `{profile}-{codec}/`, манифестами DASH и HLS в корне, постером и превью спрайтами. **Результат:** В текущей директории появится структура выходных файлов:
```
video/
├── manifest.mpd # DASH манифест
├── master.m3u8 # HLS мастер-плейлист
├── poster.jpg # Постер с указанного таймкода
├── thumbnails.jpg # Спрайт превью
├── thumbnails.vtt # Таймкоды превью
├── audio/ # Аудиосегменты (init + m4s)
├── 1080p/ # Сегменты H.264 1080p
├── 720p/ # Сегменты H.264 720p
├── 480p/ # Сегменты H.264 480p
├── 360p/ # Сегменты H.264 360p
├── 1080p-av1/ # Сегменты AV1 1080p (если выбран av1)
└── ... # Остальные профили/кодеки по схеме {profile}-{codec}
```
## Параметры CLI ## Параметры CLI
```bash ```bash
create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode] [-e encoder] [-d decoder] create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode] [-e encoder] [-d decoder] [-m]
``` ```
### Основные параметры ### Основные параметры
@@ -59,27 +69,31 @@ create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-tim
| `-p, --poster` | Таймкод для постера | `HH:MM:SS` или секунды | `00:00:00` | `-p 00:00:05` или `-p 10` | | `-p, --poster` | Таймкод для постера | `HH:MM:SS` или секунды | `00:00:00` | `-p 00:00:05` или `-p 10` |
| `-e, --encoder` | Видео энкодер | `auto`, `nvenc`, `qsv`, `amf`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-e nvenc` | | `-e, --encoder` | Видео энкодер | `auto`, `nvenc`, `qsv`, `amf`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-e nvenc` |
| `-d, --decoder` | Видео декодер (hwaccel) | `auto`, `nvenc`, `qsv`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-d cpu` | | `-d, --decoder` | Видео декодер (hwaccel) | `auto`, `nvenc`, `qsv`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-d cpu` |
| `-m, --muted` | Отключить аудио дорожку в выходных файлах | `flag` | `off` | `-m` |
### Примеры использования ### Примеры использования
```bash ```bash
# Базовая конвертация (DASH + HLS, авто кодек, автопрофили) # Базовая конвертация (DASH + HLS, авто кодек, автопрофили)
create-vod video.mp4 npx @gromlab/create-vod video.mp4
# Указать выходную директорию # Указать выходную директорию
create-vod video.mp4 ./output npx @gromlab/create-vod video.mp4 ./output
# Только выбранные разрешения # Только выбранные разрешения
create-vod video.mp4 -r 720,1080,1440 npx @gromlab/create-vod video.mp4 -r 720,1080,1440
# Высокий FPS для игровых стримов # Высокий FPS для игровых стримов
create-vod video.mp4 -r 720@60,1080@60 npx @gromlab/create-vod video.mp4 -r 720@60,1080@60
# Постер с 5-й секунды # Постер с 5-й секунды
create-vod video.mp4 -p 5 npx @gromlab/create-vod video.mp4 -p 5
# Комбинация параметров # Комбинация параметров
create-vod video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10 npx @gromlab/create-vod video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10
# Без звука
npx @gromlab/create-vod video.mp4 -m
``` ```
### Поддерживаемые разрешения ### Поддерживаемые разрешения

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{ {
"name": "@gromlab/create-vod", "name": "@gromlab/create-vod",
"author": "Gromov Sergei", "author": "Gromov Sergei",
"version": "0.1.9", "version": "0.1.15",
"description": "DASH/HLS video converter with hardware acceleration (NVENC/QSV/AMF/VAAPI), thumbnails and poster generation", "description": "DASH/HLS video converter with hardware acceleration (NVENC/QSV/AMF/VAAPI), thumbnails and poster generation",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -19,6 +19,7 @@
"dist", "dist",
"bin", "bin",
"README.md", "README.md",
"README_RU.md",
"LICENSE" "LICENSE"
], ],
"scripts": { "scripts": {

View File

@@ -31,6 +31,7 @@ let av1CQ: number | undefined;
let av1CRF: number | undefined; let av1CRF: number | undefined;
let accelerator: HardwareAccelerationOption | undefined; let accelerator: HardwareAccelerationOption | undefined;
let decoder: HardwareAccelerationOption | undefined; let decoder: HardwareAccelerationOption | undefined;
let muted = false;
// 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++) {
@@ -113,6 +114,8 @@ for (let i = 0; i < args.length; i++) {
} }
decoder = acc as HardwareAccelerationOption; decoder = acc as HardwareAccelerationOption;
i++; i++;
} else if (args[i] === '-m' || args[i] === '--muted') {
muted = true;
} else if (!args[i].startsWith('-')) { } else if (!args[i].startsWith('-')) {
// Positional argument // Positional argument
positionalArgs.push(args[i]); 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(' -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(' -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(' -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('\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)');
@@ -278,7 +282,7 @@ console.log(` File: ${input}`);
console.log(` Size: ${fileSizeMB} MB`); console.log(` Size: ${fileSizeMB} MB`);
console.log(` Resolution: ${metadata.width}x${metadata.height}`); console.log(` Resolution: ${metadata.width}x${metadata.height}`);
console.log(` FPS: ${metadata.fps.toFixed(2)}`); console.log(` FPS: ${metadata.fps.toFixed(2)}`);
console.log(` Duration: ${Math.floor(metadata.duration / 60)}m ${Math.floor(metadata.duration % 60)}s`); console.log(` Duration: ${metadata.duration.toFixed(2)}s`);
console.log(` Codec: ${metadata.codec}`); console.log(` Codec: ${metadata.codec}`);
if (metadata.videoBitrate) { if (metadata.videoBitrate) {
console.log(` Video Bitrate: ${(metadata.videoBitrate / 1000).toFixed(2)} Mbps`); console.log(` Video Bitrate: ${(metadata.videoBitrate / 1000).toFixed(2)} Mbps`);
@@ -309,7 +313,7 @@ if (customProfiles && customProfiles.length > 0) {
profileResult.warnings.forEach(warn => console.warn(` - ${warn}`)); profileResult.warnings.forEach(warn => console.warn(` - ${warn}`));
} }
displayProfiles = profileResult.profiles.map(p => p.name); displayProfiles = profileResult.profiles.map(p => p.fps ? `${p.name}@${p.fps}` : p.name);
} else { } else {
const autoProfiles = selectProfiles( const autoProfiles = selectProfiles(
metadata.width, metadata.width,
@@ -317,7 +321,7 @@ if (customProfiles && customProfiles.length > 0) {
metadata.fps, metadata.fps,
metadata.videoBitrate metadata.videoBitrate
); );
displayProfiles = autoProfiles.map(p => p.name); displayProfiles = autoProfiles.map(p => p.fps ? `${p.name}@${p.fps}` : p.name);
} }
const manifestDesc = [ const manifestDesc = [
@@ -351,6 +355,7 @@ 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(` Encoder: ${acceleratorDisplay} (available: ${encoderListDisplay})`); console.log(` Encoder: ${acceleratorDisplay} (available: ${encoderListDisplay})`);
console.log(` Decoder: ${decoderDisplay} (available: ${decoderListDisplay})`); console.log(` Decoder: ${decoderDisplay} (available: ${decoderListDisplay})`);
console.log(` Audio: ${muted ? 'disabled (muted)' : 'enabled'}`);
// Build quality settings if any are specified // Build quality settings if any are specified
let quality: QualitySettings | undefined; let quality: QualitySettings | undefined;
@@ -405,6 +410,7 @@ try {
quality, quality,
generateThumbnails: true, generateThumbnails: true,
generatePoster: true, generatePoster: true,
muted,
parallel: true, parallel: true,
onProgress: (progress) => { onProgress: (progress) => {
const stageName = progress.stage === 'encoding' ? 'Encoding' : const stageName = progress.stage === 'encoding' ? 'Encoding' :

View File

@@ -51,6 +51,7 @@ export async function convertToDash(
generatePoster: shouldGeneratePoster = true, generatePoster: shouldGeneratePoster = true,
posterTimecode = '00:00:00', posterTimecode = '00:00:00',
parallel = true, parallel = true,
muted = false,
onProgress onProgress
} = options; } = options;
@@ -97,6 +98,7 @@ Formats: ${formats?.join(',') || 'dash,hls'}
shouldGeneratePoster, shouldGeneratePoster,
posterTimecode, posterTimecode,
parallel, parallel,
muted,
onProgress onProgress
); );
} finally { } finally {
@@ -137,6 +139,7 @@ async function convertToDashInternal(
generatePosterFlag: boolean, generatePosterFlag: boolean,
posterTimecode: string, posterTimecode: string,
parallel: boolean, parallel: boolean,
muted: boolean,
onProgress?: (progress: ConversionProgress) => void onProgress?: (progress: ConversionProgress) => void
): Promise<DashConvertResult> { ): Promise<DashConvertResult> {
@@ -160,7 +163,12 @@ async function convertToDashInternal(
// Get video metadata // Get video metadata
const metadata = await getVideoMetadata(input); 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) // Determine hardware accelerator (auto by default)
const preferredAccelerator: HardwareAccelerationOption = const preferredAccelerator: HardwareAccelerationOption =
@@ -311,13 +319,14 @@ async function convertToDashInternal(
videoCodec, videoCodec,
codecPreset, codecPreset,
metadata.duration, metadata.duration,
segmentDuration, effectiveSegmentDuration,
metadata.audioBitrate, metadata.audioBitrate,
parallel, parallel,
maxConcurrent, maxConcurrent,
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
muted,
selectedDecoder === 'cpu' ? undefined : selectedDecoder, selectedDecoder === 'cpu' ? undefined : selectedDecoder,
(profileName, percent) => { (profileName, percent) => {
const profileIndex = profiles.findIndex(p => p.name === profileName); const profileIndex = profiles.findIndex(p => p.name === profileName);
@@ -350,7 +359,7 @@ async function convertToDashInternal(
codecMP4Paths, codecMP4Paths,
videoOutputDir, videoOutputDir,
profiles, profiles,
segmentDuration, effectiveSegmentDuration,
codecsSelected, codecsSelected,
formatsSelected, formatsSelected,
hasAudio hasAudio

View File

@@ -53,6 +53,7 @@ export async function encodeProfileToMP4(
codecType: 'h264' | 'av1', codecType: 'h264' | 'av1',
qualitySettings?: CodecQualitySettings, qualitySettings?: CodecQualitySettings,
optimizations?: VideoOptimizations, optimizations?: VideoOptimizations,
muted: boolean = false,
decoderAccel?: HardwareAccelerator, decoderAccel?: HardwareAccelerator,
onProgress?: (percent: number) => void onProgress?: (percent: number) => void
): Promise<string> { ): Promise<string> {
@@ -157,12 +158,15 @@ export async function encodeProfileToMP4(
// Build video filter chain // Build video filter chain
const filters: string[] = []; const filters: string[] = [];
const targetWidth = profile.width;
const targetHeight = profile.height;
if (decoderAccel === 'nvenc') { const useCudaScale = decoderAccel === 'nvenc';
// CUDA path: keep frames on GPU if (useCudaScale) {
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 { } 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) // Apply optimizations (for future use)
@@ -178,17 +182,34 @@ export async function encodeProfileToMP4(
} }
} }
// Если использовали GPU-скейл, возвращаем кадры в системную память перед CPU-фильтрами
if (useCudaScale) {
filters.push('hwdownload', 'format=nv12');
}
// Центрируем кадр, чтобы браузеры (Firefox/videotoolbox) не игнорировали PAR
filters.push(`pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2`, 'setsar=1');
args.push('-vf', filters.join(',')); args.push('-vf', filters.join(','));
// Audio encoding if (!muted) {
// Select optimal bitrate based on source (don't upscale) // Audio encoding с нормализацией таймингов и автоподпаддингом тишиной
const targetAudioBitrate = parseInt(profile.audioBitrate) || 256; const targetAudioBitrate = parseInt(profile.audioBitrate) || 256;
const optimalAudioBitrate = selectAudioBitrate(sourceAudioBitrate, targetAudioBitrate); const optimalAudioBitrate = selectAudioBitrate(sourceAudioBitrate, targetAudioBitrate);
args.push('-c:a', 'aac', '-b:a', optimalAudioBitrate); args.push('-c:a', 'aac', '-b:a', optimalAudioBitrate);
// Audio optimizations const targetDur = duration.toFixed(3);
const audioFilters: string[] = [
'aresample=async=1:min_hard_comp=0.1:first_pts=0',
`apad=whole_dur=${targetDur}`,
`atrim=0:${targetDur}`
];
if (optimizations?.audioNormalize) { if (optimizations?.audioNormalize) {
args.push('-af', 'loudnorm'); audioFilters.push('loudnorm');
}
args.push('-af', audioFilters.join(','));
} else {
args.push('-an'); // без аудио дорожки
} }
// Output // Output
@@ -217,6 +238,7 @@ export async function encodeProfilesToMP4(
codecType: 'h264' | 'av1', codecType: 'h264' | 'av1',
qualitySettings?: CodecQualitySettings, qualitySettings?: CodecQualitySettings,
optimizations?: VideoOptimizations, optimizations?: VideoOptimizations,
muted: boolean = false,
decoderAccel?: HardwareAccelerator, decoderAccel?: HardwareAccelerator,
onProgress?: (profileName: string, percent: number) => void onProgress?: (profileName: string, percent: number) => void
): Promise<Map<string, string>> { ): Promise<Map<string, string>> {
@@ -239,6 +261,7 @@ export async function encodeProfilesToMP4(
codecType, codecType,
qualitySettings, qualitySettings,
optimizations, optimizations,
muted,
decoderAccel, decoderAccel,
(percent) => { (percent) => {
if (onProgress) { if (onProgress) {
@@ -269,6 +292,7 @@ export async function encodeProfilesToMP4(
codecType, codecType,
qualitySettings, qualitySettings,
optimizations, optimizations,
muted,
decoderAccel, decoderAccel,
(percent) => { (percent) => {
if (onProgress) { if (onProgress) {

View File

@@ -86,6 +86,9 @@ export interface DashConvertOptions {
/** Предпочитаемый аппаратный ускоритель для декодера (auto по умолчанию) */ /** Предпочитаемый аппаратный ускоритель для декодера (auto по умолчанию) */
hardwareDecoder?: HardwareAccelerationOption; hardwareDecoder?: HardwareAccelerationOption;
/** Отключить аудиодорожку (muted). По умолчанию false. */
muted?: boolean;
/** Quality settings for video encoding (CQ/CRF values) */ /** Quality settings for video encoding (CQ/CRF values) */
quality?: QualitySettings; quality?: QualitySettings;

View File

@@ -90,13 +90,16 @@ export function selectAudioBitrate(
sourceAudioBitrate: number | undefined, sourceAudioBitrate: number | undefined,
targetBitrate: number = 256 targetBitrate: number = 256
): string { ): string {
const MIN_AUDIO_KBPS = 64; // не опускаться ниже базового качества
if (!sourceAudioBitrate) { if (!sourceAudioBitrate) {
// If we can't detect source bitrate, use target // If we can't detect source bitrate, use target
return `${targetBitrate}k`; return `${targetBitrate}k`;
} }
// Use minimum of source and target (no upscaling) // Не занижаем слишком низко: clamp к минималке, но не выше целевого
const optimalBitrate = Math.min(sourceAudioBitrate, targetBitrate); const clampedSource = Math.max(sourceAudioBitrate, MIN_AUDIO_KBPS);
const optimalBitrate = Math.min(clampedSource, targetBitrate);
// Round to common bitrate values for consistency // Round to common bitrate values for consistency
if (optimalBitrate <= 64) return '64k'; if (optimalBitrate <= 64) return '64k';

3
web-test/dash.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title> <title>Document</title>
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></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://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> <script src="https://unpkg.com/plyr@3"></script>
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
@@ -15,6 +15,9 @@
margin: 50px auto; margin: 50px auto;
max-width: 1500px; max-width: 1500px;
} }
video {
max-height: 800px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -24,7 +27,8 @@
<script> <script>
document.addEventListener('DOMContentLoaded', () => { 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 dash = dashjs.MediaPlayer().create();
const video = document.querySelector('video'); const video = document.querySelector('video');
dash.initialize(video, source, true); dash.initialize(video, source, true);