Compare commits
25 Commits
f550b7eb69
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b40ae34387 | |||
| 84231d705f | |||
| 2c8d9d1e9e | |||
| 41fe1a7370 | |||
| 55fb1f640a | |||
| 4293b6735a | |||
| 248fe15b62 | |||
| 81add91669 | |||
| b6c191290c | |||
| 5ab30eee4c | |||
| 187697eca6 | |||
| 346eb697cf | |||
| b8f9f0e046 | |||
| 0813bea1d4 | |||
| 3bc980ef1d | |||
| 13b624480d | |||
| b7e264d56f | |||
| 970b58c2a4 | |||
| 17748d3900 | |||
| 8575757f81 | |||
| 73289b3050 | |||
| 970f2a7daa | |||
| 69b3a4804f | |||
| 88fc443cb6 | |||
| 224f14a8e0 |
88
README.md
88
README.md
@@ -5,24 +5,14 @@
|
||||
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:**
|
||||
- ⚡ 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)
|
||||
- 📊 Quality profiles: multiple bitrates/FPS (auto or custom)
|
||||
- 🖼️ Preview: thumbnail sprite + VTT, poster from the first frame
|
||||
- ⏱️ Progress: per-profile and overall CLI progress bars
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run via npx (no install)
|
||||
npx @gromlab/create-vod video.mp4
|
||||
|
||||
# Or install globally
|
||||
npm install -g @gromlab/create-vod
|
||||
create-vod video.mp4
|
||||
```
|
||||
|
||||
**System requirements:**
|
||||
## Install
|
||||
For the CLI to work correctly, FFmpeg and MP4Box must be installed in the system.
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S ffmpeg gpac
|
||||
@@ -34,12 +24,33 @@ sudo apt install ffmpeg gpac
|
||||
brew install ffmpeg gpac
|
||||
```
|
||||
|
||||
**Output:** A folder `video/` in the current directory with segments under `{profile}-{codec}/`, DASH/HLS manifests in the root, poster, and thumbnail sprite/VTT.
|
||||
## Quick Start
|
||||
Before running, make sure `FFmpeg` and `MP4Box` are installed (see Install).
|
||||
|
||||
```bash
|
||||
npx @gromlab/create-vod video.mp4
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
```bash
|
||||
create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-p poster-timecode]
|
||||
create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode] [-e encoder] [-d decoder] [-m]
|
||||
```
|
||||
|
||||
### Main arguments
|
||||
@@ -51,40 +62,44 @@ create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description | Format | Example |
|
||||
|--------|----------------------------|----------------------------|---------------------------------|
|
||||
| `-r, --resolutions` | Quality profiles | `360`, `720@60`, `1080-60` | `-r 720,1080,1440@60` |
|
||||
| `-c, --codec` | Video codec | `h264`, `av1`, `dual` | `-c dual` (default) |
|
||||
| `-f, --format` | Streaming format | `dash`, `hls`, `both` | `-f both` (default) |
|
||||
| `-p, --poster` | Poster timecode | `HH:MM:SS` or seconds | `-p 00:00:05` or `-p 10` |
|
||||
| `--accel` | Hardware accelerator | `auto`, `nvenc`, `qsv`, `amf`, `cpu` | `--accel nvenc` |
|
||||
| Option | Description | Values / Format | Default | Example |
|
||||
|--------|----------------------------|----------------------------|----------|---------------------------------|
|
||||
| `-r, --resolutions` | Quality profiles | `360`, `720@60`, `1080-60` | auto | `-r 720,1080,1440@60` |
|
||||
| `-c, --codec` | Video codec(s) | `h264`, `av1` (comma/space separated) | `h264` | `-c h264,av1` |
|
||||
| `-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` |
|
||||
| `-d, --decoder` | Video decoder (hwaccel) | `auto`, `nvenc`, `qsv`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-d cpu` |
|
||||
| `-m, --muted` | Disable audio track | flag | off | `-m` |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Default (DASH + HLS, dual codec, auto profiles)
|
||||
create-vod video.mp4
|
||||
# Default (DASH + HLS, auto profiles)
|
||||
npx @gromlab/create-vod video.mp4
|
||||
|
||||
# Custom output directory
|
||||
create-vod video.mp4 ./output
|
||||
npx @gromlab/create-vod video.mp4 ./output
|
||||
|
||||
# Selected resolutions
|
||||
create-vod video.mp4 -r 720,1080,1440
|
||||
npx @gromlab/create-vod video.mp4 -r 720,1080,1440
|
||||
|
||||
# High FPS
|
||||
create-vod video.mp4 -r 720@60,1080@60
|
||||
|
||||
# DASH only
|
||||
create-vod video.mp4 -f dash
|
||||
|
||||
# HLS only (Safari/iOS)
|
||||
create-vod video.mp4 -f hls -c h264
|
||||
npx @gromlab/create-vod video.mp4 -r 720@60,1080@60
|
||||
|
||||
# Poster from 5th second
|
||||
create-vod video.mp4 -p 5
|
||||
npx @gromlab/create-vod video.mp4 -p 5
|
||||
|
||||
# Force CPU encode/decode
|
||||
npx @gromlab/create-vod video.mp4 -c h264 -e cpu -d cpu
|
||||
|
||||
# Force GPU encode + CPU decode
|
||||
npx @gromlab/create-vod video.mp4 -c h264 -e nvenc -d cpu
|
||||
|
||||
# Combined parameters
|
||||
create-vod video.mp4 ./output -r 720,1080@60,1440@60 -c dual -f both -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
|
||||
@@ -103,11 +118,12 @@ High FPS (60/90/120) are generated only if the source supports that FPS.
|
||||
## Defaults & Automation
|
||||
|
||||
- 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
|
||||
- Bitrate: BPP-based dynamic calculation
|
||||
- Thumbnails: auto sprite (160×90, 1s interval) + VTT
|
||||
- Poster: first frame (0:00:00, configurable via `-p`)
|
||||
- Parallel encoding: enabled
|
||||
- AV1: enabled only if hardware AV1 encoder detected (in auto mode); otherwise stays on H.264
|
||||
|
||||
**Requirements:** Node.js ≥18.0.0, FFmpeg, MP4Box (gpac), optional NVIDIA/Intel/AMD GPU for acceleration
|
||||
|
||||
77
README_RU.md
77
README_RU.md
@@ -11,18 +11,8 @@ CLI инструмент для конвертации видео в форма
|
||||
- 🖼️ Превью: thumbnail спрайты + VTT, постер с первого кадра
|
||||
- ⏱️ Прогресс: CLI прогресс-бары по профилям и общему этапу
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# Использование через npx (без установки)
|
||||
npx @gromlab/create-vod video.mp4
|
||||
|
||||
# Или глобальная установка
|
||||
npm install -g @gromlab/create-vod
|
||||
create-vod video.mp4
|
||||
```
|
||||
|
||||
**Системные требования:**
|
||||
## Install
|
||||
Для корректной работы CLI требуется установленые в системе FFmpeg и MP4Box.
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S ffmpeg gpac
|
||||
@@ -34,12 +24,33 @@ sudo apt install ffmpeg gpac
|
||||
brew install ffmpeg gpac
|
||||
```
|
||||
|
||||
**Результат:** В текущей директории будет создана папка `video/` с сегментами в папках `{profile}-{codec}/`, манифестами DASH и HLS в корне, постером и превью спрайтами.
|
||||
## Быстрый старт
|
||||
Перед запуском убедитесь, что в системе установлены `FFmpeg` и `MP4Box` (см. Install).
|
||||
```bash
|
||||
npx @gromlab/create-vod video.mp4
|
||||
```
|
||||
|
||||
**Результат:** В текущей директории появится структура выходных файлов:
|
||||
```
|
||||
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
|
||||
|
||||
```bash
|
||||
create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-p poster-timecode]
|
||||
create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode] [-e encoder] [-d decoder] [-m]
|
||||
```
|
||||
|
||||
### Основные параметры
|
||||
@@ -51,40 +62,38 @@ create-vod <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-
|
||||
|
||||
### Опциональные ключи
|
||||
|
||||
| Ключ | Описание | Формат | Пример |
|
||||
|------|----------|--------|--------|
|
||||
| `-r, --resolutions` | Выбор профилей качества | `360`, `720@60`, `1080-60` | `-r 720,1080,1440@60` |
|
||||
| `-c, --codec` | Видео кодек | `h264`, `av1`, `dual` | `-c dual` (по умолчанию) |
|
||||
| `-f, --format` | Формат стриминга | `dash`, `hls`, `both` | `-f both` (по умолчанию) |
|
||||
| `-p, --poster` | Таймкод для постера | `HH:MM:SS` или секунды | `-p 00:00:05` или `-p 10` |
|
||||
| `--accel` | Аппаратный ускоритель | `auto`, `nvenc`, `qsv`, `amf`, `cpu` | `--accel nvenc` |
|
||||
| Ключ | Описание | Значения / формат | По умолчанию | Пример |
|
||||
|------|----------|-------------------|--------------|--------|
|
||||
| `-r, --resolutions` | Выбор профилей качества | `360`, `720@60`, `1080-60` | авто | `-r 720,1080,1440@60` |
|
||||
| `-c, --codec` | Видео кодек(и) | `h264`, `av1` (через пробел или запятую) | `h264` | `-c h264,av1` |
|
||||
| `-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` |
|
||||
| `-d, --decoder` | Видео декодер (hwaccel) | `auto`, `nvenc`, `qsv`, `vaapi`, `videotoolbox`, `v4l2`, `cpu` | `auto` | `-d cpu` |
|
||||
| `-m, --muted` | Отключить аудио дорожку в выходных файлах | `flag` | `off` | `-m` |
|
||||
|
||||
### Примеры использования
|
||||
|
||||
```bash
|
||||
# Базовая конвертация (DASH + HLS, dual codec, автопрофили)
|
||||
create-vod video.mp4
|
||||
# Базовая конвертация (DASH + HLS, авто кодек, автопрофили)
|
||||
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 для игровых стримов
|
||||
create-vod video.mp4 -r 720@60,1080@60
|
||||
|
||||
# Только DASH формат
|
||||
create-vod video.mp4 -f dash
|
||||
|
||||
# Только HLS для Safari/iOS
|
||||
create-vod video.mp4 -f hls -c h264
|
||||
npx @gromlab/create-vod video.mp4 -r 720@60,1080@60
|
||||
|
||||
# Постер с 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 -c dual -f both -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
|
||||
```
|
||||
|
||||
### Поддерживаемые разрешения
|
||||
|
||||
114
bin/cli.js
114
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 (максимальная совместимость)
|
||||
- `av1` — только AV1 (лучшее сжатие, новые браузеры)
|
||||
- `dual` — оба кодека (рекомендуется)
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
@@ -75,35 +74,56 @@ create-vod video.mp4 -c h264
|
||||
|
||||
# Только AV1 (медленнее, меньше места)
|
||||
create-vod video.mp4 -c av1
|
||||
|
||||
# Оба кодека (максимальная совместимость)
|
||||
create-vod video.mp4 -c dual
|
||||
```
|
||||
|
||||
**GPU ускорение:**
|
||||
- H.264: `h264_nvenc` (NVIDIA), fallback → `libx264` (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
|
||||
- `qsv` — Intel Quick Sync
|
||||
- `amf` — AMD AMF
|
||||
- `cpu` — принудительно без GPU
|
||||
- `vaapi` — VAAPI (Linux)
|
||||
- `videotoolbox` — Apple VT (macOS)
|
||||
- `v4l2` — V4L2 M2M (ARM/SBC)
|
||||
- `cpu` — принудительно без GPU (`libx264`/`libsvtav1`)
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
create-vod video.mp4 --accel nvenc
|
||||
create-vod video.mp4 --accel qsv
|
||||
create-vod video.mp4 --accel cpu # отключить GPU
|
||||
create-vod video.mp4 -e nvenc
|
||||
create-vod video.mp4 -e qsv
|
||||
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)
|
||||
- `hls` — только HLS (HTTP Live Streaming)
|
||||
- `both` — оба формата
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
@@ -125,8 +144,8 @@ create-vod video.mp4 -f dash
|
||||
# Только HLS (для Safari/iOS)
|
||||
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) | Стандарт индустрии |
|
||||
| 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)
|
||||
|
||||
**Файловая структура:**
|
||||
@@ -163,12 +181,7 @@ output/
|
||||
└── thumbnails.vtt
|
||||
```
|
||||
|
||||
**Преимущества структуры:**
|
||||
- Сегменты хранятся один раз (нет дублирования)
|
||||
- DASH и HLS используют одни и те же .m4s файлы
|
||||
- Экономия 50% места при `format=both`
|
||||
|
||||
**По умолчанию:** `both` (максимальная совместимость)
|
||||
**По умолчанию:** генерируются DASH и HLS
|
||||
|
||||
---
|
||||
|
||||
@@ -198,7 +211,7 @@ create-vod video.mp4 -p 00:01:30
|
||||
### Базовое использование
|
||||
|
||||
```bash
|
||||
# Простейший запуск (оба формата, dual codec, автопрофили)
|
||||
# Простейший запуск (оба формата, авто кодек, автопрофили)
|
||||
create-vod video.mp4
|
||||
|
||||
# С указанием выходной директории
|
||||
@@ -226,9 +239,6 @@ create-vod video.mp4 -c h264
|
||||
|
||||
# Лучшее сжатие (только 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
|
||||
create-vod video.mp4 -f hls -c h264
|
||||
|
||||
# Оба формата для всех устройств
|
||||
create-vod video.mp4 -f both -c dual
|
||||
```
|
||||
|
||||
### Комбинированные примеры
|
||||
|
||||
```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)
|
||||
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
|
||||
@@ -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
|
||||
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)
|
||||
- Все современные устройства поддерживаются
|
||||
- Все современные устройства поддерживаются; AV1 добавляется при наличии HW
|
||||
|
||||
### Для быстрой разработки
|
||||
|
||||
@@ -341,7 +348,7 @@ create-vod video.mp4 -r 720 -c h264 -f dash
|
||||
### Для продакшена
|
||||
|
||||
```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 контента
|
||||
|
||||
```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 для премиум контента.
|
||||
@@ -367,10 +374,10 @@ create-vod video.mp4 -r 720,1080,1440,2160 -c dual -f both
|
||||
|
||||
**Решение:**
|
||||
```bash
|
||||
# Используйте h264 или dual
|
||||
# Используйте h264 или auto
|
||||
create-vod video.mp4 -f hls -c h264
|
||||
# или
|
||||
create-vod video.mp4 -f hls -c dual
|
||||
create-vod video.mp4 -f hls
|
||||
```
|
||||
|
||||
### FPS источника ниже запрошенного
|
||||
|
||||
40
docs/QUALITY.md
Normal file
40
docs/QUALITY.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Регулировка качества и измерение
|
||||
|
||||
## Целевое качество
|
||||
- Ориентир: средний VMAF ≥ 93 для кодированного видео.
|
||||
|
||||
## Что регулируем
|
||||
- GPU-энкодеры используют CQ (Constant Quality).
|
||||
- CPU-энкодеры используют CRF (Constant Rate Factor).
|
||||
- Значения по умолчанию взяты из текущей логики кодирования (см. `src/core/encoding.ts`) и могут быть переопределены через CLI (`--h264-cq`, `--h264-crf`, `--av1-cq`, `--av1-crf`).
|
||||
|
||||
## Дефолтные CQ/CRF
|
||||
|
||||
| Энкодер | H.264 | AV1 |
|
||||
|------------------|-----------------|----------------|
|
||||
| NVENC | CQ 32 | CQ 42 |
|
||||
| QSV | CQ 32 | CQ 42 |
|
||||
| AMF | CQ 32 | CQ 42 |
|
||||
| VAAPI | CQ 32 | CQ 42 |
|
||||
| videotoolbox | CQ 32 | CQ 42 |
|
||||
| v4l2 | CQ 32 | CQ 42 |
|
||||
| CPU (libx264 / libsvtav1) | CRF 25→20* | CRF 40→28* |
|
||||
|
||||
\* CPU значения зависят от целевого разрешения:
|
||||
- 360p: H.264 CRF 25, AV1 CRF 40
|
||||
- 480p: H.264 CRF 24, AV1 CRF 38
|
||||
- 720p: H.264 CRF 23, AV1 CRF 35
|
||||
- 1080p: H.264 CRF 22, AV1 CRF 32
|
||||
- 1440p: H.264 CRF 21, AV1 CRF 30
|
||||
- 2160p: H.264 CRF 20, AV1 CRF 28
|
||||
|
||||
## Как мерить качество (VMAF)
|
||||
1. Закодируйте тестовый ролик с нужными параметрами (CQ/CRF).
|
||||
2. Сравните с исходником по VMAF (разрешения должны совпадать, при необходимости приведите к одному размеру).
|
||||
3. Пример команды FFmpeg c libvmaf:
|
||||
```bash
|
||||
ffmpeg -i encoded.mp4 -i source.mp4 \
|
||||
-lavfi "[0:v]setpts=PTS-STARTPTS[enc];[1:v]setpts=PTS-STARTPTS[ref];[enc][ref]libvmaf=log_path=vmaf.json:log_fmt=json" \
|
||||
-f null -
|
||||
```
|
||||
В логах ищите `VMAF score`; если < 93 — увеличьте качество (понизьте CQ/CRF), если > 95 и нужен меньший битрейт — можно чуть поднять CQ/CRF.
|
||||
@@ -469,7 +469,7 @@ chmod +x test_codec.sh
|
||||
# - H.264 для старых устройств (iOS < 14)
|
||||
|
||||
# Пример: 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)
|
||||
- Добавлены все команды для тестирования
|
||||
- Добавлены результаты сравнения на видео 1920×1080, 135 сек
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gromlab/create-vod",
|
||||
"author": "Gromov Sergei",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.15",
|
||||
"description": "DASH/HLS video converter with hardware acceleration (NVENC/QSV/AMF/VAAPI), thumbnails and poster generation",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
@@ -19,6 +19,7 @@
|
||||
"dist",
|
||||
"bin",
|
||||
"README.md",
|
||||
"README_RU.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
|
||||
217
src/cli.ts
217
src/cli.ts
@@ -10,19 +10,18 @@
|
||||
* create-vod ./video.mp4 ./output -r 720,1080
|
||||
*/
|
||||
|
||||
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders } from './index';
|
||||
import { convertToDash, checkFFmpeg, checkMP4Box, getVideoMetadata, detectHardwareEncoders, detectHardwareDecoders, testEncoder, testDecoder } from './index';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { statSync } from 'node:fs';
|
||||
import { basename, extname } from 'node:path';
|
||||
import type { CodecType, StreamingFormat, QualitySettings, HardwareAccelerationOption } from './types';
|
||||
import type { QualitySettings, HardwareAccelerationOption, HardwareAccelerator, CodecType } from './types';
|
||||
import { selectProfiles, createProfilesFromStrings } from './config/profiles';
|
||||
|
||||
// Parse arguments
|
||||
const args = process.argv.slice(2);
|
||||
let customProfiles: string[] | undefined;
|
||||
let posterTimecode: string | undefined;
|
||||
let codecType: CodecType = 'dual'; // Default to dual codec
|
||||
let formatType: StreamingFormat = 'both'; // Default to both formats (DASH + HLS)
|
||||
let codecChoice: Array<CodecType> | undefined; // default h264
|
||||
const positionalArgs: string[] = [];
|
||||
|
||||
// Quality settings
|
||||
@@ -31,6 +30,8 @@ let h264CRF: number | undefined;
|
||||
let av1CQ: number | undefined;
|
||||
let av1CRF: number | undefined;
|
||||
let accelerator: HardwareAccelerationOption | undefined;
|
||||
let decoder: HardwareAccelerationOption | undefined;
|
||||
let muted = false;
|
||||
|
||||
// First pass: extract flags and their values
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -56,22 +57,16 @@ for (let i = 0; i < args.length; i++) {
|
||||
posterTimecode = args[i + 1];
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '-c' || args[i] === '--codec') {
|
||||
const codec = args[i + 1];
|
||||
if (codec === 'av1' || codec === 'h264' || codec === 'dual') {
|
||||
codecType = codec;
|
||||
} else {
|
||||
console.error(`❌ Invalid codec: ${codec}. Valid options: av1, h264, dual`);
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '-f' || args[i] === '--format') {
|
||||
const format = args[i + 1];
|
||||
if (format === 'dash' || format === 'hls' || format === 'both') {
|
||||
formatType = format;
|
||||
} else {
|
||||
console.error(`❌ Invalid format: ${format}. Valid options: dash, hls, both`);
|
||||
process.exit(1);
|
||||
const codecArg = args[i + 1];
|
||||
const parts = codecArg.split(/[,\s]+/).map(p => p.trim()).filter(Boolean);
|
||||
const allowed = new Set(['h264', 'av1']);
|
||||
for (const p of parts) {
|
||||
if (!allowed.has(p)) {
|
||||
console.error(`❌ Invalid codec: ${p}. Valid options: av1, h264`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
codecChoice = Array.from(new Set(parts)) as Array<'h264' | 'av1'>;
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '--h264-cq') {
|
||||
h264CQ = parseInt(args[i + 1]);
|
||||
@@ -101,15 +96,26 @@ for (let i = 0; i < args.length; i++) {
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip next arg
|
||||
} else if (args[i] === '--accel' || args[i] === '--hardware') {
|
||||
} else if (args[i] === '-e' || args[i] === '--encoder') {
|
||||
const acc = args[i + 1];
|
||||
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu'];
|
||||
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'cpu', 'vaapi', 'videotoolbox', 'v4l2'];
|
||||
if (!allowed.includes(acc as HardwareAccelerationOption)) {
|
||||
console.error(`❌ Invalid accelerator: ${acc}. Valid: auto, nvenc, qsv, amf, cpu`);
|
||||
console.error(`❌ Invalid accelerator: ${acc}. Valid: auto, nvenc, qsv, amf, vaapi, videotoolbox, v4l2, cpu`);
|
||||
process.exit(1);
|
||||
}
|
||||
accelerator = acc as HardwareAccelerationOption;
|
||||
i++;
|
||||
} else if (args[i] === '-d' || args[i] === '--decoder') {
|
||||
const acc = args[i + 1];
|
||||
const allowed: HardwareAccelerationOption[] = ['auto', 'nvenc', 'qsv', 'amf', 'vaapi', 'videotoolbox', 'v4l2', 'cpu'];
|
||||
if (!allowed.includes(acc as HardwareAccelerationOption)) {
|
||||
console.error(`❌ Invalid decoder: ${acc}. Valid: auto, nvenc, qsv, amf, vaapi, videotoolbox, v4l2, cpu`);
|
||||
process.exit(1);
|
||||
}
|
||||
decoder = acc as HardwareAccelerationOption;
|
||||
i++;
|
||||
} else if (args[i] === '-m' || args[i] === '--muted') {
|
||||
muted = true;
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
// Positional argument
|
||||
positionalArgs.push(args[i]);
|
||||
@@ -124,10 +130,12 @@ if (!input) {
|
||||
console.error('❌ Usage: create-vod <input-video> [output-dir] [options]');
|
||||
console.error('\nOptions:');
|
||||
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(' -f, --format Streaming format: dash, hls, or both (default: both)');
|
||||
console.error(' -c, --codec Video codec: av1 or h264 (default: auto = h264 + AV1 if HW)');
|
||||
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(' --accel <type> Hardware accelerator: auto|nvenc|qsv|amf|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(' -m, --muted Disable audio track (no audio in output)');
|
||||
console.error('\nQuality Options (override defaults):');
|
||||
console.error(' --h264-cq <value> H.264 GPU CQ value (0-51, lower = better, default: auto)');
|
||||
console.error(' --h264-crf <value> H.264 CPU CRF value (0-51, lower = better, default: auto)');
|
||||
@@ -138,12 +146,11 @@ if (!input) {
|
||||
console.error(' create-vod video.mp4 ./output');
|
||||
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 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 -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 -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);
|
||||
}
|
||||
|
||||
@@ -152,6 +159,7 @@ console.log('🔍 Checking system...\n');
|
||||
const hasFFmpeg = await checkFFmpeg();
|
||||
const hasMP4Box = await checkMP4Box();
|
||||
const hwEncoders = await detectHardwareEncoders();
|
||||
const hwDecoders = await detectHardwareDecoders();
|
||||
const hasAv1Hardware = hwEncoders.some(item => item.av1Encoder);
|
||||
|
||||
const accelPriority: Record<string, number> = {
|
||||
@@ -160,22 +168,77 @@ const accelPriority: Record<string, number> = {
|
||||
amf: 80,
|
||||
vaapi: 70,
|
||||
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()
|
||||
.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(`MP4Box: ${hasMP4Box ? '✅' : '❌'}`);
|
||||
const accelList = Array.from(new Set(hwEncoders.map(e => e.accelerator.toUpperCase())));
|
||||
const bestAccelName = bestAccel ? bestAccel.accelerator.toUpperCase() : undefined;
|
||||
const accelRest = accelList.filter(name => name !== bestAccelName);
|
||||
const accelLabel = bestAccelName
|
||||
? `✅ ${bestAccelName}${accelRest.length > 0 ? ` (${accelRest.join(', ')})` : ''}`
|
||||
: '❌';
|
||||
console.log(`Hardware: ${accelLabel}`);
|
||||
const accelList = Array.from(new Set(availableEncoders.map(e => e.toUpperCase())));
|
||||
const decList = Array.from(new Set(availableDecoders.map(d => d.toUpperCase())));
|
||||
|
||||
const encoderSelectedPlanned = accelerator
|
||||
? accelerator.toUpperCase()
|
||||
: ((bestAccel && bestAccel.toUpperCase()) || 'CPU');
|
||||
const encoderAll = accelList.length > 0 ? accelList : ['CPU'];
|
||||
|
||||
const decoderSelectedPlanned = decoder
|
||||
? decoder.toUpperCase()
|
||||
: ((bestDecoder && bestDecoder.toUpperCase()) || 'CPU');
|
||||
const decoderAll = decList.length > 0 ? decList : ['CPU'];
|
||||
|
||||
console.log(`Encoder: ${encoderSelectedPlanned === 'AUTO' ? ((bestAccel && bestAccel.toUpperCase()) || 'CPU') : encoderSelectedPlanned} (${encoderAll.join(', ')})`);
|
||||
console.log(`Decoder: ${decoderSelectedPlanned === 'AUTO' ? ((bestDecoder && bestDecoder.toUpperCase()) || 'CPU') : decoderSelectedPlanned} (${decoderAll.join(', ')})`);
|
||||
console.log('');
|
||||
|
||||
if (!hasFFmpeg) {
|
||||
@@ -188,25 +251,26 @@ if (!hasMP4Box) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate codec selection
|
||||
if ((codecType === 'av1' || codecType === 'dual') && !hasAv1Hardware) {
|
||||
if (codecType === 'av1') {
|
||||
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(` 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 codec selection
|
||||
const codecsRequested = codecChoice && codecChoice.length > 0 ? codecChoice : ['h264'];
|
||||
let includeH264 = codecsRequested.includes('h264');
|
||||
let includeAv1 = codecsRequested.includes('av1');
|
||||
|
||||
if (!includeH264) {
|
||||
console.warn('⚠️ H.264 is mandatory for compatibility. Adding H.264.');
|
||||
includeH264 = true;
|
||||
}
|
||||
|
||||
// Validate HLS requires H.264
|
||||
if ((formatType === 'hls' || formatType === 'both') && codecType === 'av1') {
|
||||
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`);
|
||||
process.exit(1);
|
||||
if (includeAv1 && !hasAv1Hardware) {
|
||||
console.error(`⚠️ AV1 requested but no hardware AV1 encoder found.`);
|
||||
console.error(` CPU-based AV1 encoding (libsvtav1) will be VERY slow.`);
|
||||
console.error(` Consider using --codec h264 for faster encoding.\n`);
|
||||
}
|
||||
|
||||
// Formats are always both
|
||||
const wantDash = true;
|
||||
const wantHls = true;
|
||||
|
||||
// Get video metadata and file size
|
||||
console.log('📊 Analyzing video...\n');
|
||||
const metadata = await getVideoMetadata(input);
|
||||
@@ -218,7 +282,7 @@ console.log(` File: ${input}`);
|
||||
console.log(` Size: ${fileSizeMB} MB`);
|
||||
console.log(` Resolution: ${metadata.width}x${metadata.height}`);
|
||||
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}`);
|
||||
if (metadata.videoBitrate) {
|
||||
console.log(` Video Bitrate: ${(metadata.videoBitrate / 1000).toFixed(2)} Mbps`);
|
||||
@@ -249,7 +313,7 @@ if (customProfiles && customProfiles.length > 0) {
|
||||
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 {
|
||||
const autoProfiles = selectProfiles(
|
||||
metadata.width,
|
||||
@@ -257,27 +321,41 @@ if (customProfiles && customProfiles.length > 0) {
|
||||
metadata.fps,
|
||||
metadata.videoBitrate
|
||||
);
|
||||
displayProfiles = autoProfiles.map(p => p.name);
|
||||
displayProfiles = autoProfiles.map(p => p.fps ? `${p.name}@${p.fps}` : p.name);
|
||||
}
|
||||
|
||||
const manifestDesc =
|
||||
formatType === 'both' ? 'DASH (manifest.mpd), HLS (master.m3u8)' :
|
||||
formatType === 'dash' ? 'DASH (manifest.mpd)' : 'HLS (master.m3u8)';
|
||||
const manifestDesc = [
|
||||
wantDash ? 'DASH (manifest.mpd)' : null,
|
||||
wantHls ? 'HLS (master.m3u8)' : null
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
const thumbnailsPlanned = true;
|
||||
const posterPlanned = posterTimecode || '00:00:00';
|
||||
const codecDisplay = codecType === 'dual' ? 'dual (AV1 + H.264)' : codecType;
|
||||
const codecNote = codecType === 'h264' && accelRest && accelRest.length >= 0 && !hasAv1Hardware ? ' (AV1 disabled: no HW)' : '';
|
||||
const codecListDisplay = [
|
||||
includeH264 ? 'h264' : null,
|
||||
includeAv1 ? 'av1' : null
|
||||
].filter(Boolean).join(', ');
|
||||
const codecNote = (!includeAv1 && codecsRequested.includes('av1')) ? ' (AV1 disabled: no HW)' : '';
|
||||
const bestAccelName = (bestAccel && bestAccel.toUpperCase()) || 'CPU';
|
||||
const bestDecoderName = (bestDecoder && bestDecoder.toUpperCase()) || 'CPU';
|
||||
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 decoderListDisplay = decoderAll.join(', ');
|
||||
|
||||
console.log('\n📦 Parameters:');
|
||||
console.log(` Input: ${input}`);
|
||||
console.log(` Output: ${outputDir}`);
|
||||
console.log(` Codec: ${codecDisplay}${codecNote}`);
|
||||
console.log(` Codec: ${codecListDisplay}${codecNote}`);
|
||||
console.log(` Profiles: ${displayProfiles.join(', ')}`);
|
||||
console.log(` Manifests: ${manifestDesc}`);
|
||||
console.log(` Poster: ${posterPlanned} (will be generated)`);
|
||||
console.log(` Thumbnails: ${thumbnailsPlanned ? 'yes (with VTT)' : 'no'}`);
|
||||
console.log(` Accelerator: ${bestAccel ? bestAccel.accelerator.toUpperCase() : 'CPU'}`);
|
||||
console.log(` Encoder: ${acceleratorDisplay} (available: ${encoderListDisplay})`);
|
||||
console.log(` Decoder: ${decoderDisplay} (available: ${decoderListDisplay})`);
|
||||
console.log(` Audio: ${muted ? 'disabled (muted)' : 'enabled'}`);
|
||||
|
||||
// Build quality settings if any are specified
|
||||
let quality: QualitySettings | undefined;
|
||||
@@ -316,18 +394,23 @@ const bars: Record<string, any> = {};
|
||||
let overallBar: any = null;
|
||||
|
||||
try {
|
||||
const startedAt = Date.now();
|
||||
const result = await convertToDash({
|
||||
input,
|
||||
outputDir,
|
||||
customProfiles,
|
||||
posterTimecode,
|
||||
codec: codecType,
|
||||
format: formatType,
|
||||
codec: [
|
||||
...(includeH264 ? ['h264'] as const : []),
|
||||
...(includeAv1 ? ['av1'] as const : [])
|
||||
],
|
||||
segmentDuration: 2,
|
||||
hardwareAccelerator: accelerator,
|
||||
hardwareDecoder: decoder,
|
||||
quality,
|
||||
generateThumbnails: true,
|
||||
generatePoster: true,
|
||||
muted,
|
||||
parallel: true,
|
||||
onProgress: (progress) => {
|
||||
const stageName = progress.stage === 'encoding' ? 'Encoding' :
|
||||
@@ -368,7 +451,9 @@ try {
|
||||
|
||||
multibar.stop();
|
||||
|
||||
console.log('\n✅ Conversion completed successfully!\n');
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const elapsedSec = (elapsedMs / 1000).toFixed(2);
|
||||
console.log(`\n✅ Conversion completed successfully! (${elapsedSec}s)\n`);
|
||||
|
||||
} catch (error) {
|
||||
multibar.stop();
|
||||
|
||||
@@ -87,7 +87,7 @@ export const DEFAULT_PROFILES: VideoProfile[] = [
|
||||
|
||||
/**
|
||||
* Select appropriate profiles based on input video resolution
|
||||
* Only creates profiles that are equal to or smaller than input resolution
|
||||
* Oriented by height: only profiles with height <= source height
|
||||
* Always generates 30 FPS profiles by default
|
||||
* For high FPS (>30), user must explicitly specify in customProfiles
|
||||
*/
|
||||
@@ -101,7 +101,7 @@ export function selectProfiles(
|
||||
|
||||
// Standard 30 FPS profiles (always created)
|
||||
const baseProfiles = DEFAULT_PROFILES.filter(profile => {
|
||||
return profile.width <= inputWidth && profile.height <= inputHeight;
|
||||
return profile.height <= inputHeight;
|
||||
});
|
||||
|
||||
// Add standard 30fps profiles with bitrate limit
|
||||
@@ -206,8 +206,8 @@ export function validateProfile(
|
||||
}
|
||||
|
||||
// Check if source supports this resolution
|
||||
if (profile.width > sourceWidth || profile.height > sourceHeight) {
|
||||
return { error: `Source resolution (${sourceWidth}x${sourceHeight}) is lower than ${profileStr} (${profile.width}x${profile.height})` };
|
||||
if (profile.height > sourceHeight) {
|
||||
return { error: `Source height (${sourceHeight}px) is lower than requested ${profileStr} height (${profile.height}px)` };
|
||||
}
|
||||
|
||||
// Check if requested FPS exceeds source FPS
|
||||
@@ -271,4 +271,3 @@ export function createProfilesFromStrings(
|
||||
|
||||
return { profiles, errors, warnings };
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
StreamingFormat,
|
||||
HardwareAccelerationOption,
|
||||
HardwareAccelerator,
|
||||
HardwareEncoderInfo
|
||||
HardwareEncoderInfo,
|
||||
HardwareDecoderInfo
|
||||
} from '../types';
|
||||
import {
|
||||
checkFFmpeg,
|
||||
@@ -19,7 +20,8 @@ import {
|
||||
getVideoMetadata,
|
||||
ensureDir,
|
||||
setLogFile,
|
||||
detectHardwareEncoders
|
||||
detectHardwareEncoders,
|
||||
detectHardwareDecoders
|
||||
} from '../utils';
|
||||
import { selectProfiles, createProfilesFromStrings } from '../config/profiles';
|
||||
import { generateThumbnailSprite, generatePoster } from './thumbnails';
|
||||
@@ -39,9 +41,9 @@ export async function convertToDash(
|
||||
segmentDuration = 2,
|
||||
profiles: userProfiles,
|
||||
customProfiles,
|
||||
codec = 'dual',
|
||||
format = 'both',
|
||||
useNvenc,
|
||||
codec = ['h264'],
|
||||
formats = ['dash', 'hls'],
|
||||
hardwareDecoder,
|
||||
hardwareAccelerator,
|
||||
quality,
|
||||
generateThumbnails = true,
|
||||
@@ -49,6 +51,7 @@ export async function convertToDash(
|
||||
generatePoster: shouldGeneratePoster = true,
|
||||
posterTimecode = '00:00:00',
|
||||
parallel = true,
|
||||
muted = false,
|
||||
onProgress
|
||||
} = options;
|
||||
|
||||
@@ -72,8 +75,8 @@ DASH Conversion Log
|
||||
Started: ${new Date().toISOString()}
|
||||
Input: ${input}
|
||||
Output: ${videoOutputDir}
|
||||
Codec: ${codec}
|
||||
Format: ${format}
|
||||
Codec: ${Array.isArray(codec) ? codec.join(',') : codec}
|
||||
Formats: ${formats?.join(',') || 'dash,hls'}
|
||||
===========================================\n`;
|
||||
await writeFile(logFile, header, 'utf-8');
|
||||
|
||||
@@ -86,15 +89,16 @@ Format: ${format}
|
||||
userProfiles,
|
||||
customProfiles,
|
||||
codec,
|
||||
format,
|
||||
useNvenc,
|
||||
formats,
|
||||
hardwareAccelerator,
|
||||
hardwareDecoder,
|
||||
quality,
|
||||
generateThumbnails,
|
||||
thumbnailConfig,
|
||||
shouldGeneratePoster,
|
||||
posterTimecode,
|
||||
parallel,
|
||||
muted,
|
||||
onProgress
|
||||
);
|
||||
} finally {
|
||||
@@ -125,16 +129,17 @@ async function convertToDashInternal(
|
||||
segmentDuration: number,
|
||||
userProfiles: VideoProfile[] | undefined,
|
||||
customProfiles: string[] | undefined,
|
||||
codec: CodecType,
|
||||
format: StreamingFormat,
|
||||
useNvenc: boolean | undefined,
|
||||
codec: CodecType | CodecType[],
|
||||
formats: StreamingFormat[] | undefined,
|
||||
hardwareAccelerator: HardwareAccelerationOption | undefined,
|
||||
hardwareDecoder: HardwareAccelerationOption | undefined,
|
||||
quality: DashConvertOptions['quality'],
|
||||
generateThumbnails: boolean,
|
||||
thumbnailConfig: ThumbnailConfig,
|
||||
generatePosterFlag: boolean,
|
||||
posterTimecode: string,
|
||||
parallel: boolean,
|
||||
muted: boolean,
|
||||
onProgress?: (progress: ConversionProgress) => void
|
||||
): Promise<DashConvertResult> {
|
||||
|
||||
@@ -158,24 +163,33 @@ async function convertToDashInternal(
|
||||
|
||||
// Get video metadata
|
||||
const metadata = await getVideoMetadata(input);
|
||||
const hasAudio = metadata.hasAudio;
|
||||
const hasAudio = !muted && metadata.hasAudio;
|
||||
const durationSeconds = metadata.duration;
|
||||
|
||||
// Подгоняем длительность сегмента под общий хронометраж, чтобы не оставался короткий хвост
|
||||
const segmentCount = Math.max(1, Math.ceil(durationSeconds / segmentDuration));
|
||||
const effectiveSegmentDuration = durationSeconds / segmentCount;
|
||||
|
||||
// Determine hardware accelerator (auto by default)
|
||||
const preferredAccelerator: HardwareAccelerationOption =
|
||||
hardwareAccelerator && hardwareAccelerator !== 'auto'
|
||||
? hardwareAccelerator
|
||||
: useNvenc === true
|
||||
? 'nvenc'
|
||||
: useNvenc === false
|
||||
? 'cpu'
|
||||
: 'auto';
|
||||
: 'auto';
|
||||
|
||||
const hardwareEncoders = await detectHardwareEncoders();
|
||||
const hardwareDecoders = await detectHardwareDecoders();
|
||||
|
||||
const av1HardwareAvailable = hardwareEncoders.some(info => info.av1Encoder);
|
||||
|
||||
const codecList = Array.isArray(codec) ? codec : [codec];
|
||||
const wantH264 = codecList.includes('h264');
|
||||
const wantAv1 = codecList.includes('av1');
|
||||
|
||||
const { selected, h264Encoder, av1Encoder, warnings: accelWarnings } = selectHardwareEncoders(
|
||||
hardwareEncoders,
|
||||
preferredAccelerator,
|
||||
codec
|
||||
wantH264,
|
||||
wantAv1
|
||||
);
|
||||
|
||||
if (accelWarnings.length > 0) {
|
||||
@@ -184,14 +198,22 @@ async function convertToDashInternal(
|
||||
}
|
||||
}
|
||||
|
||||
const av1HardwareAvailable = hardwareEncoders.some(info => info.av1Encoder);
|
||||
const { selected: selectedDecoder } = selectHardwareDecoders(
|
||||
hardwareDecoders,
|
||||
hardwareDecoder || 'auto'
|
||||
);
|
||||
|
||||
let effectiveCodec: CodecType = codec;
|
||||
if (codec === 'dual' && !av1HardwareAvailable) {
|
||||
console.warn('⚠️ AV1 hardware encoder not detected. Switching to H.264 only.');
|
||||
effectiveCodec = 'h264';
|
||||
if (wantAv1 && !av1HardwareAvailable) {
|
||||
console.warn('⚠️ AV1 hardware encoder not detected. AV1 will use CPU encoder (slow).');
|
||||
}
|
||||
|
||||
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[] = formats && formats.length > 0 ? Array.from(new Set(formats)) : ['dash', 'hls'];
|
||||
|
||||
// Select profiles
|
||||
let profiles: VideoProfile[];
|
||||
|
||||
@@ -260,14 +282,12 @@ async function convertToDashInternal(
|
||||
|
||||
// Determine which codecs to use based on codec parameter
|
||||
const codecs: Array<{ type: 'h264' | 'av1'; codec: string; preset: string }> = [];
|
||||
|
||||
if (effectiveCodec === 'h264' || effectiveCodec === 'dual') {
|
||||
if (codecsSelected.includes('h264')) {
|
||||
const h264Codec = h264Encoder || 'libx264';
|
||||
const h264Preset = resolvePresetForEncoder(h264Codec, 'h264');
|
||||
codecs.push({ type: 'h264', codec: h264Codec, preset: h264Preset });
|
||||
}
|
||||
|
||||
if (effectiveCodec === 'av1' || effectiveCodec === 'dual') {
|
||||
if (codecsSelected.includes('av1')) {
|
||||
const av1Codec = av1Encoder || 'libsvtav1';
|
||||
const av1Preset = resolvePresetForEncoder(av1Codec, 'av1');
|
||||
codecs.push({ type: 'av1', codec: av1Codec, preset: av1Preset });
|
||||
@@ -275,7 +295,7 @@ async function convertToDashInternal(
|
||||
|
||||
const codecNames = codecs.map(c => c.type.toUpperCase()).join(' + ');
|
||||
const accelLabel = selected === 'cpu' ? 'CPU' : selected.toUpperCase();
|
||||
reportProgress('analyzing', 20, `Using ${codecNames} encoding (${accelLabel})`, undefined);
|
||||
reportProgress('analyzing', 20, `Using ${codecNames} encoding (${accelLabel}, decoder ${selectedDecoder.toUpperCase()})`, undefined);
|
||||
|
||||
const maxConcurrent = selected === 'cpu' ? 2 : 3;
|
||||
|
||||
@@ -299,13 +319,15 @@ async function convertToDashInternal(
|
||||
videoCodec,
|
||||
codecPreset,
|
||||
metadata.duration,
|
||||
segmentDuration,
|
||||
effectiveSegmentDuration,
|
||||
metadata.audioBitrate,
|
||||
parallel,
|
||||
maxConcurrent,
|
||||
type, // Pass codec type to differentiate output files
|
||||
codecQuality, // Pass quality settings (CQ/CRF)
|
||||
undefined, // optimizations - for future use
|
||||
muted,
|
||||
selectedDecoder === 'cpu' ? undefined : selectedDecoder,
|
||||
(profileName, percent) => {
|
||||
const profileIndex = profiles.findIndex(p => p.name === profileName);
|
||||
const baseProgress = 25 + codecProgress * 40;
|
||||
@@ -337,9 +359,9 @@ async function convertToDashInternal(
|
||||
codecMP4Paths,
|
||||
videoOutputDir,
|
||||
profiles,
|
||||
segmentDuration,
|
||||
effectiveCodec,
|
||||
format,
|
||||
effectiveSegmentDuration,
|
||||
codecsSelected,
|
||||
formatsSelected,
|
||||
hasAudio
|
||||
);
|
||||
|
||||
@@ -412,8 +434,9 @@ async function convertToDashInternal(
|
||||
profiles,
|
||||
usedNvenc: codecs.some(c => c.codec.includes('nvenc')),
|
||||
selectedAccelerator: selected,
|
||||
codecType: effectiveCodec,
|
||||
format
|
||||
selectedDecoder,
|
||||
codecs: codecsSelected,
|
||||
formats: formatsSelected
|
||||
};
|
||||
}
|
||||
|
||||
@@ -430,18 +453,17 @@ const ACCEL_PRIORITY: Record<HardwareAccelerator, number> = {
|
||||
function selectHardwareEncoders(
|
||||
available: HardwareEncoderInfo[],
|
||||
preferred: HardwareAccelerationOption,
|
||||
codec: CodecType
|
||||
needsH264: boolean,
|
||||
needsAV1: boolean
|
||||
): {
|
||||
selected: HardwareAccelerator;
|
||||
h264Encoder?: string;
|
||||
av1Encoder?: string;
|
||||
warnings: string[];
|
||||
} {
|
||||
const needsH264 = codec === 'h264' || codec === 'dual';
|
||||
const needsAV1 = codec === 'av1' || codec === 'dual';
|
||||
const warnings: string[] = [];
|
||||
|
||||
const supportedForAuto = new Set<HardwareAccelerator>(['nvenc', 'qsv', 'amf']);
|
||||
const supportedForAuto = new Set<HardwareAccelerator>(['nvenc', 'qsv', 'amf', 'vaapi', 'videotoolbox', 'v4l2']);
|
||||
const relevant = available.filter(info =>
|
||||
(needsH264 && info.h264Encoder) || (needsAV1 && info.av1Encoder)
|
||||
);
|
||||
@@ -450,12 +472,20 @@ function selectHardwareEncoders(
|
||||
const pickByAccel = (acc: HardwareAccelerator) =>
|
||||
relevant.find(item => item.accelerator === acc);
|
||||
|
||||
// Явное указание CPU: никакого fallback на железо
|
||||
if (preferred === 'cpu') {
|
||||
return {
|
||||
selected: 'cpu',
|
||||
h264Encoder: undefined,
|
||||
av1Encoder: undefined,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
let base: HardwareEncoderInfo | undefined;
|
||||
|
||||
if (preferred !== 'auto') {
|
||||
if (preferred === 'cpu') {
|
||||
base = undefined;
|
||||
} else if (!supportedForAuto.has(preferred)) {
|
||||
if (!supportedForAuto.has(preferred)) {
|
||||
warnings.push(`Ускоритель "${preferred}" пока не поддерживается, использую CPU`);
|
||||
} else {
|
||||
base = pickByAccel(preferred);
|
||||
@@ -496,7 +526,7 @@ function selectHardwareEncoders(
|
||||
};
|
||||
}
|
||||
|
||||
if (preferred !== 'auto' && preferred !== 'cpu') {
|
||||
if (preferred !== 'auto') {
|
||||
warnings.push(
|
||||
`Ускоритель "${preferred}" не поддерживает ${codecType.toUpperCase()}, использую CPU`
|
||||
);
|
||||
@@ -518,6 +548,36 @@ function selectHardwareEncoders(
|
||||
};
|
||||
}
|
||||
|
||||
function selectHardwareDecoders(
|
||||
available: HardwareDecoderInfo[],
|
||||
preferred: HardwareAccelerationOption
|
||||
): {
|
||||
selected: HardwareAccelerator;
|
||||
} {
|
||||
const supportedForAuto = new Set<HardwareAccelerator>(['nvenc', 'qsv', 'vaapi', 'videotoolbox', 'v4l2']);
|
||||
|
||||
const pick = (acc: HardwareAccelerator) => available.find(info => info.accelerator === acc);
|
||||
|
||||
if (preferred !== 'auto') {
|
||||
if (preferred === 'cpu') {
|
||||
return { selected: 'cpu' };
|
||||
}
|
||||
const item = pick(preferred);
|
||||
return { selected: item ? item.accelerator : 'cpu' };
|
||||
}
|
||||
|
||||
const pool = available.filter(info => supportedForAuto.has(info.accelerator));
|
||||
if (pool.length === 0) {
|
||||
return { selected: 'cpu' };
|
||||
}
|
||||
|
||||
const best = pool.sort(
|
||||
(a, b) => (ACCEL_PRIORITY[b.accelerator] || 0) - (ACCEL_PRIORITY[a.accelerator] || 0)
|
||||
)[0];
|
||||
|
||||
return { selected: best.accelerator };
|
||||
}
|
||||
|
||||
function resolvePresetForEncoder(encoder: string, codecType: 'h264' | 'av1'): string {
|
||||
if (encoder.includes('nvenc')) return 'p4';
|
||||
if (encoder.includes('qsv')) return 'medium';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from 'node:path';
|
||||
import { execFFmpeg, selectAudioBitrate } from '../utils';
|
||||
import type { VideoProfile, VideoOptimizations, CodecQualitySettings } from '../types';
|
||||
import type { VideoProfile, VideoOptimizations, CodecQualitySettings, HardwareAccelerator } from '../types';
|
||||
|
||||
/**
|
||||
* Get default CQ/CRF value based on resolution and codec
|
||||
@@ -53,15 +53,30 @@ export async function encodeProfileToMP4(
|
||||
codecType: 'h264' | 'av1',
|
||||
qualitySettings?: CodecQualitySettings,
|
||||
optimizations?: VideoOptimizations,
|
||||
muted: boolean = false,
|
||||
decoderAccel?: HardwareAccelerator,
|
||||
onProgress?: (percent: number) => void
|
||||
): Promise<string> {
|
||||
const outputPath = join(tempDir, `video_${codecType}_${profile.name}.mp4`);
|
||||
|
||||
const args = [
|
||||
'-y',
|
||||
'-i', input,
|
||||
'-c:v', videoCodec
|
||||
];
|
||||
const args = ['-y'];
|
||||
|
||||
// Hardware decode (optional)
|
||||
if (decoderAccel) {
|
||||
if (decoderAccel === 'nvenc') {
|
||||
args.push('-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda');
|
||||
} else if (decoderAccel === 'qsv') {
|
||||
args.push('-hwaccel', 'qsv');
|
||||
} else if (decoderAccel === 'vaapi') {
|
||||
args.push('-hwaccel', 'vaapi');
|
||||
} else if (decoderAccel === 'videotoolbox') {
|
||||
args.push('-hwaccel', 'videotoolbox');
|
||||
} else if (decoderAccel === 'v4l2') {
|
||||
args.push('-hwaccel', 'v4l2');
|
||||
}
|
||||
}
|
||||
|
||||
args.push('-i', input, '-c:v', videoCodec);
|
||||
|
||||
// Determine if using GPU or CPU encoder
|
||||
const isGPU = videoCodec.includes('nvenc') || videoCodec.includes('qsv') || videoCodec.includes('amf') || videoCodec.includes('vaapi') || videoCodec.includes('videotoolbox') || videoCodec.includes('v4l2');
|
||||
@@ -142,7 +157,17 @@ export async function encodeProfileToMP4(
|
||||
);
|
||||
|
||||
// Build video filter chain
|
||||
const filters: string[] = [`scale=${profile.width}:${profile.height}`];
|
||||
const filters: string[] = [];
|
||||
const targetWidth = profile.width;
|
||||
const targetHeight = profile.height;
|
||||
|
||||
const useCudaScale = decoderAccel === 'nvenc';
|
||||
if (useCudaScale) {
|
||||
// CUDA path: вписываем в профиль с сохранением исходного AR
|
||||
filters.push(`scale_cuda=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease:force_divisible_by=2`);
|
||||
} else {
|
||||
filters.push(`scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease:force_divisible_by=2`);
|
||||
}
|
||||
|
||||
// Apply optimizations (for future use)
|
||||
if (optimizations) {
|
||||
@@ -157,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(','));
|
||||
|
||||
// Audio encoding
|
||||
// Select optimal bitrate based on source (don't upscale)
|
||||
const targetAudioBitrate = parseInt(profile.audioBitrate) || 256;
|
||||
const optimalAudioBitrate = selectAudioBitrate(sourceAudioBitrate, targetAudioBitrate);
|
||||
args.push('-c:a', 'aac', '-b:a', optimalAudioBitrate);
|
||||
if (!muted) {
|
||||
// Audio encoding с нормализацией таймингов и автоподпаддингом тишиной
|
||||
const targetAudioBitrate = parseInt(profile.audioBitrate) || 256;
|
||||
const optimalAudioBitrate = selectAudioBitrate(sourceAudioBitrate, targetAudioBitrate);
|
||||
args.push('-c:a', 'aac', '-b:a', optimalAudioBitrate);
|
||||
|
||||
// Audio optimizations
|
||||
if (optimizations?.audioNormalize) {
|
||||
args.push('-af', 'loudnorm');
|
||||
const targetDur = duration.toFixed(3);
|
||||
const audioFilters: string[] = [
|
||||
'aresample=async=1:min_hard_comp=0.1:first_pts=0',
|
||||
`apad=whole_dur=${targetDur}`,
|
||||
`atrim=0:${targetDur}`
|
||||
];
|
||||
if (optimizations?.audioNormalize) {
|
||||
audioFilters.push('loudnorm');
|
||||
}
|
||||
args.push('-af', audioFilters.join(','));
|
||||
} else {
|
||||
args.push('-an'); // без аудио дорожки
|
||||
}
|
||||
|
||||
// Output
|
||||
@@ -196,6 +238,8 @@ export async function encodeProfilesToMP4(
|
||||
codecType: 'h264' | 'av1',
|
||||
qualitySettings?: CodecQualitySettings,
|
||||
optimizations?: VideoOptimizations,
|
||||
muted: boolean = false,
|
||||
decoderAccel?: HardwareAccelerator,
|
||||
onProgress?: (profileName: string, percent: number) => void
|
||||
): Promise<Map<string, string>> {
|
||||
const mp4Files = new Map<string, string>();
|
||||
@@ -217,6 +261,8 @@ export async function encodeProfilesToMP4(
|
||||
codecType,
|
||||
qualitySettings,
|
||||
optimizations,
|
||||
muted,
|
||||
decoderAccel,
|
||||
(percent) => {
|
||||
if (onProgress) {
|
||||
onProgress(profile.name, percent);
|
||||
@@ -246,6 +292,8 @@ export async function encodeProfilesToMP4(
|
||||
codecType,
|
||||
qualitySettings,
|
||||
optimizations,
|
||||
muted,
|
||||
decoderAccel,
|
||||
(percent) => {
|
||||
if (onProgress) {
|
||||
onProgress(profile.name, percent);
|
||||
|
||||
@@ -45,9 +45,7 @@ export async function validateAndFixManifest(manifestPath: string): Promise<void
|
||||
* Changes: $RepresentationID$_$Number$ → $RepresentationID$/$RepresentationID$_$Number$
|
||||
*/
|
||||
export async function updateManifestPaths(
|
||||
manifestPath: string,
|
||||
profiles: VideoProfile[],
|
||||
codecType: CodecType
|
||||
manifestPath: string
|
||||
): Promise<void> {
|
||||
let mpd = await readFile(manifestPath, 'utf-8');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from 'node:path';
|
||||
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 {
|
||||
validateAndFixManifest,
|
||||
@@ -21,10 +21,11 @@ export async function packageToDash(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codecType: CodecType,
|
||||
codecs: Array<'h264' | 'av1'>,
|
||||
hasAudio: boolean
|
||||
): Promise<string> {
|
||||
const manifestPath = join(outputDir, 'manifest.mpd');
|
||||
const useCodecSuffix = codecs.length > 1;
|
||||
|
||||
// Build MP4Box command
|
||||
const args = [
|
||||
@@ -47,7 +48,7 @@ export async function packageToDash(
|
||||
}
|
||||
|
||||
// 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
|
||||
args.push(`${mp4Path}#video:id=${representationId}`);
|
||||
@@ -66,13 +67,13 @@ export async function packageToDash(
|
||||
|
||||
// MP4Box creates files in the same directory as output MPD
|
||||
// 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
|
||||
await updateManifestPaths(manifestPath, profiles, codecType);
|
||||
await updateManifestPaths(manifestPath);
|
||||
|
||||
// For dual-codec mode, separate H.264 and AV1 into different AdaptationSets
|
||||
if (codecType === 'dual') {
|
||||
if (useCodecSuffix) {
|
||||
await separateCodecAdaptationSets(manifestPath);
|
||||
}
|
||||
|
||||
@@ -89,22 +90,18 @@ export async function packageToDash(
|
||||
async function organizeSegments(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
codecType: CodecType,
|
||||
codecs: Array<'h264' | 'av1'>,
|
||||
hasAudio: boolean
|
||||
): Promise<void> {
|
||||
const { readdir, rename, mkdir } = await import('node:fs/promises');
|
||||
|
||||
// For dual-codec mode, create codec-specific subdirectories (e.g., "720p-h264/", "720p-av1/")
|
||||
// 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 useCodecSuffix = codecs.length > 1;
|
||||
|
||||
const representationIds: string[] = [];
|
||||
|
||||
for (const codec of codecs) {
|
||||
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);
|
||||
|
||||
const profileDir = join(outputDir, repId);
|
||||
@@ -158,7 +155,7 @@ export async function packageToHLS(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codecType: CodecType
|
||||
useCodecSuffix: boolean
|
||||
): Promise<string> {
|
||||
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`);
|
||||
}
|
||||
|
||||
// Representation ID for HLS (no codec suffix since we only use H.264)
|
||||
const representationId = profile.name;
|
||||
// Representation ID for HLS (добавляем суффикс, если есть несколько кодеков)
|
||||
const representationId = useCodecSuffix ? `${profile.name}-h264` : profile.name;
|
||||
|
||||
// Add video track with representation ID
|
||||
args.push(`${mp4Path}#video:id=${representationId}`);
|
||||
@@ -206,7 +203,7 @@ export async function packageToHLS(
|
||||
|
||||
// MP4Box creates files in the same directory as output manifest
|
||||
// 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
|
||||
await updateHLSManifestPaths(manifestPath, profiles);
|
||||
@@ -220,12 +217,13 @@ export async function packageToHLS(
|
||||
*/
|
||||
async function organizeSegmentsHLS(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[]
|
||||
profiles: VideoProfile[],
|
||||
useCodecSuffix: boolean
|
||||
): Promise<void> {
|
||||
const representationIds: string[] = [];
|
||||
|
||||
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);
|
||||
|
||||
const profileDir = join(outputDir, repId);
|
||||
@@ -275,27 +273,33 @@ export async function packageToFormats(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codec: CodecType,
|
||||
format: StreamingFormat,
|
||||
codecs: Array<'h264' | 'av1'>,
|
||||
formats: StreamingFormat[],
|
||||
hasAudio: boolean
|
||||
): Promise<{ manifestPath?: string; hlsManifestPath?: string }> {
|
||||
|
||||
let manifestPath: string | undefined;
|
||||
let hlsManifestPath: string | undefined;
|
||||
|
||||
// Step 1: Generate DASH segments and manifest using MP4Box
|
||||
if (format === 'dash' || format === 'both') {
|
||||
manifestPath = await packageToDash(codecMP4Files, outputDir, profiles, segmentDuration, codec, hasAudio);
|
||||
const needSegments = formats.length > 0;
|
||||
const needDash = formats.includes('dash');
|
||||
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
|
||||
if (format === 'hls' || format === 'both') {
|
||||
// HLS generation from segments
|
||||
if (needHls) {
|
||||
hlsManifestPath = await generateHLSPlaylists(
|
||||
outputDir,
|
||||
profiles,
|
||||
segmentDuration,
|
||||
codec,
|
||||
codecs.length > 1,
|
||||
hasAudio
|
||||
);
|
||||
}
|
||||
@@ -310,7 +314,7 @@ async function generateHLSPlaylists(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codecType: CodecType,
|
||||
useCodecSuffix: boolean,
|
||||
hasAudio: boolean
|
||||
): Promise<string> {
|
||||
const masterPlaylistPath = join(outputDir, 'master.m3u8');
|
||||
@@ -318,7 +322,7 @@ async function generateHLSPlaylists(
|
||||
|
||||
// Generate media playlist for each H.264 profile
|
||||
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);
|
||||
|
||||
// Read segment files from profile directory
|
||||
|
||||
@@ -11,9 +11,11 @@ export type {
|
||||
VideoMetadata,
|
||||
VideoOptimizations,
|
||||
CodecType,
|
||||
StreamingFormat,
|
||||
HardwareAccelerator,
|
||||
HardwareAccelerationOption,
|
||||
HardwareEncoderInfo
|
||||
HardwareEncoderInfo,
|
||||
HardwareDecoderInfo
|
||||
} from './types';
|
||||
|
||||
// Utility exports
|
||||
@@ -24,7 +26,10 @@ export {
|
||||
checkAV1Support,
|
||||
getVideoMetadata,
|
||||
selectAudioBitrate,
|
||||
detectHardwareEncoders
|
||||
detectHardwareEncoders,
|
||||
detectHardwareDecoders,
|
||||
testEncoder,
|
||||
testDecoder
|
||||
} from './utils';
|
||||
|
||||
// Profile exports
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Video codec type for encoding
|
||||
*/
|
||||
export type CodecType = 'av1' | 'h264' | 'dual';
|
||||
export type CodecType = 'av1' | 'h264';
|
||||
|
||||
/**
|
||||
* Streaming format type
|
||||
*/
|
||||
export type StreamingFormat = 'dash' | 'hls' | 'both';
|
||||
export type StreamingFormat = 'dash' | 'hls';
|
||||
|
||||
/**
|
||||
* Тип аппаратного ускорителя
|
||||
@@ -27,6 +27,13 @@ export interface HardwareEncoderInfo {
|
||||
av1Encoder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Набор доступных декодеров/accel
|
||||
*/
|
||||
export interface HardwareDecoderInfo {
|
||||
accelerator: HardwareAccelerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality settings for a codec
|
||||
*/
|
||||
@@ -68,17 +75,19 @@ export interface DashConvertOptions {
|
||||
/** Custom resolution profiles as strings (e.g., ['360p', '480p', '720p@60']) */
|
||||
customProfiles?: string[];
|
||||
|
||||
/** Video codec to use: 'av1', 'h264', or 'dual' for both (default: 'dual') */
|
||||
codec?: CodecType;
|
||||
/** Video codec selection: one or multiple (default: ['h264']) */
|
||||
codec?: CodecType | CodecType[];
|
||||
|
||||
/** Streaming format to generate: 'dash', 'hls', or 'both' (default: 'both') */
|
||||
format?: StreamingFormat;
|
||||
/** Streaming formats: list (default: ['dash','hls']) */
|
||||
formats?: StreamingFormat[];
|
||||
|
||||
/** Enable NVENC hardware acceleration (auto-detect if undefined) — устарело, используйте hardwareAccelerator */
|
||||
useNvenc?: boolean;
|
||||
|
||||
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
|
||||
/** Предпочитаемый аппаратный ускоритель (auto по умолчанию) */
|
||||
hardwareAccelerator?: HardwareAccelerationOption;
|
||||
/** Предпочитаемый аппаратный ускоритель для декодера (auto по умолчанию) */
|
||||
hardwareDecoder?: HardwareAccelerationOption;
|
||||
|
||||
/** Отключить аудиодорожку (muted). По умолчанию false. */
|
||||
muted?: boolean;
|
||||
|
||||
/** Quality settings for video encoding (CQ/CRF values) */
|
||||
quality?: QualitySettings;
|
||||
@@ -166,10 +175,10 @@ export interface ConversionProgress {
|
||||
* Result of DASH conversion
|
||||
*/
|
||||
export interface DashConvertResult {
|
||||
/** Path to generated DASH manifest (if format is 'dash' or 'both') */
|
||||
/** Path to generated DASH manifest (если форматы включают DASH) */
|
||||
manifestPath?: string;
|
||||
|
||||
/** Path to generated HLS manifest (if format is 'hls' or 'both') */
|
||||
/** Path to generated HLS manifest (если форматы включают HLS) */
|
||||
hlsManifestPath?: string;
|
||||
|
||||
/** Paths to generated video segments */
|
||||
@@ -195,12 +204,14 @@ export interface DashConvertResult {
|
||||
|
||||
/** Выбранный аппаратный ускоритель */
|
||||
selectedAccelerator: HardwareAccelerator;
|
||||
/** Выбранный аппаратный декодер */
|
||||
selectedDecoder: HardwareAccelerator;
|
||||
|
||||
/** Codec type used for encoding */
|
||||
codecType: CodecType;
|
||||
/** Список использованных кодеков */
|
||||
codecs: CodecType[];
|
||||
|
||||
/** Streaming format generated */
|
||||
format: StreamingFormat;
|
||||
/** Список сгенерированных форматов */
|
||||
formats: StreamingFormat[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,9 @@ export {
|
||||
checkNvenc,
|
||||
checkAV1Support,
|
||||
detectHardwareEncoders,
|
||||
detectHardwareDecoders,
|
||||
testEncoder,
|
||||
testDecoder,
|
||||
execFFmpeg,
|
||||
execMP4Box,
|
||||
setLogFile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { appendFile } from 'node:fs/promises';
|
||||
import type { HardwareAccelerator, HardwareEncoderInfo } from '../types';
|
||||
import type { HardwareAccelerator, HardwareDecoderInfo, HardwareEncoderInfo } from '../types';
|
||||
|
||||
// Global variable for log file path
|
||||
let currentLogFile: string | null = null;
|
||||
@@ -159,6 +159,91 @@ export async function detectHardwareEncoders(): Promise<HardwareEncoderInfo[]> {
|
||||
return detected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список доступных аппаратных декодеров (по выводу ffmpeg -hwaccels)
|
||||
*/
|
||||
export async function detectHardwareDecoders(): Promise<HardwareDecoderInfo[]> {
|
||||
const output: string = await new Promise((resolve) => {
|
||||
const proc = spawn('ffmpeg', ['-hide_banner', '-hwaccels']);
|
||||
let data = '';
|
||||
proc.stdout.on('data', (chunk) => data += chunk.toString());
|
||||
proc.on('error', () => resolve(''));
|
||||
proc.on('close', () => resolve(data));
|
||||
});
|
||||
|
||||
const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
const decoders: HardwareDecoderInfo[] = [];
|
||||
|
||||
const map: Record<string, HardwareAccelerator> = {
|
||||
cuda: 'nvenc',
|
||||
qsv: 'qsv',
|
||||
vaapi: 'vaapi',
|
||||
videotoolbox: 'videotoolbox',
|
||||
v4l2m2m: 'v4l2',
|
||||
dxva2: 'amf'
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const acc = map[line];
|
||||
if (acc) {
|
||||
decoders.push({ accelerator: acc });
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -90,13 +90,16 @@ export function selectAudioBitrate(
|
||||
sourceAudioBitrate: number | undefined,
|
||||
targetBitrate: number = 256
|
||||
): string {
|
||||
const MIN_AUDIO_KBPS = 64; // не опускаться ниже базового качества
|
||||
|
||||
if (!sourceAudioBitrate) {
|
||||
// If we can't detect source bitrate, use target
|
||||
return `${targetBitrate}k`;
|
||||
}
|
||||
|
||||
// Use minimum of source and target (no upscaling)
|
||||
const optimalBitrate = Math.min(sourceAudioBitrate, targetBitrate);
|
||||
// Не занижаем слишком низко: clamp к минималке, но не выше целевого
|
||||
const clampedSource = Math.max(sourceAudioBitrate, MIN_AUDIO_KBPS);
|
||||
const optimalBitrate = Math.min(clampedSource, targetBitrate);
|
||||
|
||||
// Round to common bitrate values for consistency
|
||||
if (optimalBitrate <= 64) return '64k';
|
||||
|
||||
3
web-test/dash.all.min.js
vendored
Normal file
3
web-test/dash.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
|
||||
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/polyfill/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"></script> -->
|
||||
<script src="dash.all.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/polyfill/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"></script>
|
||||
<script src="https://unpkg.com/plyr@3"></script>
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
margin: 50px auto;
|
||||
max-width: 1500px;
|
||||
}
|
||||
video {
|
||||
max-height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -24,7 +27,8 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const source = 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd';
|
||||
const source = 'http://localhost:3000/test-videotoolbox/manifest.mpd';
|
||||
// const source = 'http://localhost:3000/test-nvenc/manifest.mpd';
|
||||
const dash = dashjs.MediaPlayer().create();
|
||||
const video = document.querySelector('video');
|
||||
dash.initialize(video, source, true);
|
||||
|
||||
Reference in New Issue
Block a user