Compare commits
13 Commits
0813bea1d4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b40ae34387 | |||
| 84231d705f | |||
| 2c8d9d1e9e | |||
| 41fe1a7370 | |||
| 55fb1f640a | |||
| 4293b6735a | |||
| 248fe15b62 | |||
| 81add91669 | |||
| b6c191290c | |||
| 5ab30eee4c | |||
| 187697eca6 | |||
| 346eb697cf | |||
| b8f9f0e046 |
43
README.md
43
README.md
@@ -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
|
||||||
|
|||||||
40
README_RU.md
40
README_RU.md
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Поддерживаемые разрешения
|
### Поддерживаемые разрешения
|
||||||
|
|||||||
94
bin/cli.js
94
bin/cli.js
File diff suppressed because one or more lines are too long
@@ -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": {
|
||||||
|
|||||||
12
src/cli.ts
12
src/cli.ts
@@ -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' :
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
if (optimizations?.audioNormalize) {
|
const audioFilters: string[] = [
|
||||||
args.push('-af', 'loudnorm');
|
'aresample=async=1:min_hard_comp=0.1:first_pts=0',
|
||||||
|
`apad=whole_dur=${targetDur}`,
|
||||||
|
`atrim=0:${targetDur}`
|
||||||
|
];
|
||||||
|
if (optimizations?.audioNormalize) {
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -81,10 +81,13 @@ export interface DashConvertOptions {
|
|||||||
/** Streaming formats: list (default: ['dash','hls']) */
|
/** Streaming formats: list (default: ['dash','hls']) */
|
||||||
formats?: StreamingFormat[];
|
formats?: StreamingFormat[];
|
||||||
|
|
||||||
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
|
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
|
||||||
hardwareAccelerator?: HardwareAccelerationOption;
|
hardwareAccelerator?: HardwareAccelerationOption;
|
||||||
/** Предпочитаемый аппаратный ускоритель для декодера (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;
|
||||||
|
|||||||
@@ -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
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">
|
<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" />
|
||||||
|
|
||||||
@@ -14,7 +14,10 @@
|
|||||||
.container {
|
.container {
|
||||||
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);
|
||||||
@@ -35,4 +39,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user