fix: Исправление выбора энкодер/декодер
This commit is contained in:
30
README.md
30
README.md
@@ -5,7 +5,7 @@
|
|||||||
CLI tool to convert videos to DASH and HLS with hardware acceleration (NVENC / Intel QSV / AMD AMF / VAAPI), adaptive streaming, and automatic thumbnails/poster.
|
CLI tool to convert videos to DASH and HLS with hardware acceleration (NVENC / Intel QSV / AMD AMF / VAAPI), adaptive streaming, and automatic thumbnails/poster.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- ⚡ Hardware acceleration: NVENC / Intel QSV / AMD AMF / VAAPI (auto priority)
|
- ⚡ Hardware acceleration: auto-detect encoder/decoder (NVENC / Intel QSV / AMD AMF / VAAPI / CPU)
|
||||||
- 🎯 Formats: DASH and HLS (shared segments)
|
- 🎯 Formats: DASH and HLS (shared segments)
|
||||||
- 📊 Quality profiles: multiple bitrates/FPS (auto or custom)
|
- 📊 Quality profiles: multiple bitrates/FPS (auto or custom)
|
||||||
- 🖼️ Preview: thumbnail sprite + VTT, poster from the first frame
|
- 🖼️ Preview: thumbnail sprite + VTT, poster from the first frame
|
||||||
@@ -51,18 +51,19 @@ create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Description | Format | Example |
|
| Option | Description | Values / Format | Default | Example |
|
||||||
|--------|----------------------------|----------------------------|---------------------------------|
|
|--------|----------------------------|----------------------------|----------|---------------------------------|
|
||||||
| `-r, --resolutions` | Quality profiles | `360`, `720@60`, `1080-60` | `-r 720,1080,1440@60` |
|
| `-r, --resolutions` | Quality profiles | `360`, `720@60`, `1080-60` | auto | `-r 720,1080,1440@60` |
|
||||||
| `-c, --codec` | Video codec | `h264`, `av1`, `dual` | `-c dual` (default) |
|
| `-c, --codec` | Video codec | `h264`, `av1` | auto (h264 + AV1 if HW) | `-c h264` |
|
||||||
| `-f, --format` | Streaming format | `dash`, `hls`, `both` | `-f both` (default) |
|
| `-f, --format` | Streaming format | `dash`, `hls` | auto (dash + hls) | `-f dash` |
|
||||||
| `-p, --poster` | Poster timecode | `HH:MM:SS` or seconds | `-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` |
|
||||||
| `--accel` | Hardware accelerator | `auto`, `nvenc`, `qsv`, `amf`, `cpu` | `--accel 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` |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default (DASH + HLS, dual codec, auto profiles)
|
# Default (DASH + HLS, auto codec, auto profiles)
|
||||||
create-vod video.mp4
|
create-vod video.mp4
|
||||||
|
|
||||||
# Custom output directory
|
# Custom output directory
|
||||||
@@ -83,8 +84,14 @@ create-vod video.mp4 -f hls -c h264
|
|||||||
# Poster from 5th second
|
# Poster from 5th second
|
||||||
create-vod video.mp4 -p 5
|
create-vod video.mp4 -p 5
|
||||||
|
|
||||||
|
# Force CPU encode/decode
|
||||||
|
create-vod video.mp4 -c h264 -e cpu -d cpu
|
||||||
|
|
||||||
|
# Force GPU encode + CPU decode
|
||||||
|
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 -c dual -f both -p 00:00:10
|
create-vod video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported resolutions
|
### Supported resolutions
|
||||||
@@ -103,11 +110,12 @@ High FPS (60/90/120) are generated only if the source supports that FPS.
|
|||||||
## Defaults & Automation
|
## Defaults & Automation
|
||||||
|
|
||||||
- Segment duration: 2 seconds
|
- Segment duration: 2 seconds
|
||||||
- Hardware accel: auto-detect (GPU if available, else CPU)
|
- Hardware accel: auto-detect best encoder/decoder (GPU if available, else CPU)
|
||||||
- Profiles: auto-selected based on source resolution
|
- Profiles: auto-selected based on source resolution
|
||||||
- Bitrate: BPP-based dynamic calculation
|
- Bitrate: BPP-based dynamic calculation
|
||||||
- Thumbnails: auto sprite (160×90, 1s interval) + VTT
|
- Thumbnails: auto sprite (160×90, 1s interval) + VTT
|
||||||
- Poster: first frame (0:00:00, configurable via `-p`)
|
- Poster: first frame (0:00:00, configurable via `-p`)
|
||||||
- Parallel encoding: enabled
|
- Parallel encoding: enabled
|
||||||
|
- AV1: enabled only if hardware AV1 encoder detected (in auto mode); otherwise остаётся H.264
|
||||||
|
|
||||||
**Requirements:** Node.js ≥18.0.0, FFmpeg, MP4Box (gpac), optional NVIDIA/Intel/AMD GPU for acceleration
|
**Requirements:** Node.js ≥18.0.0, FFmpeg, MP4Box (gpac), optional NVIDIA/Intel/AMD GPU for acceleration
|
||||||
|
|||||||
19
README_RU.md
19
README_RU.md
@@ -51,18 +51,19 @@ create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-
|
|||||||
|
|
||||||
### Опциональные ключи
|
### Опциональные ключи
|
||||||
|
|
||||||
| Ключ | Описание | Формат | Пример |
|
| Ключ | Описание | Значения / формат | По умолчанию | Пример |
|
||||||
|------|----------|--------|--------|
|
|------|----------|-------------------|--------------|--------|
|
||||||
| `-r, --resolutions` | Выбор профилей качества | `360`, `720@60`, `1080-60` | `-r 720,1080,1440@60` |
|
| `-r, --resolutions` | Выбор профилей качества | `360`, `720@60`, `1080-60` | авто | `-r 720,1080,1440@60` |
|
||||||
| `-c, --codec` | Видео кодек | `h264`, `av1`, `dual` | `-c dual` (по умолчанию) |
|
| `-c, --codec` | Видео кодек | `h264`, `av1` | авто (h264 + AV1 при наличии HW) | `-c h264` |
|
||||||
| `-f, --format` | Формат стриминга | `dash`, `hls`, `both` | `-f both` (по умолчанию) |
|
| `-f, --format` | Формат стриминга | `dash`, `hls` | авто (dash + hls) | `-f dash` |
|
||||||
| `-p, --poster` | Таймкод для постера | `HH:MM:SS` или секунды | `-p 00:00:05` или `-p 10` |
|
| `-p, --poster` | Таймкод для постера | `HH:MM:SS` или секунды | `00:00:00` | `-p 00:00:05` или `-p 10` |
|
||||||
| `--accel` | Аппаратный ускоритель | `auto`, `nvenc`, `qsv`, `amf`, `cpu` | `--accel 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` |
|
||||||
|
|
||||||
### Примеры использования
|
### Примеры использования
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Базовая конвертация (DASH + HLS, dual codec, автопрофили)
|
# Базовая конвертация (DASH + HLS, авто кодек, автопрофили)
|
||||||
create-vod video.mp4
|
create-vod video.mp4
|
||||||
|
|
||||||
# Указать выходную директорию
|
# Указать выходную директорию
|
||||||
@@ -84,7 +85,7 @@ create-vod video.mp4 -f hls -c h264
|
|||||||
create-vod video.mp4 -p 5
|
create-vod video.mp4 -p 5
|
||||||
|
|
||||||
# Комбинация параметров
|
# Комбинация параметров
|
||||||
create-vod video.mp4 ./output -r 720,1080@60,1440@60 -c dual -f both -p 00:00:10
|
create-vod video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10
|
||||||
```
|
```
|
||||||
|
|
||||||
### Поддерживаемые разрешения
|
### Поддерживаемые разрешения
|
||||||
|
|||||||
124
bin/cli.js
124
bin/cli.js
File diff suppressed because one or more lines are too long
37
docs/CLI_OPTIONS.md
Normal file
37
docs/CLI_OPTIONS.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Поддерживаемые опции CLI
|
||||||
|
|
||||||
|
## Базовый вызов
|
||||||
|
```
|
||||||
|
create-vod <input-video> [output-dir] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ключевые опции
|
||||||
|
- `-r, --resolutions` — список профилей (например: `360,720@60,1080`).
|
||||||
|
- `-c, --codec` — `h264` | `av1` (по умолчанию auto = h264 + AV1 при наличии HW).
|
||||||
|
- `-f, --format` — `dash` | `hls` (по умолчанию auto = dash + hls).
|
||||||
|
- `-p, --poster` — таймкод постера (`HH:MM:SS` или секунды, по умолчанию 0:00:00).
|
||||||
|
- `-e, --encoder` — аппаратный/софт энкодер: `auto` | `cpu` | `nvenc` | `qsv` | `amf` | `vaapi` | `videotoolbox` | `v4l2`.
|
||||||
|
- `-d, --decoder` — аппаратный/софт декодер: `auto` | `cpu` | `nvenc` (cuda) | `qsv` | `vaapi` | `videotoolbox` | `v4l2`.
|
||||||
|
- `--h264-cq / --h264-crf` — ручное качество для H.264 (GPU CQ / CPU CRF).
|
||||||
|
- `--av1-cq / --av1-crf` — ручное качество для AV1 (GPU CQ / CPU CRF).
|
||||||
|
|
||||||
|
## Что передаём в FFmpeg
|
||||||
|
- `cpu` (энкодер) → H.264: `libx264`, AV1: `libsvtav1`.
|
||||||
|
- `nvenc` → `h264_nvenc`, `av1_nvenc` (если доступен).
|
||||||
|
- `qsv` → `h264_qsv`, `av1_qsv`.
|
||||||
|
- `amf` → `h264_amf`, `av1_amf`.
|
||||||
|
- `vaapi` → `h264_vaapi`, `av1_vaapi`.
|
||||||
|
- `videotoolbox` → `h264_videotoolbox` (macOS).
|
||||||
|
- `v4l2` → `h264_v4l2m2m` (зависит от SoC).
|
||||||
|
|
||||||
|
Декодеры:
|
||||||
|
- `cpu` — без `-hwaccel`.
|
||||||
|
- `nvenc` — `-hwaccel cuda -hwaccel_output_format cuda`.
|
||||||
|
- `qsv` — `-hwaccel qsv`.
|
||||||
|
- `vaapi` — `-hwaccel vaapi -vaapi_device /dev/dri/renderD128`.
|
||||||
|
- `videotoolbox` — `-hwaccel videotoolbox`.
|
||||||
|
- `v4l2` — `-hwaccel v4l2m2m`.
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
- В авто-режиме AV1 включается только при наличии аппаратного AV1; иначе остаётся H.264.
|
||||||
|
- `auto` выбирает лучший обнаруженный ускоритель по приоритету (nvenc → qsv → amf → vaapi → videotoolbox → v4l2 → cpu).
|
||||||
@@ -66,7 +66,6 @@ create-vod video.mp4 -r 360 720@60 1080 1440@120
|
|||||||
**Значения:**
|
**Значения:**
|
||||||
- `h264` — только H.264 (максимальная совместимость)
|
- `h264` — только H.264 (максимальная совместимость)
|
||||||
- `av1` — только AV1 (лучшее сжатие, новые браузеры)
|
- `av1` — только AV1 (лучшее сжатие, новые браузеры)
|
||||||
- `dual` — оба кодека (рекомендуется)
|
|
||||||
|
|
||||||
**Примеры:**
|
**Примеры:**
|
||||||
```bash
|
```bash
|
||||||
@@ -75,35 +74,56 @@ create-vod video.mp4 -c h264
|
|||||||
|
|
||||||
# Только AV1 (медленнее, меньше места)
|
# Только AV1 (медленнее, меньше места)
|
||||||
create-vod video.mp4 -c av1
|
create-vod video.mp4 -c av1
|
||||||
|
|
||||||
# Оба кодека (максимальная совместимость)
|
|
||||||
create-vod video.mp4 -c dual
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**GPU ускорение:**
|
**GPU ускорение:**
|
||||||
- H.264: `h264_nvenc` (NVIDIA), fallback → `libx264` (CPU)
|
- H.264: `h264_nvenc` (NVIDIA), fallback → `libx264` (CPU)
|
||||||
- AV1: `av1_nvenc` (NVIDIA), `av1_qsv` (Intel), `av1_amf` (AMD), fallback → `libsvtav1` (CPU)
|
- AV1: `av1_nvenc` (NVIDIA), `av1_qsv` (Intel), `av1_amf` (AMD), fallback → `libsvtav1` (CPU)
|
||||||
|
|
||||||
**По умолчанию:** `dual`
|
**По умолчанию:** авто (H.264 всегда, AV1 если есть аппаратный AV1)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `--accel` — Аппаратный ускоритель
|
### `-e, --encoder` — Видео энкодер
|
||||||
|
|
||||||
Выбор приоритетного ускорителя. По умолчанию выбирается лучший из доступных.
|
Выбор приоритетного видео энкодера.
|
||||||
|
|
||||||
**Значения:**
|
**Значения:**
|
||||||
- `auto` — автоопределение по приоритету (NVENC → QSV → AMF → CPU)
|
- `auto` — автоопределение по приоритету (NVENC → QSV → AMF → VAAPI → CPU)
|
||||||
- `nvenc` — NVIDIA NVENC
|
- `nvenc` — NVIDIA NVENC
|
||||||
- `qsv` — Intel Quick Sync
|
- `qsv` — Intel Quick Sync
|
||||||
- `amf` — AMD AMF
|
- `amf` — AMD AMF
|
||||||
- `cpu` — принудительно без GPU
|
- `vaapi` — VAAPI (Linux)
|
||||||
|
- `videotoolbox` — Apple VT (macOS)
|
||||||
|
- `v4l2` — V4L2 M2M (ARM/SBC)
|
||||||
|
- `cpu` — принудительно без GPU (`libx264`/`libsvtav1`)
|
||||||
|
|
||||||
**Примеры:**
|
**Примеры:**
|
||||||
```bash
|
```bash
|
||||||
create-vod video.mp4 --accel nvenc
|
create-vod video.mp4 -e nvenc
|
||||||
create-vod video.mp4 --accel qsv
|
create-vod video.mp4 -e qsv
|
||||||
create-vod video.mp4 --accel cpu # отключить GPU
|
create-vod video.mp4 -e cpu # отключить GPU
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `-d, --decoder` — Видео декодер (hwaccel)
|
||||||
|
|
||||||
|
Выбор аппаратного декодера / hwaccel.
|
||||||
|
|
||||||
|
**Значения:**
|
||||||
|
- `auto` — автоопределение (NVDEC → QSV → VAAPI → CPU)
|
||||||
|
- `nvenc` — CUDA/NVDEC
|
||||||
|
- `qsv` — Intel Quick Sync
|
||||||
|
- `vaapi` — VAAPI (Linux)
|
||||||
|
- `videotoolbox` — Apple VT (macOS)
|
||||||
|
- `v4l2` — V4L2 M2M (ARM/SBC)
|
||||||
|
- `cpu` — программный декод
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
```bash
|
||||||
|
create-vod video.mp4 -d nvenc
|
||||||
|
create-vod video.mp4 -d cpu # декод только на CPU
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -115,7 +135,6 @@ create-vod video.mp4 --accel cpu # отключить GPU
|
|||||||
**Значения:**
|
**Значения:**
|
||||||
- `dash` — только DASH (MPEG-DASH)
|
- `dash` — только DASH (MPEG-DASH)
|
||||||
- `hls` — только HLS (HTTP Live Streaming)
|
- `hls` — только HLS (HTTP Live Streaming)
|
||||||
- `both` — оба формата
|
|
||||||
|
|
||||||
**Примеры:**
|
**Примеры:**
|
||||||
```bash
|
```bash
|
||||||
@@ -125,8 +144,8 @@ create-vod video.mp4 -f dash
|
|||||||
# Только HLS (для Safari/iOS)
|
# Только HLS (для Safari/iOS)
|
||||||
create-vod video.mp4 -f hls
|
create-vod video.mp4 -f hls
|
||||||
|
|
||||||
# Оба формата (максимальная совместимость)
|
# Оба формата (по умолчанию авто)
|
||||||
create-vod video.mp4 -f both
|
create-vod video.mp4 # формат не указывать
|
||||||
```
|
```
|
||||||
|
|
||||||
**Особенности:**
|
**Особенности:**
|
||||||
@@ -135,10 +154,9 @@ create-vod video.mp4 -f both
|
|||||||
|--------|--------|---------------|------------|
|
|--------|--------|---------------|------------|
|
||||||
| DASH | H.264 + AV1 | Chrome, Firefox, Edge, Safari (с dash.js) | Стандарт индустрии |
|
| DASH | H.264 + AV1 | Chrome, Firefox, Edge, Safari (с dash.js) | Стандарт индустрии |
|
||||||
| HLS | H.264 только | Safari, iOS, все браузеры | Требует H.264 |
|
| HLS | H.264 только | Safari, iOS, все браузеры | Требует H.264 |
|
||||||
| both | H.264 + AV1 (DASH), H.264 (HLS) | Максимальная | Рекомендуется |
|
|
||||||
|
|
||||||
**Ограничения:**
|
**Ограничения:**
|
||||||
- HLS требует `--codec h264` или `--codec dual`
|
- HLS требует наличие H.264 в итоговом наборе кодеков
|
||||||
- AV1 не поддерживается в HLS (Safari не поддерживает AV1)
|
- AV1 не поддерживается в HLS (Safari не поддерживает AV1)
|
||||||
|
|
||||||
**Файловая структура:**
|
**Файловая структура:**
|
||||||
@@ -163,12 +181,7 @@ output/
|
|||||||
└── thumbnails.vtt
|
└── thumbnails.vtt
|
||||||
```
|
```
|
||||||
|
|
||||||
**Преимущества структуры:**
|
**По умолчанию:** генерируются DASH и HLS
|
||||||
- Сегменты хранятся один раз (нет дублирования)
|
|
||||||
- DASH и HLS используют одни и те же .m4s файлы
|
|
||||||
- Экономия 50% места при `format=both`
|
|
||||||
|
|
||||||
**По умолчанию:** `both` (максимальная совместимость)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -198,7 +211,7 @@ create-vod video.mp4 -p 00:01:30
|
|||||||
### Базовое использование
|
### Базовое использование
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Простейший запуск (оба формата, dual codec, автопрофили)
|
# Простейший запуск (оба формата, авто кодек, автопрофили)
|
||||||
create-vod video.mp4
|
create-vod video.mp4
|
||||||
|
|
||||||
# С указанием выходной директории
|
# С указанием выходной директории
|
||||||
@@ -226,9 +239,6 @@ create-vod video.mp4 -c h264
|
|||||||
|
|
||||||
# Лучшее сжатие (только AV1)
|
# Лучшее сжатие (только AV1)
|
||||||
create-vod video.mp4 -c av1
|
create-vod video.mp4 -c av1
|
||||||
|
|
||||||
# Максимальная совместимость
|
|
||||||
create-vod video.mp4 -c dual
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Выбор формата
|
### Выбор формата
|
||||||
@@ -239,19 +249,16 @@ create-vod video.mp4 -f dash
|
|||||||
|
|
||||||
# HLS для Safari/iOS
|
# HLS для Safari/iOS
|
||||||
create-vod video.mp4 -f hls -c h264
|
create-vod video.mp4 -f hls -c h264
|
||||||
|
|
||||||
# Оба формата для всех устройств
|
|
||||||
create-vod video.mp4 -f both -c dual
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Комбинированные примеры
|
### Комбинированные примеры
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Производственная конфигурация
|
# Производственная конфигурация
|
||||||
create-vod video.mp4 ./cdn/videos -r 360,720,1080 -c dual -f both
|
create-vod video.mp4 ./cdn/videos -r 360,720,1080
|
||||||
|
|
||||||
# High-end конфигурация (4K, high FPS)
|
# High-end конфигурация (4K, high FPS)
|
||||||
create-vod video.mp4 -r 720@60,1080@60,1440@120,2160@60 -c dual -f both
|
create-vod video.mp4 -r 720@60,1080@60,1440@120,2160@60
|
||||||
|
|
||||||
# Быстрая конвертация для тестов
|
# Быстрая конвертация для тестов
|
||||||
create-vod video.mp4 -r 720 -c h264 -f dash
|
create-vod video.mp4 -r 720 -c h264 -f dash
|
||||||
@@ -308,7 +315,7 @@ brew install ffmpeg gpac
|
|||||||
|
|
||||||
### Время конвертации (примерные данные)
|
### Время конвертации (примерные данные)
|
||||||
|
|
||||||
Видео 4K, 10 секунд, dual codec, 3 профиля:
|
Видео 4K, 10 секунд, h264 + av1 (авто), 3 профиля:
|
||||||
|
|
||||||
| Конфигурация | Время |
|
| Конфигурация | Время |
|
||||||
|--------------|-------|
|
|--------------|-------|
|
||||||
@@ -322,13 +329,13 @@ brew install ffmpeg gpac
|
|||||||
### Для максимальной совместимости
|
### Для максимальной совместимости
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
create-vod video.mp4 -c dual -f both
|
create-vod video.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
Генерирует:
|
Генерирует:
|
||||||
- DASH с H.264 + AV1 (Chrome, Firefox, Edge)
|
- DASH с H.264 + AV1 (Chrome, Firefox, Edge при наличии поддержки)
|
||||||
- HLS с H.264 (Safari, iOS)
|
- HLS с H.264 (Safari, iOS)
|
||||||
- Все современные устройства поддерживаются
|
- Все современные устройства поддерживаются; AV1 добавляется при наличии HW
|
||||||
|
|
||||||
### Для быстрой разработки
|
### Для быстрой разработки
|
||||||
|
|
||||||
@@ -341,7 +348,7 @@ create-vod video.mp4 -r 720 -c h264 -f dash
|
|||||||
### Для продакшена
|
### Для продакшена
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
create-vod video.mp4 -r 360,480,720,1080,1440 -c dual -f both
|
create-vod video.mp4 -r 360,480,720,1080,1440
|
||||||
```
|
```
|
||||||
|
|
||||||
Широкий диапазон профилей для всех устройств.
|
Широкий диапазон профилей для всех устройств.
|
||||||
@@ -349,7 +356,7 @@ create-vod video.mp4 -r 360,480,720,1080,1440 -c dual -f both
|
|||||||
### Для 4K контента
|
### Для 4K контента
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
create-vod video.mp4 -r 720,1080,1440,2160 -c dual -f both
|
create-vod video.mp4 -r 720,1080,1440,2160
|
||||||
```
|
```
|
||||||
|
|
||||||
От HD до 4K для премиум контента.
|
От HD до 4K для премиум контента.
|
||||||
@@ -367,10 +374,10 @@ create-vod video.mp4 -r 720,1080,1440,2160 -c dual -f both
|
|||||||
|
|
||||||
**Решение:**
|
**Решение:**
|
||||||
```bash
|
```bash
|
||||||
# Используйте h264 или dual
|
# Используйте h264 или auto
|
||||||
create-vod video.mp4 -f hls -c h264
|
create-vod video.mp4 -f hls -c h264
|
||||||
# или
|
# или
|
||||||
create-vod video.mp4 -f hls -c dual
|
create-vod video.mp4 -f hls
|
||||||
```
|
```
|
||||||
|
|
||||||
### FPS источника ниже запрошенного
|
### FPS источника ниже запрошенного
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ chmod +x test_codec.sh
|
|||||||
# - H.264 для старых устройств (iOS < 14)
|
# - H.264 для старых устройств (iOS < 14)
|
||||||
|
|
||||||
# Пример: create-vod уже реализует это
|
# Пример: create-vod уже реализует это
|
||||||
create-vod input.mp4 output/ -c dual -f both -r 360,720,1080
|
create-vod input.mp4 output/ -r 360,720,1080
|
||||||
```
|
```
|
||||||
|
|
||||||
### Для архивирования
|
### Для архивирования
|
||||||
@@ -555,4 +555,3 @@ done
|
|||||||
- Протестированы кодеки: VP9, AV1 (CPU/GPU), H.264 (CPU/GPU)
|
- Протестированы кодеки: VP9, AV1 (CPU/GPU), H.264 (CPU/GPU)
|
||||||
- Добавлены все команды для тестирования
|
- Добавлены все команды для тестирования
|
||||||
- Добавлены результаты сравнения на видео 1920×1080, 135 сек
|
- Добавлены результаты сравнения на видео 1920×1080, 135 сек
|
||||||
|
|
||||||
|
|||||||
157
src/cli.ts
157
src/cli.ts
@@ -10,19 +10,19 @@
|
|||||||
* create-vod ./video.mp4 ./output -r 720,1080
|
* create-vod ./video.mp4 ./output -r 720,1080
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders, detectHardwareDecoders } from './index';
|
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders, detectHardwareDecoders, testEncoder, testDecoder } from './index';
|
||||||
import cliProgress from 'cli-progress';
|
import cliProgress from 'cli-progress';
|
||||||
import { statSync } from 'node:fs';
|
import { statSync } from 'node:fs';
|
||||||
import { basename, extname } from 'node:path';
|
import { basename, extname } from 'node:path';
|
||||||
import type { CodecType, StreamingFormat, QualitySettings, HardwareAccelerationOption } from './types';
|
import type { CodecChoice, StreamingFormatChoice, QualitySettings, HardwareAccelerationOption, HardwareAccelerator } from './types';
|
||||||
import { selectProfiles, createProfilesFromStrings } from './config/profiles';
|
import { selectProfiles, createProfilesFromStrings } from './config/profiles';
|
||||||
|
|
||||||
// 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
|
let codecChoice: CodecChoice = 'auto'; // h264 + AV1 if HW
|
||||||
let formatType: StreamingFormat = 'both'; // Default to both formats (DASH + HLS)
|
let formatChoice: StreamingFormatChoice = 'auto'; // DASH + HLS
|
||||||
const positionalArgs: string[] = [];
|
const positionalArgs: string[] = [];
|
||||||
|
|
||||||
// Quality settings
|
// Quality settings
|
||||||
@@ -58,19 +58,19 @@ for (let i = 0; i < args.length; i++) {
|
|||||||
i++; // Skip next arg
|
i++; // Skip next arg
|
||||||
} else if (args[i] === '-c' || args[i] === '--codec') {
|
} else if (args[i] === '-c' || args[i] === '--codec') {
|
||||||
const codec = args[i + 1];
|
const codec = args[i + 1];
|
||||||
if (codec === 'av1' || codec === 'h264' || codec === 'dual') {
|
if (codec === 'av1' || codec === 'h264') {
|
||||||
codecType = codec;
|
codecChoice = codec;
|
||||||
} else {
|
} else {
|
||||||
console.error(`❌ Invalid codec: ${codec}. Valid options: av1, h264, dual`);
|
console.error(`❌ Invalid codec: ${codec}. Valid options: av1, h264`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
i++; // Skip next arg
|
i++; // Skip next arg
|
||||||
} else if (args[i] === '-f' || args[i] === '--format') {
|
} else if (args[i] === '-f' || args[i] === '--format') {
|
||||||
const format = args[i + 1];
|
const format = args[i + 1];
|
||||||
if (format === 'dash' || format === 'hls' || format === 'both') {
|
if (format === 'dash' || format === 'hls') {
|
||||||
formatType = format;
|
formatChoice = format;
|
||||||
} else {
|
} else {
|
||||||
console.error(`❌ Invalid format: ${format}. Valid options: dash, hls, both`);
|
console.error(`❌ Invalid format: ${format}. Valid options: dash, hls`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
i++; // Skip next arg
|
i++; // Skip next arg
|
||||||
@@ -102,7 +102,7 @@ for (let i = 0; i < args.length; i++) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
i++; // Skip next arg
|
i++; // Skip next arg
|
||||||
} else if (args[i] === '--accel' || args[i] === '--hardware' || args[i] === '-e' || args[i] === '--encoder') {
|
} else if (args[i] === '-e' || args[i] === '--encoder') {
|
||||||
const acc = args[i + 1];
|
const acc = args[i + 1];
|
||||||
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu', 'vaapi', 'videotoolbox', 'v4l2'];
|
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu', 'vaapi', 'videotoolbox', 'v4l2'];
|
||||||
if (!allowed.includes(acc as HardwareAccelerationOption)) {
|
if (!allowed.includes(acc as HardwareAccelerationOption)) {
|
||||||
@@ -134,8 +134,8 @@ if (!input) {
|
|||||||
console.error('❌ Usage: create-vod <input-video> [output-dir] [options]');
|
console.error('❌ Usage: create-vod <input-video> [output-dir] [options]');
|
||||||
console.error('\nOptions:');
|
console.error('\nOptions:');
|
||||||
console.error(' -r, --resolutions Video resolutions (e.g., 360,480,720 or 720@60,1080@60)');
|
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(' -c, --codec Video codec: av1 or h264 (default: auto = h264 + AV1 if HW)');
|
||||||
console.error(' -f, --format Streaming format: dash, hls, or both (default: both)');
|
console.error(' -f, --format Streaming format: dash or hls (default: auto = dash + hls)');
|
||||||
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)');
|
||||||
@@ -149,12 +149,11 @@ if (!input) {
|
|||||||
console.error(' create-vod video.mp4 ./output');
|
console.error(' create-vod video.mp4 ./output');
|
||||||
console.error(' create-vod video.mp4 -r 360,480,720');
|
console.error(' create-vod video.mp4 -r 360,480,720');
|
||||||
console.error(' create-vod video.mp4 -c av1 --av1-cq 40');
|
console.error(' create-vod video.mp4 -c av1 --av1-cq 40');
|
||||||
console.error(' create-vod video.mp4 -c dual --h264-cq 30 --av1-cq 39');
|
console.error(' create-vod video.mp4 -c h264 --h264-cq 30');
|
||||||
console.error(' create-vod video.mp4 -f hls');
|
console.error(' create-vod video.mp4 -f hls');
|
||||||
console.error(' create-vod video.mp4 -c dual -f both');
|
|
||||||
console.error(' create-vod video.mp4 -r 720@60,1080@60,2160@60 -c av1 -f dash');
|
console.error(' create-vod video.mp4 -r 720@60,1080@60,2160@60 -c av1 -f dash');
|
||||||
console.error(' create-vod video.mp4 -p 00:00:05');
|
console.error(' create-vod video.mp4 -p 00:00:05');
|
||||||
console.error(' create-vod video.mp4 ./output -r 720,1080 -c dual -f both -p 10 --h264-cq 28 --av1-cq 37');
|
console.error(' create-vod video.mp4 ./output -r 720,1080 -p 10 --h264-cq 28');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +162,8 @@ console.log('🔍 Checking system...\n');
|
|||||||
const hasFFmpeg = await checkFFmpeg();
|
const hasFFmpeg = await checkFFmpeg();
|
||||||
const hasMP4Box = await checkMP4Box();
|
const hasMP4Box = await checkMP4Box();
|
||||||
const hwEncoders = await detectHardwareEncoders();
|
const hwEncoders = await detectHardwareEncoders();
|
||||||
const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder);
|
|
||||||
const hwDecoders = await detectHardwareDecoders();
|
const hwDecoders = await detectHardwareDecoders();
|
||||||
|
const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder);
|
||||||
|
|
||||||
const accelPriority: Record<string, number> = {
|
const accelPriority: Record<string, number> = {
|
||||||
nvenc: 100,
|
nvenc: 100,
|
||||||
@@ -172,31 +171,77 @@ const accelPriority: Record<string, number> = {
|
|||||||
amf: 80,
|
amf: 80,
|
||||||
vaapi: 70,
|
vaapi: 70,
|
||||||
videotoolbox: 65,
|
videotoolbox: 65,
|
||||||
v4l2: 60
|
v4l2: 60,
|
||||||
|
cpu: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const bestAccel = hwEncoders
|
const encoderMap: Record<string, string> = {
|
||||||
|
nvenc: 'h264_nvenc',
|
||||||
|
qsv: 'h264_qsv',
|
||||||
|
amf: 'h264_amf',
|
||||||
|
vaapi: 'h264_vaapi',
|
||||||
|
videotoolbox: 'h264_videotoolbox',
|
||||||
|
v4l2: 'h264_v4l2m2m',
|
||||||
|
cpu: 'libx264'
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoderCandidates = Array.from(new Set([...hwEncoders.map(e => e.accelerator), 'cpu']));
|
||||||
|
const decoderCandidates = Array.from(new Set([...hwDecoders.map(d => d.accelerator), 'cpu']));
|
||||||
|
|
||||||
|
async function filterEncoders() {
|
||||||
|
const result: HardwareAccelerationOption[] = [];
|
||||||
|
for (const acc of encoderCandidates) {
|
||||||
|
if (acc === 'amf') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const encoderName = encoderMap[acc] || 'libx264';
|
||||||
|
const ok = await testEncoder(encoderName);
|
||||||
|
if (ok) result.push(acc as HardwareAccelerationOption);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filterDecoders() {
|
||||||
|
const result: HardwareAccelerationOption[] = [];
|
||||||
|
for (const acc of decoderCandidates) {
|
||||||
|
if (acc === 'cpu') {
|
||||||
|
result.push('cpu');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ok = await testDecoder(acc as HardwareAccelerator, input);
|
||||||
|
if (ok) result.push(acc as HardwareAccelerationOption);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableEncoders = await filterEncoders();
|
||||||
|
const availableDecoders = await filterDecoders();
|
||||||
|
|
||||||
|
const bestAccel = availableEncoders
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (accelPriority[b.accelerator] || 0) - (accelPriority[a.accelerator] || 0))[0];
|
.sort((a, b) => (accelPriority[b] || 0) - (accelPriority[a] || 0))[0];
|
||||||
|
|
||||||
|
const bestDecoder = availableDecoders
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (accelPriority[b] || 0) - (accelPriority[a] || 0))[0];
|
||||||
|
|
||||||
console.log(`FFmpeg: ${hasFFmpeg ? '✅' : '❌'}`);
|
console.log(`FFmpeg: ${hasFFmpeg ? '✅' : '❌'}`);
|
||||||
console.log(`MP4Box: ${hasMP4Box ? '✅' : '❌'}`);
|
console.log(`MP4Box: ${hasMP4Box ? '✅' : '❌'}`);
|
||||||
const accelList = Array.from(new Set(hwEncoders.map(e => e.accelerator.toUpperCase())));
|
const accelList = Array.from(new Set(availableEncoders.map(e => e.toUpperCase())));
|
||||||
const bestAccelName = bestAccel ? bestAccel.accelerator.toUpperCase() : undefined;
|
const decList = Array.from(new Set(availableDecoders.map(d => d.toUpperCase())));
|
||||||
const accelRest = accelList.filter(name => name !== bestAccelName);
|
|
||||||
const encoderSelectedPlanned = accelerator
|
const encoderSelectedPlanned = accelerator
|
||||||
? accelerator.toUpperCase()
|
? accelerator.toUpperCase()
|
||||||
: (bestAccelName || 'CPU');
|
: ((bestAccel && bestAccel.toUpperCase()) || 'CPU');
|
||||||
const encoderAll = accelList.length > 0 ? accelList : ['CPU'];
|
const encoderAll = accelList.length > 0 ? accelList : ['CPU'];
|
||||||
|
|
||||||
const decList = Array.from(new Set(hwDecoders.map((d) => d.accelerator.toUpperCase())));
|
|
||||||
const decoderSelectedPlanned = decoder
|
const decoderSelectedPlanned = decoder
|
||||||
? decoder.toUpperCase()
|
? decoder.toUpperCase()
|
||||||
: (decList[0] || 'CPU');
|
: ((bestDecoder && bestDecoder.toUpperCase()) || 'CPU');
|
||||||
const decoderAll = decList.length > 0 ? decList : ['CPU'];
|
const decoderAll = decList.length > 0 ? decList : ['CPU'];
|
||||||
|
|
||||||
console.log(`Encoder: ${encoderSelectedPlanned === 'AUTO' ? (bestAccelName || 'CPU') : encoderSelectedPlanned} (${encoderAll.join(', ')})`);
|
console.log(`Encoder: ${encoderSelectedPlanned === 'AUTO' ? ((bestAccel && bestAccel.toUpperCase()) || 'CPU') : encoderSelectedPlanned} (${encoderAll.join(', ')})`);
|
||||||
console.log(`Decoder: ${decoderSelectedPlanned === 'AUTO' ? (decList[0] || 'CPU') : decoderSelectedPlanned} (${decoderAll.join(', ')})`);
|
console.log(`Decoder: ${decoderSelectedPlanned === 'AUTO' ? ((bestDecoder && bestDecoder.toUpperCase()) || 'CPU') : decoderSelectedPlanned} (${decoderAll.join(', ')})`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
if (!hasFFmpeg) {
|
if (!hasFFmpeg) {
|
||||||
@@ -209,22 +254,28 @@ if (!hasMP4Box) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate codec selection
|
// Resolve codec selection
|
||||||
if ((codecType === 'av1' || codecType === 'dual') && !hasAv1Hardware) {
|
let includeH264 = codecChoice === 'h264' || codecChoice === 'auto';
|
||||||
if (codecType === 'av1') {
|
let includeAv1 = codecChoice === 'av1' || codecChoice === 'auto';
|
||||||
|
|
||||||
|
if (includeAv1 && !hasAv1Hardware && codecChoice === 'auto') {
|
||||||
|
includeAv1 = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codecChoice === 'av1' && !hasAv1Hardware) {
|
||||||
console.error(`⚠️ Warning: AV1 encoding requested but no hardware AV1 encoder found.`);
|
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(` CPU-based AV1 encoding (libsvtav1) will be VERY slow.`);
|
||||||
console.error(` Consider using --codec h264 for faster encoding.\n`);
|
console.error(` Consider using --codec h264 for faster encoding.\n`);
|
||||||
} else if (codecType === 'dual') {
|
|
||||||
console.warn(`⚠️ AV1 hardware encoder not detected. Using H.264 only (disable AV1).`);
|
|
||||||
codecType = 'h264';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve formats
|
||||||
|
const wantDash = formatChoice === 'dash' || formatChoice === 'auto';
|
||||||
|
const wantHls = formatChoice === 'hls' || formatChoice === 'auto';
|
||||||
|
|
||||||
// Validate HLS requires H.264
|
// Validate HLS requires H.264
|
||||||
if ((formatType === 'hls' || formatType === 'both') && codecType === 'av1') {
|
if (wantHls && !includeH264) {
|
||||||
console.error(`❌ Error: HLS format requires H.264 codec for Safari/iOS compatibility.`);
|
console.error(`❌ Error: HLS format requires H.264 codec for Safari/iOS compatibility.`);
|
||||||
console.error(` Please use --codec h264 or --codec dual with --format hls\n`);
|
console.error(` Please use --codec h264 or omit --codec to keep H.264.\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,27 +332,31 @@ if (customProfiles && customProfiles.length > 0) {
|
|||||||
displayProfiles = autoProfiles.map(p => p.name);
|
displayProfiles = autoProfiles.map(p => p.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifestDesc =
|
const manifestDesc = [
|
||||||
formatType === 'both' ? 'DASH (manifest.mpd), HLS (master.m3u8)' :
|
wantDash ? 'DASH (manifest.mpd)' : null,
|
||||||
formatType === 'dash' ? 'DASH (manifest.mpd)' : 'HLS (master.m3u8)';
|
wantHls ? 'HLS (master.m3u8)' : null
|
||||||
|
].filter(Boolean).join(', ');
|
||||||
|
|
||||||
const thumbnailsPlanned = true;
|
const thumbnailsPlanned = true;
|
||||||
const posterPlanned = posterTimecode || '00:00:00';
|
const posterPlanned = posterTimecode || '00:00:00';
|
||||||
const codecDisplay = codecType === 'dual' ? 'dual (AV1 + H.264)' : codecType;
|
const codecListDisplay = [
|
||||||
const codecNote = codecType === 'h264' && !hasAv1Hardware ? ' (AV1 disabled: no HW)' : '';
|
includeH264 ? 'h264' : null,
|
||||||
const plannedAccel = accelerator ? accelerator.toUpperCase() : (bestAccelName || 'CPU');
|
includeAv1 ? 'av1' : null
|
||||||
const plannedDecoder = decoder ? decoder.toUpperCase() : (hwDecoders[0]?.accelerator.toUpperCase() || 'CPU');
|
].filter(Boolean).join(', ');
|
||||||
const acceleratorDisplay = plannedAccel === 'AUTO' ? (bestAccelName || 'CPU') : plannedAccel;
|
const codecNote = (!includeAv1 && codecChoice === 'auto' && !hasAv1Hardware) ? ' (AV1 disabled: no HW)' : '';
|
||||||
const decoderDisplay = plannedDecoder === 'AUTO'
|
const bestAccelName = (bestAccel && bestAccel.toUpperCase()) || 'CPU';
|
||||||
? (hwDecoders[0]?.accelerator.toUpperCase() || 'CPU')
|
const bestDecoderName = (bestDecoder && bestDecoder.toUpperCase()) || 'CPU';
|
||||||
: plannedDecoder;
|
const plannedAccel = accelerator ? accelerator.toUpperCase() : bestAccelName;
|
||||||
|
const plannedDecoder = decoder ? decoder.toUpperCase() : bestDecoderName;
|
||||||
|
const acceleratorDisplay = plannedAccel === 'AUTO' ? bestAccelName : plannedAccel;
|
||||||
|
const decoderDisplay = plannedDecoder === 'AUTO' ? bestDecoderName : plannedDecoder;
|
||||||
const encoderListDisplay = encoderAll.join(', ');
|
const encoderListDisplay = encoderAll.join(', ');
|
||||||
const decoderListDisplay = decoderAll.join(', ');
|
const decoderListDisplay = decoderAll.join(', ');
|
||||||
|
|
||||||
console.log('\n📦 Parameters:');
|
console.log('\n📦 Parameters:');
|
||||||
console.log(` Input: ${input}`);
|
console.log(` Input: ${input}`);
|
||||||
console.log(` Output: ${outputDir}`);
|
console.log(` Output: ${outputDir}`);
|
||||||
console.log(` Codec: ${codecDisplay}${codecNote}`);
|
console.log(` Codec: ${codecListDisplay}${codecNote}`);
|
||||||
console.log(` Profiles: ${displayProfiles.join(', ')}`);
|
console.log(` Profiles: ${displayProfiles.join(', ')}`);
|
||||||
console.log(` Manifests: ${manifestDesc}`);
|
console.log(` Manifests: ${manifestDesc}`);
|
||||||
console.log(` Poster: ${posterPlanned} (will be generated)`);
|
console.log(` Poster: ${posterPlanned} (will be generated)`);
|
||||||
@@ -352,8 +407,8 @@ try {
|
|||||||
outputDir,
|
outputDir,
|
||||||
customProfiles,
|
customProfiles,
|
||||||
posterTimecode,
|
posterTimecode,
|
||||||
codec: codecType,
|
codec: codecChoice,
|
||||||
format: formatType,
|
format: formatChoice,
|
||||||
segmentDuration: 2,
|
segmentDuration: 2,
|
||||||
hardwareAccelerator: accelerator,
|
hardwareAccelerator: accelerator,
|
||||||
hardwareDecoder: decoder,
|
hardwareDecoder: decoder,
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import type {
|
|||||||
ThumbnailConfig,
|
ThumbnailConfig,
|
||||||
ConversionProgress,
|
ConversionProgress,
|
||||||
CodecType,
|
CodecType,
|
||||||
|
CodecChoice,
|
||||||
StreamingFormat,
|
StreamingFormat,
|
||||||
|
StreamingFormatChoice,
|
||||||
HardwareAccelerationOption,
|
HardwareAccelerationOption,
|
||||||
HardwareAccelerator,
|
HardwareAccelerator,
|
||||||
HardwareEncoderInfo,
|
HardwareEncoderInfo,
|
||||||
@@ -41,8 +43,8 @@ export async function convertToDash(
|
|||||||
segmentDuration = 2,
|
segmentDuration = 2,
|
||||||
profiles: userProfiles,
|
profiles: userProfiles,
|
||||||
customProfiles,
|
customProfiles,
|
||||||
codec = 'dual',
|
codec = 'auto',
|
||||||
format = 'both',
|
format = 'auto',
|
||||||
hardwareDecoder,
|
hardwareDecoder,
|
||||||
hardwareAccelerator,
|
hardwareAccelerator,
|
||||||
quality,
|
quality,
|
||||||
@@ -127,8 +129,8 @@ async function convertToDashInternal(
|
|||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
userProfiles: VideoProfile[] | undefined,
|
userProfiles: VideoProfile[] | undefined,
|
||||||
customProfiles: string[] | undefined,
|
customProfiles: string[] | undefined,
|
||||||
codec: CodecType,
|
codec: CodecChoice,
|
||||||
format: StreamingFormat,
|
format: StreamingFormatChoice,
|
||||||
hardwareAccelerator: HardwareAccelerationOption | undefined,
|
hardwareAccelerator: HardwareAccelerationOption | undefined,
|
||||||
hardwareDecoder: HardwareAccelerationOption | undefined,
|
hardwareDecoder: HardwareAccelerationOption | undefined,
|
||||||
quality: DashConvertOptions['quality'],
|
quality: DashConvertOptions['quality'],
|
||||||
@@ -171,10 +173,20 @@ async function convertToDashInternal(
|
|||||||
const hardwareEncoders = await detectHardwareEncoders();
|
const hardwareEncoders = await detectHardwareEncoders();
|
||||||
const hardwareDecoders = await detectHardwareDecoders();
|
const hardwareDecoders = await detectHardwareDecoders();
|
||||||
|
|
||||||
|
const av1HardwareAvailable = hardwareEncoders.some(info => info.av1Encoder);
|
||||||
|
|
||||||
|
let wantH264 = codec === 'h264' || codec === 'auto';
|
||||||
|
let wantAv1 = codec === 'av1' || codec === 'auto';
|
||||||
|
|
||||||
|
if (codec === 'auto' && !av1HardwareAvailable) {
|
||||||
|
wantAv1 = false;
|
||||||
|
}
|
||||||
|
|
||||||
const { selected, h264Encoder, av1Encoder, warnings: accelWarnings } = selectHardwareEncoders(
|
const { selected, h264Encoder, av1Encoder, warnings: accelWarnings } = selectHardwareEncoders(
|
||||||
hardwareEncoders,
|
hardwareEncoders,
|
||||||
preferredAccelerator,
|
preferredAccelerator,
|
||||||
codec
|
wantH264,
|
||||||
|
wantAv1
|
||||||
);
|
);
|
||||||
|
|
||||||
if (accelWarnings.length > 0) {
|
if (accelWarnings.length > 0) {
|
||||||
@@ -188,14 +200,20 @@ async function convertToDashInternal(
|
|||||||
hardwareDecoder || 'auto'
|
hardwareDecoder || 'auto'
|
||||||
);
|
);
|
||||||
|
|
||||||
const av1HardwareAvailable = hardwareEncoders.some(info => info.av1Encoder);
|
if (codec === 'av1' && !av1HardwareAvailable) {
|
||||||
|
console.warn('⚠️ AV1 hardware encoder not detected. AV1 will use CPU encoder (slow).');
|
||||||
let effectiveCodec: CodecType = codec;
|
|
||||||
if (codec === 'dual' && !av1HardwareAvailable) {
|
|
||||||
console.warn('⚠️ AV1 hardware encoder not detected. Switching to H.264 only.');
|
|
||||||
effectiveCodec = 'h264';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codecsSelected: Array<'h264' | 'av1'> = [];
|
||||||
|
if (wantH264) codecsSelected.push('h264');
|
||||||
|
if (wantAv1) codecsSelected.push('av1');
|
||||||
|
if (codecsSelected.length === 0) codecsSelected.push('h264');
|
||||||
|
|
||||||
|
const formatsSelected: StreamingFormat[] = [];
|
||||||
|
if (format === 'dash' || format === 'auto') formatsSelected.push('dash');
|
||||||
|
if (format === 'hls' || format === 'auto') formatsSelected.push('hls');
|
||||||
|
if (formatsSelected.length === 0) formatsSelected.push('dash');
|
||||||
|
|
||||||
// Select profiles
|
// Select profiles
|
||||||
let profiles: VideoProfile[];
|
let profiles: VideoProfile[];
|
||||||
|
|
||||||
@@ -264,14 +282,12 @@ async function convertToDashInternal(
|
|||||||
|
|
||||||
// Determine which codecs to use based on codec parameter
|
// Determine which codecs to use based on codec parameter
|
||||||
const codecs: Array<{ type: 'h264' | 'av1'; codec: string; preset: string }> = [];
|
const codecs: Array<{ type: 'h264' | 'av1'; codec: string; preset: string }> = [];
|
||||||
|
if (codecsSelected.includes('h264')) {
|
||||||
if (effectiveCodec === 'h264' || effectiveCodec === 'dual') {
|
|
||||||
const h264Codec = h264Encoder || 'libx264';
|
const h264Codec = h264Encoder || 'libx264';
|
||||||
const h264Preset = resolvePresetForEncoder(h264Codec, 'h264');
|
const h264Preset = resolvePresetForEncoder(h264Codec, 'h264');
|
||||||
codecs.push({ type: 'h264', codec: h264Codec, preset: h264Preset });
|
codecs.push({ type: 'h264', codec: h264Codec, preset: h264Preset });
|
||||||
}
|
}
|
||||||
|
if (codecsSelected.includes('av1')) {
|
||||||
if (effectiveCodec === 'av1' || effectiveCodec === 'dual') {
|
|
||||||
const av1Codec = av1Encoder || 'libsvtav1';
|
const av1Codec = av1Encoder || 'libsvtav1';
|
||||||
const av1Preset = resolvePresetForEncoder(av1Codec, 'av1');
|
const av1Preset = resolvePresetForEncoder(av1Codec, 'av1');
|
||||||
codecs.push({ type: 'av1', codec: av1Codec, preset: av1Preset });
|
codecs.push({ type: 'av1', codec: av1Codec, preset: av1Preset });
|
||||||
@@ -343,8 +359,8 @@ async function convertToDashInternal(
|
|||||||
videoOutputDir,
|
videoOutputDir,
|
||||||
profiles,
|
profiles,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
effectiveCodec,
|
codecsSelected,
|
||||||
format,
|
formatsSelected,
|
||||||
hasAudio
|
hasAudio
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -418,8 +434,8 @@ async function convertToDashInternal(
|
|||||||
usedNvenc: codecs.some(c => c.codec.includes('nvenc')),
|
usedNvenc: codecs.some(c => c.codec.includes('nvenc')),
|
||||||
selectedAccelerator: selected,
|
selectedAccelerator: selected,
|
||||||
selectedDecoder,
|
selectedDecoder,
|
||||||
codecType: effectiveCodec,
|
codecs: codecsSelected,
|
||||||
format
|
formats: formatsSelected
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,15 +452,14 @@ const ACCEL_PRIORITY: Record<HardwareAccelerator, number> = {
|
|||||||
function selectHardwareEncoders(
|
function selectHardwareEncoders(
|
||||||
available: HardwareEncoderInfo[],
|
available: HardwareEncoderInfo[],
|
||||||
preferred: HardwareAccelerationOption,
|
preferred: HardwareAccelerationOption,
|
||||||
codec: CodecType
|
needsH264: boolean,
|
||||||
|
needsAV1: boolean
|
||||||
): {
|
): {
|
||||||
selected: HardwareAccelerator;
|
selected: HardwareAccelerator;
|
||||||
h264Encoder?: string;
|
h264Encoder?: string;
|
||||||
av1Encoder?: string;
|
av1Encoder?: string;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
} {
|
} {
|
||||||
const needsH264 = codec === 'h264' || codec === 'dual';
|
|
||||||
const needsAV1 = codec === 'av1' || codec === 'dual';
|
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|
||||||
const supportedForAuto = new Set<HardwareAccelerator>(['nvenc', 'qsv', 'amf']);
|
const supportedForAuto = new Set<HardwareAccelerator>(['nvenc', 'qsv', 'amf']);
|
||||||
@@ -456,12 +471,20 @@ function selectHardwareEncoders(
|
|||||||
const pickByAccel = (acc: HardwareAccelerator) =>
|
const pickByAccel = (acc: HardwareAccelerator) =>
|
||||||
relevant.find(item => item.accelerator === acc);
|
relevant.find(item => item.accelerator === acc);
|
||||||
|
|
||||||
|
// Явное указание CPU: никакого fallback на железо
|
||||||
|
if (preferred === 'cpu') {
|
||||||
|
return {
|
||||||
|
selected: 'cpu',
|
||||||
|
h264Encoder: undefined,
|
||||||
|
av1Encoder: undefined,
|
||||||
|
warnings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let base: HardwareEncoderInfo | undefined;
|
let base: HardwareEncoderInfo | undefined;
|
||||||
|
|
||||||
if (preferred !== 'auto') {
|
if (preferred !== 'auto') {
|
||||||
if (preferred === 'cpu') {
|
if (!supportedForAuto.has(preferred)) {
|
||||||
base = undefined;
|
|
||||||
} else if (!supportedForAuto.has(preferred)) {
|
|
||||||
warnings.push(`Ускоритель "${preferred}" пока не поддерживается, использую CPU`);
|
warnings.push(`Ускоритель "${preferred}" пока не поддерживается, использую CPU`);
|
||||||
} else {
|
} else {
|
||||||
base = pickByAccel(preferred);
|
base = pickByAccel(preferred);
|
||||||
@@ -502,7 +525,7 @@ function selectHardwareEncoders(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferred !== 'auto' && preferred !== 'cpu') {
|
if (preferred !== 'auto') {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`Ускоритель "${preferred}" не поддерживает ${codecType.toUpperCase()}, использую CPU`
|
`Ускоритель "${preferred}" не поддерживает ${codecType.toUpperCase()}, использую CPU`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -156,7 +156,14 @@ export async function encodeProfileToMP4(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Build video filter chain
|
// Build video filter chain
|
||||||
const filters: string[] = [`scale=${profile.width}:${profile.height}`];
|
const filters: string[] = [];
|
||||||
|
|
||||||
|
if (decoderAccel === 'nvenc') {
|
||||||
|
// CUDA path: keep frames on GPU
|
||||||
|
filters.push(`scale_cuda=${profile.width}:${profile.height}`);
|
||||||
|
} else {
|
||||||
|
filters.push(`scale=${profile.width}:${profile.height}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply optimizations (for future use)
|
// Apply optimizations (for future use)
|
||||||
if (optimizations) {
|
if (optimizations) {
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ export async function validateAndFixManifest(manifestPath: string): Promise<void
|
|||||||
* Changes: $RepresentationID$_$Number$ → $RepresentationID$/$RepresentationID$_$Number$
|
* Changes: $RepresentationID$_$Number$ → $RepresentationID$/$RepresentationID$_$Number$
|
||||||
*/
|
*/
|
||||||
export async function updateManifestPaths(
|
export async function updateManifestPaths(
|
||||||
manifestPath: string,
|
manifestPath: string
|
||||||
profiles: VideoProfile[],
|
|
||||||
codecType: CodecType
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let mpd = await readFile(manifestPath, 'utf-8');
|
let mpd = await readFile(manifestPath, 'utf-8');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { execMP4Box } from '../utils';
|
import { execMP4Box } from '../utils';
|
||||||
import type { VideoProfile, CodecType, StreamingFormat } from '../types';
|
import type { VideoProfile, StreamingFormat } from '../types';
|
||||||
import { readdir, rename, mkdir, writeFile } from 'node:fs/promises';
|
import { readdir, rename, mkdir, writeFile } from 'node:fs/promises';
|
||||||
import {
|
import {
|
||||||
validateAndFixManifest,
|
validateAndFixManifest,
|
||||||
@@ -21,10 +21,11 @@ export async function packageToDash(
|
|||||||
outputDir: string,
|
outputDir: string,
|
||||||
profiles: VideoProfile[],
|
profiles: VideoProfile[],
|
||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
codecType: CodecType,
|
codecs: Array<'h264' | 'av1'>,
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const manifestPath = join(outputDir, 'manifest.mpd');
|
const manifestPath = join(outputDir, 'manifest.mpd');
|
||||||
|
const useCodecSuffix = codecs.length > 1;
|
||||||
|
|
||||||
// Build MP4Box command
|
// Build MP4Box command
|
||||||
const args = [
|
const args = [
|
||||||
@@ -47,7 +48,7 @@ export async function packageToDash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Representation ID includes codec: e.g., "720p-h264", "720p-av1"
|
// Representation ID includes codec: e.g., "720p-h264", "720p-av1"
|
||||||
const representationId = codecType === 'dual' ? `${profile.name}-${codec}` : profile.name;
|
const representationId = useCodecSuffix ? `${profile.name}-${codec}` : profile.name;
|
||||||
|
|
||||||
// Add video track with representation ID
|
// Add video track with representation ID
|
||||||
args.push(`${mp4Path}#video:id=${representationId}`);
|
args.push(`${mp4Path}#video:id=${representationId}`);
|
||||||
@@ -66,13 +67,13 @@ 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, codecType, hasAudio);
|
await organizeSegments(outputDir, profiles, codecs, hasAudio);
|
||||||
|
|
||||||
// Update MPD to reflect new file structure with subdirectories
|
// Update MPD to reflect new file structure with subdirectories
|
||||||
await updateManifestPaths(manifestPath, profiles, codecType);
|
await updateManifestPaths(manifestPath);
|
||||||
|
|
||||||
// For dual-codec mode, separate H.264 and AV1 into different AdaptationSets
|
// For dual-codec mode, separate H.264 and AV1 into different AdaptationSets
|
||||||
if (codecType === 'dual') {
|
if (useCodecSuffix) {
|
||||||
await separateCodecAdaptationSets(manifestPath);
|
await separateCodecAdaptationSets(manifestPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,22 +90,18 @@ export async function packageToDash(
|
|||||||
async function organizeSegments(
|
async function organizeSegments(
|
||||||
outputDir: string,
|
outputDir: string,
|
||||||
profiles: VideoProfile[],
|
profiles: VideoProfile[],
|
||||||
codecType: CodecType,
|
codecs: Array<'h264' | 'av1'>,
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { readdir, rename, mkdir } = await import('node:fs/promises');
|
const { readdir, rename, mkdir } = await import('node:fs/promises');
|
||||||
|
|
||||||
// For dual-codec mode, create codec-specific subdirectories (e.g., "720p-h264/", "720p-av1/")
|
const useCodecSuffix = codecs.length > 1;
|
||||||
// For single-codec mode, use simple profile names (e.g., "720p/")
|
|
||||||
const codecs: Array<'h264' | 'av1'> = [];
|
|
||||||
if (codecType === 'h264' || codecType === 'dual') codecs.push('h264');
|
|
||||||
if (codecType === 'av1' || codecType === 'dual') codecs.push('av1');
|
|
||||||
|
|
||||||
const representationIds: string[] = [];
|
const representationIds: string[] = [];
|
||||||
|
|
||||||
for (const codec of codecs) {
|
for (const codec of codecs) {
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
const repId = codecType === 'dual' ? `${profile.name}-${codec}` : profile.name;
|
const repId = useCodecSuffix ? `${profile.name}-${codec}` : profile.name;
|
||||||
representationIds.push(repId);
|
representationIds.push(repId);
|
||||||
|
|
||||||
const profileDir = join(outputDir, repId);
|
const profileDir = join(outputDir, repId);
|
||||||
@@ -158,7 +155,7 @@ export async function packageToHLS(
|
|||||||
outputDir: string,
|
outputDir: string,
|
||||||
profiles: VideoProfile[],
|
profiles: VideoProfile[],
|
||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
codecType: CodecType
|
useCodecSuffix: boolean
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const manifestPath = join(outputDir, 'master.m3u8');
|
const manifestPath = join(outputDir, 'master.m3u8');
|
||||||
|
|
||||||
@@ -188,8 +185,8 @@ export async function packageToHLS(
|
|||||||
throw new Error(`MP4 file not found for profile: ${profile.name}, codec: h264`);
|
throw new Error(`MP4 file not found for profile: ${profile.name}, codec: h264`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Representation ID for HLS (no codec suffix since we only use H.264)
|
// Representation ID for HLS (добавляем суффикс, если есть несколько кодеков)
|
||||||
const representationId = profile.name;
|
const representationId = useCodecSuffix ? `${profile.name}-h264` : profile.name;
|
||||||
|
|
||||||
// Add video track with representation ID
|
// Add video track with representation ID
|
||||||
args.push(`${mp4Path}#video:id=${representationId}`);
|
args.push(`${mp4Path}#video:id=${representationId}`);
|
||||||
@@ -206,7 +203,7 @@ export async function packageToHLS(
|
|||||||
|
|
||||||
// MP4Box creates files in the same directory as output manifest
|
// MP4Box creates files in the same directory as output manifest
|
||||||
// Move segment files to profile subdirectories for clean structure
|
// Move segment files to profile subdirectories for clean structure
|
||||||
await organizeSegmentsHLS(outputDir, profiles);
|
await organizeSegmentsHLS(outputDir, profiles, useCodecSuffix);
|
||||||
|
|
||||||
// Update manifest to reflect new file structure with subdirectories
|
// Update manifest to reflect new file structure with subdirectories
|
||||||
await updateHLSManifestPaths(manifestPath, profiles);
|
await updateHLSManifestPaths(manifestPath, profiles);
|
||||||
@@ -220,12 +217,13 @@ export async function packageToHLS(
|
|||||||
*/
|
*/
|
||||||
async function organizeSegmentsHLS(
|
async function organizeSegmentsHLS(
|
||||||
outputDir: string,
|
outputDir: string,
|
||||||
profiles: VideoProfile[]
|
profiles: VideoProfile[],
|
||||||
|
useCodecSuffix: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const representationIds: string[] = [];
|
const representationIds: string[] = [];
|
||||||
|
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
const repId = profile.name; // Just profile name, no codec
|
const repId = useCodecSuffix ? `${profile.name}-h264` : profile.name;
|
||||||
representationIds.push(repId);
|
representationIds.push(repId);
|
||||||
|
|
||||||
const profileDir = join(outputDir, repId);
|
const profileDir = join(outputDir, repId);
|
||||||
@@ -275,27 +273,33 @@ export async function packageToFormats(
|
|||||||
outputDir: string,
|
outputDir: string,
|
||||||
profiles: VideoProfile[],
|
profiles: VideoProfile[],
|
||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
codec: CodecType,
|
codecs: Array<'h264' | 'av1'>,
|
||||||
format: StreamingFormat,
|
formats: StreamingFormat[],
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
): Promise<{ manifestPath?: string; hlsManifestPath?: string }> {
|
): Promise<{ manifestPath?: string; hlsManifestPath?: string }> {
|
||||||
|
|
||||||
let manifestPath: string | undefined;
|
let manifestPath: string | undefined;
|
||||||
let hlsManifestPath: string | undefined;
|
let hlsManifestPath: string | undefined;
|
||||||
|
|
||||||
// Step 1: Generate DASH segments and manifest using MP4Box
|
const needSegments = formats.length > 0;
|
||||||
if (format === 'dash' || format === 'both') {
|
const needDash = formats.includes('dash');
|
||||||
manifestPath = await packageToDash(codecMP4Files, outputDir, profiles, segmentDuration, codec, hasAudio);
|
const needHls = formats.includes('hls');
|
||||||
|
|
||||||
|
// Step 1: Generate DASH segments and manifest using MP4Box (segments нужны для обоих форматов)
|
||||||
|
if (needSegments) {
|
||||||
|
manifestPath = await packageToDash(codecMP4Files, outputDir, profiles, segmentDuration, codecs, hasAudio);
|
||||||
|
if (!needDash) {
|
||||||
|
manifestPath = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Generate HLS playlists from existing segments
|
// Step 2: Generate HLS playlists from existing segments
|
||||||
if (format === 'hls' || format === 'both') {
|
if (needHls) {
|
||||||
// HLS generation from segments
|
|
||||||
hlsManifestPath = await generateHLSPlaylists(
|
hlsManifestPath = await generateHLSPlaylists(
|
||||||
outputDir,
|
outputDir,
|
||||||
profiles,
|
profiles,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
codec,
|
codecs.length > 1,
|
||||||
hasAudio
|
hasAudio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -310,7 +314,7 @@ async function generateHLSPlaylists(
|
|||||||
outputDir: string,
|
outputDir: string,
|
||||||
profiles: VideoProfile[],
|
profiles: VideoProfile[],
|
||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
codecType: CodecType,
|
useCodecSuffix: boolean,
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const masterPlaylistPath = join(outputDir, 'master.m3u8');
|
const masterPlaylistPath = join(outputDir, 'master.m3u8');
|
||||||
@@ -318,7 +322,7 @@ async function generateHLSPlaylists(
|
|||||||
|
|
||||||
// Generate media playlist for each H.264 profile
|
// Generate media playlist for each H.264 profile
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
const profileDir = codecType === 'dual' ? `${profile.name}-h264` : profile.name;
|
const profileDir = useCodecSuffix ? `${profile.name}-h264` : profile.name;
|
||||||
const profilePath = join(outputDir, profileDir);
|
const profilePath = join(outputDir, profileDir);
|
||||||
|
|
||||||
// Read segment files from profile directory
|
// Read segment files from profile directory
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export type {
|
|||||||
VideoMetadata,
|
VideoMetadata,
|
||||||
VideoOptimizations,
|
VideoOptimizations,
|
||||||
CodecType,
|
CodecType,
|
||||||
|
CodecChoice,
|
||||||
|
StreamingFormat,
|
||||||
|
StreamingFormatChoice,
|
||||||
HardwareAccelerator,
|
HardwareAccelerator,
|
||||||
HardwareAccelerationOption,
|
HardwareAccelerationOption,
|
||||||
HardwareEncoderInfo,
|
HardwareEncoderInfo,
|
||||||
@@ -26,7 +29,9 @@ export {
|
|||||||
getVideoMetadata,
|
getVideoMetadata,
|
||||||
selectAudioBitrate,
|
selectAudioBitrate,
|
||||||
detectHardwareEncoders,
|
detectHardwareEncoders,
|
||||||
detectHardwareDecoders
|
detectHardwareDecoders,
|
||||||
|
testEncoder,
|
||||||
|
testDecoder
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
// Profile exports
|
// Profile exports
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* Video codec type for encoding
|
* Video codec type for encoding
|
||||||
*/
|
*/
|
||||||
export type CodecType = 'av1' | 'h264' | 'dual';
|
export type CodecType = 'av1' | 'h264';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streaming format type
|
* Streaming format type
|
||||||
*/
|
*/
|
||||||
export type StreamingFormat = 'dash' | 'hls' | 'both';
|
export type StreamingFormat = 'dash' | 'hls';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пользовательский выбор кодека (auto = h264 + av1 при наличии HW)
|
||||||
|
*/
|
||||||
|
export type CodecChoice = CodecType | 'auto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пользовательский выбор форматов (auto = dash + hls)
|
||||||
|
*/
|
||||||
|
export type StreamingFormatChoice = StreamingFormat | 'auto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Тип аппаратного ускорителя
|
* Тип аппаратного ускорителя
|
||||||
@@ -75,11 +85,11 @@ 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') */
|
/** Video codec selection: h264, av1, or auto (default: auto = h264 + AV1 if HW) */
|
||||||
codec?: CodecType;
|
codec?: CodecChoice;
|
||||||
|
|
||||||
/** Streaming format to generate: 'dash', 'hls', or 'both' (default: 'both') */
|
/** Streaming formats: dash, hls, or auto (default: auto = оба) */
|
||||||
format?: StreamingFormat;
|
format?: StreamingFormatChoice;
|
||||||
|
|
||||||
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
|
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
|
||||||
hardwareAccelerator?: HardwareAccelerationOption;
|
hardwareAccelerator?: HardwareAccelerationOption;
|
||||||
@@ -172,10 +182,10 @@ export interface ConversionProgress {
|
|||||||
* Result of DASH conversion
|
* Result of DASH conversion
|
||||||
*/
|
*/
|
||||||
export interface DashConvertResult {
|
export interface DashConvertResult {
|
||||||
/** Path to generated DASH manifest (if format is 'dash' or 'both') */
|
/** Path to generated DASH manifest (если форматы включают DASH) */
|
||||||
manifestPath?: string;
|
manifestPath?: string;
|
||||||
|
|
||||||
/** Path to generated HLS manifest (if format is 'hls' or 'both') */
|
/** Path to generated HLS manifest (если форматы включают HLS) */
|
||||||
hlsManifestPath?: string;
|
hlsManifestPath?: string;
|
||||||
|
|
||||||
/** Paths to generated video segments */
|
/** Paths to generated video segments */
|
||||||
@@ -204,11 +214,11 @@ export interface DashConvertResult {
|
|||||||
/** Выбранный аппаратный декодер */
|
/** Выбранный аппаратный декодер */
|
||||||
selectedDecoder: HardwareAccelerator;
|
selectedDecoder: HardwareAccelerator;
|
||||||
|
|
||||||
/** Codec type used for encoding */
|
/** Список использованных кодеков */
|
||||||
codecType: CodecType;
|
codecs: CodecType[];
|
||||||
|
|
||||||
/** Streaming format generated */
|
/** Список сгенерированных форматов */
|
||||||
format: StreamingFormat;
|
formats: StreamingFormat[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export {
|
|||||||
checkAV1Support,
|
checkAV1Support,
|
||||||
detectHardwareEncoders,
|
detectHardwareEncoders,
|
||||||
detectHardwareDecoders,
|
detectHardwareDecoders,
|
||||||
|
testEncoder,
|
||||||
|
testDecoder,
|
||||||
execFFmpeg,
|
execFFmpeg,
|
||||||
execMP4Box,
|
execMP4Box,
|
||||||
setLogFile
|
setLogFile
|
||||||
|
|||||||
@@ -193,6 +193,57 @@ export async function detectHardwareDecoders(): Promise<HardwareDecoderInfo[]> {
|
|||||||
return decoders;
|
return decoders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простой smoke-тест энкодера FFmpeg (1 кадр из testsrc)
|
||||||
|
*/
|
||||||
|
export async function testEncoder(encoder: string): Promise<boolean> {
|
||||||
|
const args = [
|
||||||
|
'-v', 'error',
|
||||||
|
'-f', 'lavfi',
|
||||||
|
'-i', 'testsrc=size=320x240:rate=1',
|
||||||
|
'-frames:v', '1',
|
||||||
|
'-an',
|
||||||
|
'-c:v', encoder,
|
||||||
|
'-f', 'null', '-'
|
||||||
|
];
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const proc = spawn('ffmpeg', args);
|
||||||
|
proc.on('error', () => resolve(false));
|
||||||
|
proc.on('close', (code) => resolve(code === 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простой smoke-тест декодера FFmpeg (1 кадр входного файла)
|
||||||
|
*/
|
||||||
|
export async function testDecoder(accel: HardwareAccelerator, input: string): Promise<boolean> {
|
||||||
|
const args = ['-v', 'error'];
|
||||||
|
|
||||||
|
if (accel === 'nvenc') {
|
||||||
|
args.push('-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda');
|
||||||
|
} else if (accel === 'qsv') {
|
||||||
|
args.push('-hwaccel', 'qsv');
|
||||||
|
} else if (accel === 'vaapi') {
|
||||||
|
args.push('-hwaccel', 'vaapi', '-vaapi_device', '/dev/dri/renderD128');
|
||||||
|
} else if (accel === 'videotoolbox') {
|
||||||
|
args.push('-hwaccel', 'videotoolbox');
|
||||||
|
} else if (accel === 'v4l2') {
|
||||||
|
args.push('-hwaccel', 'v4l2m2m');
|
||||||
|
} else if (accel === 'amf') {
|
||||||
|
// AMF декод чаще всего не используется напрямую; считаем недоступным
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('-i', input, '-frames:v', '1', '-f', 'null', '-');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const proc = spawn('ffmpeg', args);
|
||||||
|
proc.on('error', () => resolve(false));
|
||||||
|
proc.on('close', (code) => resolve(code === 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute FFmpeg command with progress tracking
|
* Execute FFmpeg command with progress tracking
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user