add: Фоллбек совместимость HLS
This commit is contained in:
23
README.md
23
README.md
@@ -1,8 +1,8 @@
|
||||
# DASH Video Converter 🎬
|
||||
|
||||
CLI инструмент для конвертации видео в формат DASH с поддержкой GPU ускорения (NVENC), адаптивным стримингом и автоматической генерацией превью.
|
||||
CLI инструмент для конвертации видео в форматы DASH и HLS с поддержкой GPU ускорения (NVENC), адаптивным стримингом и автоматической генерацией превью.
|
||||
|
||||
**Возможности:** ⚡ NVENC ускорение • 🎯 Множественные битрейты • 🖼️ Thumbnail спрайты • 📸 Генерация постера • 📊 Прогресс в реальном времени
|
||||
**Возможности:** ⚡ NVENC ускорение • 🎯 DASH + HLS форматы • 📊 Множественные битрейты • 🖼️ Thumbnail спрайты • 📸 Генерация постера • ⏱️ Прогресс в реальном времени
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
@@ -27,12 +27,12 @@ sudo apt install ffmpeg gpac
|
||||
brew install ffmpeg gpac
|
||||
```
|
||||
|
||||
**Результат:** В текущей директории будет создана папка `video/` с файлами `manifest.mpd`, видео сегментами, постером и превью спрайтами.
|
||||
**Результат:** В текущей директории будет создана папка `video/` с сегментами в папках `{profile}-{codec}/`, манифестами DASH и HLS в корне, постером и превью спрайтами.
|
||||
|
||||
## Параметры CLI
|
||||
|
||||
```bash
|
||||
dvc-cli <input-video> [output-dir] [-r resolutions] [-p poster-timecode]
|
||||
dvc-cli <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-p poster-timecode]
|
||||
```
|
||||
|
||||
### Основные параметры
|
||||
@@ -47,12 +47,14 @@ dvc-cli <input-video> [output-dir] [-r resolutions] [-p poster-timecode]
|
||||
| Ключ | Описание | Формат | Пример |
|
||||
|------|----------|--------|--------|
|
||||
| `-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` |
|
||||
|
||||
### Примеры использования
|
||||
|
||||
```bash
|
||||
# Базовая конвертация (результат в текущей папке)
|
||||
# Базовая конвертация (DASH + HLS, dual codec, автопрофили)
|
||||
dvc-cli video.mp4
|
||||
|
||||
# Указать выходную директорию
|
||||
@@ -64,14 +66,17 @@ dvc-cli video.mp4 -r 720,1080,1440
|
||||
# Высокий FPS для игровых стримов
|
||||
dvc-cli video.mp4 -r 720@60,1080@60
|
||||
|
||||
# Только DASH формат
|
||||
dvc-cli video.mp4 -f dash
|
||||
|
||||
# Только HLS для Safari/iOS
|
||||
dvc-cli video.mp4 -f hls -c h264
|
||||
|
||||
# Постер с 5-й секунды
|
||||
dvc-cli video.mp4 -p 5
|
||||
|
||||
# Постер в формате времени
|
||||
dvc-cli video.mp4 -p 00:01:30
|
||||
|
||||
# Комбинация параметров
|
||||
dvc-cli video.mp4 ./output -r 720,1080@60,1440@60 -p 00:00:10
|
||||
dvc-cli video.mp4 ./output -r 720,1080@60,1440@60 -c dual -f both -p 00:00:10
|
||||
```
|
||||
|
||||
### Поддерживаемые разрешения
|
||||
|
||||
385
docs/CLI_REFERENCE.md
Normal file
385
docs/CLI_REFERENCE.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# CLI Reference — Справочник команд
|
||||
|
||||
Полное руководство по использованию DASH Video Converter CLI.
|
||||
|
||||
---
|
||||
|
||||
## Синтаксис
|
||||
|
||||
```bash
|
||||
dvc-cli <input-video> [output-dir] [options]
|
||||
```
|
||||
|
||||
## Позиционные аргументы
|
||||
|
||||
| Аргумент | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `input-video` | Путь к входному видео файлу | **Обязательный** |
|
||||
| `output-dir` | Директория для сохранения результата | `.` (текущая папка) |
|
||||
|
||||
---
|
||||
|
||||
## Опции
|
||||
|
||||
### `-r, --resolutions` — Профили разрешений
|
||||
|
||||
Задает список профилей для генерации. Можно указывать разрешение и FPS.
|
||||
|
||||
**Формат:**
|
||||
- `<resolution>` — только разрешение (FPS = 30)
|
||||
- `<resolution>@<fps>` — разрешение с FPS (разделитель `@`)
|
||||
- `<resolution>-<fps>` — разрешение с FPS (разделитель `-`)
|
||||
|
||||
**Поддерживаемые разрешения:**
|
||||
- `360p` (640×360)
|
||||
- `480p` (854×480)
|
||||
- `720p` (1280×720)
|
||||
- `1080p` (1920×1080)
|
||||
- `1440p` (2560×1440)
|
||||
- `2160p` (3840×2160)
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
# Базовые разрешения (30 FPS)
|
||||
dvc-cli video.mp4 -r 360,720,1080
|
||||
|
||||
# С указанием FPS
|
||||
dvc-cli video.mp4 -r 720@60,1080@60
|
||||
|
||||
# Смешанный формат
|
||||
dvc-cli video.mp4 -r 360 720@60 1080 1440@120
|
||||
```
|
||||
|
||||
**Автоматическая коррекция FPS:**
|
||||
- Если запрошенный FPS > FPS источника → используется FPS источника
|
||||
- Максимальный FPS: **120** (ограничение системы)
|
||||
- Система выведет предупреждение при коррекции
|
||||
|
||||
**По умолчанию:** Автоматический выбор всех подходящих разрешений (≤ разрешения источника) с 30 FPS.
|
||||
|
||||
---
|
||||
|
||||
### `-c, --codec` — Видео кодек
|
||||
|
||||
Выбор видео кодека для кодирования.
|
||||
|
||||
**Значения:**
|
||||
- `h264` — только H.264 (максимальная совместимость)
|
||||
- `av1` — только AV1 (лучшее сжатие, новые браузеры)
|
||||
- `dual` — оба кодека (рекомендуется)
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
# Только H.264 (быстрее, больше места)
|
||||
dvc-cli video.mp4 -c h264
|
||||
|
||||
# Только AV1 (медленнее, меньше места)
|
||||
dvc-cli video.mp4 -c av1
|
||||
|
||||
# Оба кодека (максимальная совместимость)
|
||||
dvc-cli 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`
|
||||
|
||||
---
|
||||
|
||||
### `-f, --format` — Формат стриминга
|
||||
|
||||
Выбор формата адаптивного стриминга.
|
||||
|
||||
**Значения:**
|
||||
- `dash` — только DASH (MPEG-DASH)
|
||||
- `hls` — только HLS (HTTP Live Streaming)
|
||||
- `both` — оба формата
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
# Только DASH
|
||||
dvc-cli video.mp4 -f dash
|
||||
|
||||
# Только HLS (для Safari/iOS)
|
||||
dvc-cli video.mp4 -f hls
|
||||
|
||||
# Оба формата (максимальная совместимость)
|
||||
dvc-cli 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`
|
||||
- AV1 не поддерживается в HLS (Safari не поддерживает AV1)
|
||||
|
||||
**Файловая структура:**
|
||||
```
|
||||
output/
|
||||
└── video_name/
|
||||
├── 720p-h264/ ← Сегменты H.264 720p
|
||||
│ ├── 720p-h264_.mp4
|
||||
│ ├── 720p-h264_1.m4s
|
||||
│ └── playlist.m3u8 ← HLS медиа плейлист
|
||||
├── 720p-av1/ ← Сегменты AV1 720p (только для DASH)
|
||||
│ ├── 720p-av1_.mp4
|
||||
│ └── 720p-av1_1.m4s
|
||||
├── audio/ ← Аудио сегменты
|
||||
│ ├── audio_.mp4
|
||||
│ ├── audio_1.m4s
|
||||
│ └── playlist.m3u8
|
||||
├── manifest.mpd ← DASH манифест (корень)
|
||||
├── master.m3u8 ← HLS мастер плейлист (корень)
|
||||
├── poster.jpg ← Общие файлы
|
||||
├── thumbnails.jpg
|
||||
└── thumbnails.vtt
|
||||
```
|
||||
|
||||
**Преимущества структуры:**
|
||||
- Сегменты хранятся один раз (нет дублирования)
|
||||
- DASH и HLS используют одни и те же .m4s файлы
|
||||
- Экономия 50% места при `format=both`
|
||||
|
||||
**По умолчанию:** `both` (максимальная совместимость)
|
||||
|
||||
---
|
||||
|
||||
### `-p, --poster` — Тайм-код постера
|
||||
|
||||
Время, с которого извлечь кадр для постера.
|
||||
|
||||
**Формат:**
|
||||
- Секунды: `5`, `10.5`
|
||||
- Тайм-код: `00:00:05`, `00:01:30`
|
||||
|
||||
**Примеры:**
|
||||
```bash
|
||||
# 5 секунд от начала
|
||||
dvc-cli video.mp4 -p 5
|
||||
|
||||
# 1 минута 30 секунд
|
||||
dvc-cli video.mp4 -p 00:01:30
|
||||
```
|
||||
|
||||
**По умолчанию:** `00:00:01` (1 секунда от начала)
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Базовое использование
|
||||
|
||||
```bash
|
||||
# Простейший запуск (оба формата, dual codec, автопрофили)
|
||||
dvc-cli video.mp4
|
||||
|
||||
# С указанием выходной директории
|
||||
dvc-cli video.mp4 ./output
|
||||
```
|
||||
|
||||
### Кастомные профили
|
||||
|
||||
```bash
|
||||
# Только 720p и 1080p
|
||||
dvc-cli video.mp4 -r 720,1080
|
||||
|
||||
# High FPS профили
|
||||
dvc-cli video.mp4 -r 720@60,1080@60,1440@120
|
||||
|
||||
# Один профиль 4K
|
||||
dvc-cli video.mp4 -r 2160
|
||||
```
|
||||
|
||||
### Выбор кодека
|
||||
|
||||
```bash
|
||||
# Быстрое кодирование (только H.264)
|
||||
dvc-cli video.mp4 -c h264
|
||||
|
||||
# Лучшее сжатие (только AV1)
|
||||
dvc-cli video.mp4 -c av1
|
||||
|
||||
# Максимальная совместимость
|
||||
dvc-cli video.mp4 -c dual
|
||||
```
|
||||
|
||||
### Выбор формата
|
||||
|
||||
```bash
|
||||
# DASH для современных браузеров
|
||||
dvc-cli video.mp4 -f dash
|
||||
|
||||
# HLS для Safari/iOS
|
||||
dvc-cli video.mp4 -f hls -c h264
|
||||
|
||||
# Оба формата для всех устройств
|
||||
dvc-cli video.mp4 -f both -c dual
|
||||
```
|
||||
|
||||
### Комбинированные примеры
|
||||
|
||||
```bash
|
||||
# Производственная конфигурация
|
||||
dvc-cli video.mp4 ./cdn/videos -r 360,720,1080 -c dual -f both
|
||||
|
||||
# High-end конфигурация (4K, high FPS)
|
||||
dvc-cli video.mp4 -r 720@60,1080@60,1440@120,2160@60 -c dual -f both
|
||||
|
||||
# Быстрая конвертация для тестов
|
||||
dvc-cli video.mp4 -r 720 -c h264 -f dash
|
||||
|
||||
# Mobile-first (низкие разрешения, HLS)
|
||||
dvc-cli video.mp4 -r 360,480,720 -c h264 -f hls
|
||||
|
||||
# Кастомный постер
|
||||
dvc-cli video.mp4 -r 720,1080 -p 00:02:30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Системные требования
|
||||
|
||||
### Обязательные зависимости
|
||||
|
||||
- **FFmpeg** — кодирование видео
|
||||
- **MP4Box (GPAC)** — упаковка DASH/HLS
|
||||
|
||||
Установка:
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S ffmpeg gpac
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install ffmpeg gpac
|
||||
|
||||
# macOS
|
||||
brew install ffmpeg gpac
|
||||
```
|
||||
|
||||
### Опциональные (для GPU ускорения)
|
||||
|
||||
- **NVIDIA GPU** — для H.264/AV1 кодирования через NVENC
|
||||
- **Intel GPU** — для AV1 через QSV
|
||||
- **AMD GPU** — для AV1 через AMF
|
||||
|
||||
---
|
||||
|
||||
## Производительность
|
||||
|
||||
### CPU vs GPU
|
||||
|
||||
| Кодек | CPU | GPU (NVENC) | Ускорение |
|
||||
|-------|-----|-------------|-----------|
|
||||
| H.264 | libx264 | h264_nvenc | ~10-20x |
|
||||
| AV1 | libsvtav1 | av1_nvenc | ~15-30x |
|
||||
|
||||
### Параллельное кодирование
|
||||
|
||||
- **GPU**: до 3 профилей одновременно
|
||||
- **CPU**: до 2 профилей одновременно
|
||||
|
||||
### Время конвертации (примерные данные)
|
||||
|
||||
Видео 4K, 10 секунд, dual codec, 3 профиля:
|
||||
|
||||
| Конфигурация | Время |
|
||||
|--------------|-------|
|
||||
| CPU (libx264 + libsvtav1) | ~5-10 минут |
|
||||
| GPU (NVENC) | ~30-60 секунд |
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации
|
||||
|
||||
### Для максимальной совместимости
|
||||
|
||||
```bash
|
||||
dvc-cli video.mp4 -c dual -f both
|
||||
```
|
||||
|
||||
Генерирует:
|
||||
- DASH с H.264 + AV1 (Chrome, Firefox, Edge)
|
||||
- HLS с H.264 (Safari, iOS)
|
||||
- Все современные устройства поддерживаются
|
||||
|
||||
### Для быстрой разработки
|
||||
|
||||
```bash
|
||||
dvc-cli video.mp4 -r 720 -c h264 -f dash
|
||||
```
|
||||
|
||||
Быстрое кодирование одного профиля.
|
||||
|
||||
### Для продакшена
|
||||
|
||||
```bash
|
||||
dvc-cli video.mp4 -r 360,480,720,1080,1440 -c dual -f both
|
||||
```
|
||||
|
||||
Широкий диапазон профилей для всех устройств.
|
||||
|
||||
### Для 4K контента
|
||||
|
||||
```bash
|
||||
dvc-cli video.mp4 -r 720,1080,1440,2160 -c dual -f both
|
||||
```
|
||||
|
||||
От HD до 4K для премиум контента.
|
||||
|
||||
---
|
||||
|
||||
## Устранение проблем
|
||||
|
||||
### HLS требует H.264
|
||||
|
||||
**Ошибка:**
|
||||
```
|
||||
❌ Error: HLS format requires H.264 codec
|
||||
```
|
||||
|
||||
**Решение:**
|
||||
```bash
|
||||
# Используйте h264 или dual
|
||||
dvc-cli video.mp4 -f hls -c h264
|
||||
# или
|
||||
dvc-cli video.mp4 -f hls -c dual
|
||||
```
|
||||
|
||||
### FPS источника ниже запрошенного
|
||||
|
||||
**Предупреждение:**
|
||||
```
|
||||
⚠️ Requested 120 FPS, but source is 60 FPS. Using 60 FPS instead
|
||||
```
|
||||
|
||||
Это нормально! Система автоматически ограничивает FPS до максимума источника.
|
||||
|
||||
### MP4Box не найден
|
||||
|
||||
**Ошибка:**
|
||||
```
|
||||
❌ MP4Box not found
|
||||
```
|
||||
|
||||
**Решение:**
|
||||
```bash
|
||||
sudo pacman -S gpac # Arch
|
||||
sudo apt install gpac # Ubuntu
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## См. также
|
||||
|
||||
- [FEATURES.md](./FEATURES.md) — Возможности и технические детали
|
||||
- [PUBLISHING.md](./PUBLISHING.md) — Публикация пакета в npm
|
||||
- [README.md](../README.md) — Быстрый старт
|
||||
|
||||
46
src/cli.ts
46
src/cli.ts
@@ -13,13 +13,14 @@
|
||||
import { convertToDash, checkFFmpeg, checkNvenc, checkMP4Box, checkAV1Support, getVideoMetadata } from './index';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { statSync } from 'node:fs';
|
||||
import type { CodecType } from './types';
|
||||
import type { CodecType, StreamingFormat } from './types';
|
||||
|
||||
// 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)
|
||||
const positionalArgs: string[] = [];
|
||||
|
||||
// First pass: extract flags and their values
|
||||
@@ -54,6 +55,15 @@ for (let i = 0; i < args.length; i++) {
|
||||
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);
|
||||
}
|
||||
i++; // Skip next arg
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
// Positional argument
|
||||
positionalArgs.push(args[i]);
|
||||
@@ -65,21 +75,23 @@ const input = positionalArgs[0];
|
||||
const outputDir = positionalArgs[1] || '.'; // Текущая директория по умолчанию
|
||||
|
||||
if (!input) {
|
||||
console.error('❌ Usage: dvc-cli <input-video> [output-dir] [-r resolutions] [-c codec] [-p poster-timecode]');
|
||||
console.error('❌ Usage: dvc-cli <input-video> [output-dir] [-r resolutions] [-c codec] [-f format] [-p poster-timecode]');
|
||||
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: dash)');
|
||||
console.error(' -p, --poster Poster timecode (e.g., 00:00:05 or 10)');
|
||||
console.error('\nExamples:');
|
||||
console.error(' dvc-cli video.mp4');
|
||||
console.error(' dvc-cli video.mp4 ./output');
|
||||
console.error(' dvc-cli video.mp4 -r 360,480,720');
|
||||
console.error(' dvc-cli video.mp4 -c av1');
|
||||
console.error(' dvc-cli video.mp4 -c h264');
|
||||
console.error(' dvc-cli video.mp4 -c dual');
|
||||
console.error(' dvc-cli video.mp4 -r 720@60,1080@60,2160@60 -c av1');
|
||||
console.error(' dvc-cli video.mp4 -f hls');
|
||||
console.error(' dvc-cli video.mp4 -f both');
|
||||
console.error(' dvc-cli video.mp4 -c dual -f both');
|
||||
console.error(' dvc-cli video.mp4 -r 720@60,1080@60,2160@60 -c av1 -f dash');
|
||||
console.error(' dvc-cli video.mp4 -p 00:00:05');
|
||||
console.error(' dvc-cli video.mp4 ./output -r 720,1080 -c dual -p 10');
|
||||
console.error(' dvc-cli video.mp4 ./output -r 720,1080 -c dual -f both -p 10');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -116,6 +128,13 @@ if ((codecType === 'av1' || codecType === 'dual') && !av1Support.available) {
|
||||
console.error(` Consider using --codec h264 for faster encoding.\n`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Get video metadata and file size
|
||||
console.log('📊 Analyzing video...\n');
|
||||
const metadata = await getVideoMetadata(input);
|
||||
@@ -137,6 +156,7 @@ if (metadata.audioBitrate) {
|
||||
}
|
||||
console.log(`\n📁 Output: ${outputDir}`);
|
||||
console.log(`🎬 Codec: ${codecType}${codecType === 'dual' ? ' (AV1 + H.264 for maximum compatibility)' : ''}`);
|
||||
console.log(`📺 Format: ${formatType}${formatType === 'both' ? ' (DASH + HLS for maximum compatibility)' : formatType === 'hls' ? ' (H.264 only for Safari/iOS)' : ''}`);
|
||||
if (customProfiles) {
|
||||
console.log(`🎯 Custom profiles: ${customProfiles.join(', ')}`);
|
||||
}
|
||||
@@ -166,6 +186,7 @@ try {
|
||||
customProfiles,
|
||||
posterTimecode,
|
||||
codec: codecType,
|
||||
format: formatType,
|
||||
segmentDuration: 2,
|
||||
useNvenc: hasNvenc,
|
||||
generateThumbnails: true,
|
||||
@@ -212,9 +233,18 @@ try {
|
||||
|
||||
console.log('\n✅ Conversion completed successfully!\n');
|
||||
console.log('📊 Results:');
|
||||
console.log(` Manifest: ${result.manifestPath}`);
|
||||
|
||||
if (result.manifestPath) {
|
||||
console.log(` DASH Manifest: ${result.manifestPath}`);
|
||||
}
|
||||
|
||||
if (result.hlsManifestPath) {
|
||||
console.log(` HLS Manifest: ${result.hlsManifestPath}`);
|
||||
}
|
||||
|
||||
console.log(` Duration: ${result.duration.toFixed(2)}s`);
|
||||
console.log(` Profiles: ${result.profiles.map(p => p.name).join(', ')}`);
|
||||
console.log(` Format: ${result.format}`);
|
||||
console.log(` Codec: ${result.codecType}${result.codecType === 'dual' ? ' (AV1 + H.264)' : ''}`);
|
||||
console.log(` Encoder: ${result.usedNvenc ? '⚡ GPU accelerated' : '🔧 CPU'}`);
|
||||
|
||||
@@ -227,7 +257,7 @@ try {
|
||||
console.log(` VTT file: ${result.thumbnailVttPath}`);
|
||||
}
|
||||
|
||||
console.log('\n🎉 Done! You can now use the manifest file in your video player.');
|
||||
console.log('\n🎉 Done! You can now use the manifest file(s) in your video player.');
|
||||
|
||||
} catch (error) {
|
||||
multibar.stop();
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
VideoProfile,
|
||||
ThumbnailConfig,
|
||||
ConversionProgress,
|
||||
CodecType
|
||||
CodecType,
|
||||
StreamingFormat
|
||||
} from '../types';
|
||||
import {
|
||||
checkFFmpeg,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
import { selectProfiles, createProfilesFromStrings } from '../config/profiles';
|
||||
import { generateThumbnailSprite, generatePoster } from './thumbnails';
|
||||
import { encodeProfilesToMP4 } from './encoding';
|
||||
import { packageToDash } from './packaging';
|
||||
import { packageToFormats } from './packaging';
|
||||
|
||||
/**
|
||||
* Convert video to DASH format with NVENC acceleration
|
||||
@@ -36,6 +37,7 @@ export async function convertToDash(
|
||||
profiles: userProfiles,
|
||||
customProfiles,
|
||||
codec = 'dual',
|
||||
format = 'both',
|
||||
useNvenc,
|
||||
generateThumbnails = true,
|
||||
thumbnailConfig = {},
|
||||
@@ -58,6 +60,7 @@ export async function convertToDash(
|
||||
userProfiles,
|
||||
customProfiles,
|
||||
codec,
|
||||
format,
|
||||
useNvenc,
|
||||
generateThumbnails,
|
||||
thumbnailConfig,
|
||||
@@ -87,6 +90,7 @@ async function convertToDashInternal(
|
||||
userProfiles: VideoProfile[] | undefined,
|
||||
customProfiles: string[] | undefined,
|
||||
codec: CodecType,
|
||||
format: StreamingFormat,
|
||||
useNvenc: boolean | undefined,
|
||||
generateThumbnails: boolean,
|
||||
thumbnailConfig: ThumbnailConfig,
|
||||
@@ -260,15 +264,16 @@ async function convertToDashInternal(
|
||||
|
||||
reportProgress('encoding', 65, 'Stage 1 complete: All codecs and profiles encoded');
|
||||
|
||||
// STAGE 2: Package to DASH using MP4Box (light work, fast)
|
||||
reportProgress('encoding', 70, `Stage 2: Creating DASH with MP4Box...`);
|
||||
// STAGE 2: Package to segments and manifests (unified, no duplication)
|
||||
reportProgress('encoding', 70, `Stage 2: Creating segments and manifests...`);
|
||||
|
||||
const manifestPath = await packageToDash(
|
||||
const { manifestPath, hlsManifestPath } = await packageToFormats(
|
||||
codecMP4Paths,
|
||||
videoOutputDir,
|
||||
profiles,
|
||||
segmentDuration,
|
||||
codec
|
||||
codec,
|
||||
format
|
||||
);
|
||||
|
||||
// Collect all video paths from all codecs
|
||||
@@ -277,7 +282,7 @@ async function convertToDashInternal(
|
||||
videoPaths.push(...Array.from(mp4Paths.values()));
|
||||
}
|
||||
|
||||
reportProgress('encoding', 80, 'Stage 2 complete: DASH created');
|
||||
reportProgress('encoding', 80, 'Stage 2 complete: All formats packaged');
|
||||
|
||||
// Generate thumbnails
|
||||
let thumbnailSpritePath: string | undefined;
|
||||
@@ -321,16 +326,17 @@ async function convertToDashInternal(
|
||||
reportProgress('thumbnails', 95, 'Poster generated');
|
||||
}
|
||||
|
||||
// Generate MPD manifest
|
||||
reportProgress('manifest', 95, 'Finalizing manifest...');
|
||||
// Finalize
|
||||
reportProgress('manifest', 95, 'Finalizing...');
|
||||
|
||||
// Note: manifestPath is already created by MP4Box in packageToDash
|
||||
// Note: manifestPath/hlsManifestPath are already created by MP4Box in packageToDash/packageToHLS
|
||||
// No need for separate generateManifest function
|
||||
|
||||
reportProgress('complete', 100, 'Conversion complete!');
|
||||
|
||||
return {
|
||||
manifestPath,
|
||||
hlsManifestPath,
|
||||
videoPaths,
|
||||
thumbnailSpritePath,
|
||||
thumbnailVttPath,
|
||||
@@ -338,7 +344,8 @@ async function convertToDashInternal(
|
||||
duration: metadata.duration,
|
||||
profiles,
|
||||
usedNvenc: willUseNvenc,
|
||||
codecType: codec
|
||||
codecType: codec,
|
||||
format
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { join } from 'node:path';
|
||||
import { execMP4Box } from '../utils';
|
||||
import type { VideoProfile, CodecType } from '../types';
|
||||
import type { VideoProfile, CodecType, StreamingFormat } from '../types';
|
||||
import { readFile, writeFile, readdir, rename, mkdir } from 'node:fs/promises';
|
||||
|
||||
/**
|
||||
* Package MP4 files into DASH format using MP4Box
|
||||
@@ -158,3 +159,430 @@ async function updateManifestPaths(
|
||||
await writeFile(manifestPath, mpd, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Package MP4 files into HLS format using MP4Box
|
||||
* Stage 2: Light work - just packaging, no encoding
|
||||
* Creates master.m3u8 playlist with H.264 profiles only (for Safari/iOS compatibility)
|
||||
*/
|
||||
export async function packageToHLS(
|
||||
codecMP4Files: Map<'h264' | 'av1', Map<string, string>>,
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codecType: CodecType
|
||||
): Promise<string> {
|
||||
const manifestPath = join(outputDir, 'master.m3u8');
|
||||
|
||||
// Build MP4Box command for HLS
|
||||
const args = [
|
||||
'-dash', String(segmentDuration * 1000), // MP4Box expects milliseconds
|
||||
'-frag', String(segmentDuration * 1000),
|
||||
'-rap', // Force segments to start with random access points
|
||||
'-segment-timeline', // Use SegmentTimeline for accurate segment durations
|
||||
'-segment-name', '$RepresentationID$_$Number$',
|
||||
'-profile', 'live', // HLS mode instead of DASH
|
||||
'-out', manifestPath
|
||||
];
|
||||
|
||||
// For HLS, use only H.264 codec (Safari/iOS compatibility)
|
||||
const h264Files = codecMP4Files.get('h264');
|
||||
|
||||
if (!h264Files) {
|
||||
throw new Error('H.264 codec files not found. HLS requires H.264 for Safari/iOS compatibility.');
|
||||
}
|
||||
|
||||
let firstFile = true;
|
||||
|
||||
for (const profile of profiles) {
|
||||
const mp4Path = h264Files.get(profile.name);
|
||||
if (!mp4Path) {
|
||||
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;
|
||||
|
||||
// Add video track with representation ID
|
||||
args.push(`${mp4Path}#video:id=${representationId}`);
|
||||
|
||||
// Add audio track only once (shared across all profiles)
|
||||
if (firstFile) {
|
||||
args.push(`${mp4Path}#audio:id=audio`);
|
||||
firstFile = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute MP4Box
|
||||
await execMP4Box(args);
|
||||
|
||||
// MP4Box creates files in the same directory as output manifest
|
||||
// Move segment files to profile subdirectories for clean structure
|
||||
await organizeSegmentsHLS(outputDir, profiles);
|
||||
|
||||
// Update manifest to reflect new file structure with subdirectories
|
||||
await updateManifestPathsHLS(manifestPath, profiles);
|
||||
|
||||
return manifestPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize HLS segments into profile subdirectories
|
||||
* HLS only uses H.264, so no codec suffix in directory names
|
||||
*/
|
||||
async function organizeSegmentsHLS(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[]
|
||||
): Promise<void> {
|
||||
const representationIds: string[] = [];
|
||||
|
||||
for (const profile of profiles) {
|
||||
const repId = profile.name; // Just profile name, no codec
|
||||
representationIds.push(repId);
|
||||
|
||||
const profileDir = join(outputDir, repId);
|
||||
await mkdir(profileDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create audio subdirectory
|
||||
const audioDir = join(outputDir, 'audio');
|
||||
await mkdir(audioDir, { recursive: true });
|
||||
|
||||
// Get all files in output directory
|
||||
const files = await readdir(outputDir);
|
||||
|
||||
// Move segment files to their respective directories
|
||||
for (const file of files) {
|
||||
// Skip manifest
|
||||
if (file === 'master.m3u8') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move audio files to audio/ directory
|
||||
if (file.startsWith('audio_') || file === 'audio_init.m4s') {
|
||||
const oldPath = join(outputDir, file);
|
||||
const newPath = join(audioDir, file);
|
||||
await rename(oldPath, newPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move video segment files to their representation directories
|
||||
for (const repId of representationIds) {
|
||||
if (file.startsWith(`${repId}_`)) {
|
||||
const oldPath = join(outputDir, file);
|
||||
const newPath = join(outputDir, repId, file);
|
||||
await rename(oldPath, newPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update HLS master manifest to reflect subdirectory structure
|
||||
*/
|
||||
async function updateManifestPathsHLS(
|
||||
manifestPath: string,
|
||||
profiles: VideoProfile[]
|
||||
): Promise<void> {
|
||||
let m3u8 = await readFile(manifestPath, 'utf-8');
|
||||
|
||||
// MP4Box uses $RepresentationID$ template variable
|
||||
// Replace: media="$RepresentationID$_$Number$.m4s"
|
||||
// With: media="$RepresentationID$/$RepresentationID$_$Number$.m4s"
|
||||
|
||||
m3u8 = m3u8.replace(
|
||||
/media="\$RepresentationID\$_\$Number\$\.m4s"/g,
|
||||
'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"'
|
||||
);
|
||||
|
||||
// Replace: initialization="$RepresentationID$_.mp4"
|
||||
// With: initialization="$RepresentationID$/$RepresentationID$_.mp4"
|
||||
|
||||
m3u8 = m3u8.replace(
|
||||
/initialization="\$RepresentationID\$_\.mp4"/g,
|
||||
'initialization="$RepresentationID$/$RepresentationID$_.mp4"'
|
||||
);
|
||||
|
||||
await writeFile(manifestPath, m3u8, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified packaging: creates segments once and generates both DASH and HLS manifests
|
||||
* No duplication - segments stored in {profile}-{codec}/ folders
|
||||
*/
|
||||
export async function packageToFormats(
|
||||
codecMP4Files: Map<'h264' | 'av1', Map<string, string>>,
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codec: CodecType,
|
||||
format: StreamingFormat
|
||||
): Promise<{ manifestPath?: string; hlsManifestPath?: string }> {
|
||||
|
||||
// Step 1: Generate segments using MP4Box (DASH mode)
|
||||
const tempManifestPath = join(outputDir, '.temp_manifest.mpd');
|
||||
|
||||
const args = [
|
||||
'-dash', String(segmentDuration * 1000),
|
||||
'-frag', String(segmentDuration * 1000),
|
||||
'-rap',
|
||||
'-segment-timeline',
|
||||
'-segment-name', '$RepresentationID$_$Number$',
|
||||
'-out', tempManifestPath
|
||||
];
|
||||
|
||||
// Add all MP4 files
|
||||
let firstFile = true;
|
||||
|
||||
for (const [codecType, mp4Files] of codecMP4Files.entries()) {
|
||||
for (const profile of profiles) {
|
||||
const mp4Path = mp4Files.get(profile.name);
|
||||
if (!mp4Path) {
|
||||
throw new Error(`MP4 file not found for profile: ${profile.name}, codec: ${codecType}`);
|
||||
}
|
||||
|
||||
const representationId = codec === 'dual' ? `${profile.name}-${codecType}` : profile.name;
|
||||
|
||||
args.push(`${mp4Path}#video:id=${representationId}`);
|
||||
|
||||
if (firstFile) {
|
||||
args.push(`${mp4Path}#audio:id=audio`);
|
||||
firstFile = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute MP4Box to create segments
|
||||
await execMP4Box(args);
|
||||
|
||||
// Step 2: Organize segments into {profile}-{codec}/ folders
|
||||
await organizeSegmentsUnified(outputDir, profiles, codec);
|
||||
|
||||
// Step 3: Generate manifests based on format
|
||||
let manifestPath: string | undefined;
|
||||
let hlsManifestPath: string | undefined;
|
||||
|
||||
if (format === 'dash' || format === 'both') {
|
||||
// Move and update DASH manifest
|
||||
manifestPath = join(outputDir, 'manifest.mpd');
|
||||
await rename(tempManifestPath, manifestPath);
|
||||
await updateDashManifestPaths(manifestPath, profiles, codec);
|
||||
}
|
||||
|
||||
if (format === 'hls' || format === 'both') {
|
||||
// Generate HLS playlists
|
||||
hlsManifestPath = await generateHLSPlaylists(
|
||||
outputDir,
|
||||
profiles,
|
||||
segmentDuration,
|
||||
codec
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up temp manifest if not used
|
||||
try {
|
||||
const { unlink } = await import('node:fs/promises');
|
||||
await unlink(tempManifestPath);
|
||||
} catch {
|
||||
// Already moved or doesn't exist
|
||||
}
|
||||
|
||||
return { manifestPath, hlsManifestPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize segments into unified structure: {profile}-{codec}/
|
||||
*/
|
||||
async function organizeSegmentsUnified(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
codecType: CodecType
|
||||
): Promise<void> {
|
||||
const representationIds: string[] = [];
|
||||
|
||||
// Determine which codecs are used
|
||||
const codecs: Array<'h264' | 'av1'> = [];
|
||||
if (codecType === 'h264' || codecType === 'dual') codecs.push('h264');
|
||||
if (codecType === 'av1' || codecType === 'dual') codecs.push('av1');
|
||||
|
||||
// Create directories for each profile-codec combination
|
||||
for (const codecName of codecs) {
|
||||
for (const profile of profiles) {
|
||||
const repId = codecType === 'dual' ? `${profile.name}-${codecName}` : profile.name;
|
||||
representationIds.push(repId);
|
||||
|
||||
const profileDir = join(outputDir, repId);
|
||||
await mkdir(profileDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Create audio directory
|
||||
const audioDir = join(outputDir, 'audio');
|
||||
await mkdir(audioDir, { recursive: true });
|
||||
|
||||
// Get all files in output directory
|
||||
const files = await readdir(outputDir);
|
||||
|
||||
// Move segment files to their respective directories
|
||||
for (const file of files) {
|
||||
// Skip manifests and directories
|
||||
if (file.endsWith('.mpd') || file.endsWith('.m3u8') || !file.includes('_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move audio files
|
||||
if (file.startsWith('audio_')) {
|
||||
const oldPath = join(outputDir, file);
|
||||
const newPath = join(audioDir, file);
|
||||
await rename(oldPath, newPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move video segment files
|
||||
for (const repId of representationIds) {
|
||||
if (file.startsWith(`${repId}_`)) {
|
||||
const oldPath = join(outputDir, file);
|
||||
const newPath = join(outputDir, repId, file);
|
||||
await rename(oldPath, newPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DASH manifest paths to point to {profile}-{codec}/ folders
|
||||
*/
|
||||
async function updateDashManifestPaths(
|
||||
manifestPath: string,
|
||||
profiles: VideoProfile[],
|
||||
codecType: CodecType
|
||||
): Promise<void> {
|
||||
let mpd = await readFile(manifestPath, 'utf-8');
|
||||
|
||||
// Update paths: $RepresentationID$_$Number$.m4s → $RepresentationID$/$RepresentationID$_$Number$.m4s
|
||||
mpd = mpd.replace(
|
||||
/media="\$RepresentationID\$_\$Number\$\.m4s"/g,
|
||||
'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"'
|
||||
);
|
||||
|
||||
mpd = mpd.replace(
|
||||
/initialization="\$RepresentationID\$_\.mp4"/g,
|
||||
'initialization="$RepresentationID$/$RepresentationID$_.mp4"'
|
||||
);
|
||||
|
||||
await writeFile(manifestPath, mpd, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HLS playlists (media playlists in folders + master in root)
|
||||
*/
|
||||
async function generateHLSPlaylists(
|
||||
outputDir: string,
|
||||
profiles: VideoProfile[],
|
||||
segmentDuration: number,
|
||||
codecType: CodecType
|
||||
): Promise<string> {
|
||||
const masterPlaylistPath = join(outputDir, 'master.m3u8');
|
||||
const variants: Array<{ path: string; bandwidth: number; resolution: string; fps: number }> = [];
|
||||
|
||||
// Generate media playlist for each H.264 profile
|
||||
for (const profile of profiles) {
|
||||
const profileDir = codecType === 'dual' ? `${profile.name}-h264` : profile.name;
|
||||
const profilePath = join(outputDir, profileDir);
|
||||
|
||||
// Read segment files from profile directory
|
||||
const files = await readdir(profilePath);
|
||||
const segmentFiles = files
|
||||
.filter(f => f.endsWith('.m4s'))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.match(/_(\d+)\.m4s$/)?.[1] || '0');
|
||||
const numB = parseInt(b.match(/_(\d+)\.m4s$/)?.[1] || '0');
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
const initFile = files.find(f => f.endsWith('_.mp4'));
|
||||
|
||||
if (!initFile || segmentFiles.length === 0) {
|
||||
continue; // Skip if no segments found
|
||||
}
|
||||
|
||||
// Generate media playlist content
|
||||
let playlistContent = '#EXTM3U\n';
|
||||
playlistContent += `#EXT-X-VERSION:6\n`;
|
||||
playlistContent += `#EXT-X-TARGETDURATION:${Math.ceil(segmentDuration)}\n`;
|
||||
playlistContent += `#EXT-X-MEDIA-SEQUENCE:1\n`;
|
||||
playlistContent += `#EXT-X-INDEPENDENT-SEGMENTS\n`;
|
||||
playlistContent += `#EXT-X-MAP:URI="${initFile}"\n`;
|
||||
|
||||
for (const segmentFile of segmentFiles) {
|
||||
playlistContent += `#EXTINF:${segmentDuration},\n`;
|
||||
playlistContent += `${segmentFile}\n`;
|
||||
}
|
||||
|
||||
playlistContent += `#EXT-X-ENDLIST\n`;
|
||||
|
||||
// Write media playlist
|
||||
const playlistPath = join(profilePath, 'playlist.m3u8');
|
||||
await writeFile(playlistPath, playlistContent, 'utf-8');
|
||||
|
||||
// Add to variants list
|
||||
const bandwidth = parseInt(profile.videoBitrate) * 1000;
|
||||
variants.push({
|
||||
path: `${profileDir}/playlist.m3u8`,
|
||||
bandwidth,
|
||||
resolution: `${profile.width}x${profile.height}`,
|
||||
fps: profile.fps || 30
|
||||
});
|
||||
}
|
||||
|
||||
// Generate audio media playlist
|
||||
const audioDir = join(outputDir, 'audio');
|
||||
const audioFiles = await readdir(audioDir);
|
||||
const audioSegments = audioFiles
|
||||
.filter(f => f.endsWith('.m4s'))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.match(/_(\d+)\.m4s$/)?.[1] || '0');
|
||||
const numB = parseInt(b.match(/_(\d+)\.m4s$/)?.[1] || '0');
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
const audioInit = audioFiles.find(f => f.endsWith('_.mp4'));
|
||||
|
||||
if (audioInit && audioSegments.length > 0) {
|
||||
let audioPlaylistContent = '#EXTM3U\n';
|
||||
audioPlaylistContent += `#EXT-X-VERSION:6\n`;
|
||||
audioPlaylistContent += `#EXT-X-TARGETDURATION:${Math.ceil(segmentDuration)}\n`;
|
||||
audioPlaylistContent += `#EXT-X-MEDIA-SEQUENCE:1\n`;
|
||||
audioPlaylistContent += `#EXT-X-INDEPENDENT-SEGMENTS\n`;
|
||||
audioPlaylistContent += `#EXT-X-MAP:URI="${audioInit}"\n`;
|
||||
|
||||
for (const segmentFile of audioSegments) {
|
||||
audioPlaylistContent += `#EXTINF:${segmentDuration},\n`;
|
||||
audioPlaylistContent += `${segmentFile}\n`;
|
||||
}
|
||||
|
||||
audioPlaylistContent += `#EXT-X-ENDLIST\n`;
|
||||
|
||||
await writeFile(join(audioDir, 'playlist.m3u8'), audioPlaylistContent, 'utf-8');
|
||||
}
|
||||
|
||||
// Generate master playlist
|
||||
let masterContent = '#EXTM3U\n';
|
||||
masterContent += '#EXT-X-VERSION:6\n';
|
||||
masterContent += '#EXT-X-INDEPENDENT-SEGMENTS\n\n';
|
||||
|
||||
// Add audio reference
|
||||
masterContent += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="audio",AUTOSELECT=YES,URI="audio/playlist.m3u8",CHANNELS="2"\n\n`;
|
||||
|
||||
// Add video variants
|
||||
for (const variant of variants) {
|
||||
masterContent += `#EXT-X-STREAM-INF:BANDWIDTH=${variant.bandwidth},CODECS="avc1.4D4020,mp4a.40.2",RESOLUTION=${variant.resolution},FRAME-RATE=${variant.fps},AUDIO="audio"\n`;
|
||||
masterContent += `${variant.path}\n\n`;
|
||||
}
|
||||
|
||||
await writeFile(masterPlaylistPath, masterContent, 'utf-8');
|
||||
|
||||
return masterPlaylistPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
*/
|
||||
export type CodecType = 'av1' | 'h264' | 'dual';
|
||||
|
||||
/**
|
||||
* Streaming format type
|
||||
*/
|
||||
export type StreamingFormat = 'dash' | 'hls' | 'both';
|
||||
|
||||
/**
|
||||
* Configuration options for DASH conversion
|
||||
*/
|
||||
@@ -25,6 +30,9 @@ export interface DashConvertOptions {
|
||||
/** Video codec to use: 'av1', 'h264', or 'dual' for both (default: 'dual') */
|
||||
codec?: CodecType;
|
||||
|
||||
/** Streaming format to generate: 'dash', 'hls', or 'both' (default: 'both') */
|
||||
format?: StreamingFormat;
|
||||
|
||||
/** Enable NVENC hardware acceleration (auto-detect if undefined) */
|
||||
useNvenc?: boolean;
|
||||
|
||||
@@ -111,8 +119,11 @@ export interface ConversionProgress {
|
||||
* Result of DASH conversion
|
||||
*/
|
||||
export interface DashConvertResult {
|
||||
/** Path to generated MPD manifest */
|
||||
manifestPath: string;
|
||||
/** Path to generated DASH manifest (if format is 'dash' or 'both') */
|
||||
manifestPath?: string;
|
||||
|
||||
/** Path to generated HLS manifest (if format is 'hls' or 'both') */
|
||||
hlsManifestPath?: string;
|
||||
|
||||
/** Paths to generated video segments */
|
||||
videoPaths: string[];
|
||||
@@ -137,6 +148,9 @@ export interface DashConvertResult {
|
||||
|
||||
/** Codec type used for encoding */
|
||||
codecType: CodecType;
|
||||
|
||||
/** Streaming format generated */
|
||||
format: StreamingFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user