Compare commits
9
Commits
c68b887b60
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fda950e5 | ||
|
|
5b4b36c928 | ||
|
|
f3f88edc0f | ||
|
|
1eda5b676f | ||
|
|
702a92ccd5 | ||
|
|
e78b2368ae | ||
|
|
71de57af38 | ||
|
|
fe2db3e1d1 | ||
|
|
df6087d0d8 |
@@ -0,0 +1,151 @@
|
|||||||
|
# Video Encoder (videnc)
|
||||||
|
|
||||||
|
A Deno-based video encoding application that converts videos to the AV1 video
|
||||||
|
codec with Opus audio in MKV containers.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Encodes videos to AV1 (libsvtav1) for high efficiency
|
||||||
|
- Converts audio to Opus for excellent quality at low bitrates
|
||||||
|
- Automatically detects video properties (resolution, frame rate, interlacing,
|
||||||
|
anamorphic content)
|
||||||
|
- Applies appropriate filters for interlaced and anamorphic content
|
||||||
|
- Preserves and embeds metadata
|
||||||
|
- Integrates with OMDB API for automatic metadata fetching
|
||||||
|
- Generates shell scripts for encoding commands
|
||||||
|
- Supports multiple audio and subtitle streams
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Deno](https://deno.com/) v1.0 or higher
|
||||||
|
- [FFmpeg](https://ffmpeg.org/) with libsvtav1 support
|
||||||
|
- [FFprobe](https://ffmpeg.org/ffprobe.html) (usually bundled with FFmpeg)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd videenc_ts
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure you have Deno installed:
|
||||||
|
```bash
|
||||||
|
deno --version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ensure you have FFmpeg with libsvtav1 installed:
|
||||||
|
```bash
|
||||||
|
ffmpeg -version
|
||||||
|
ffmpeg -h encoder=libsvtav1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./videnc -i <input_video_file>
|
||||||
|
```
|
||||||
|
|
||||||
|
This will analyze the video and generate a shell script (`encode.sh` or
|
||||||
|
`encode_N.sh`) containing the FFmpeg command for encoding.
|
||||||
|
|
||||||
|
### With OMDB Metadata
|
||||||
|
|
||||||
|
To use OMDB metadata:
|
||||||
|
|
||||||
|
1. Get an API key from [OMDB API](http://www.omdbapi.com/apikey.aspx)
|
||||||
|
2. Add it to your settings file at `~/.config/videnc/settings.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"omdb_api_key": "your_api_key_here"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Run the encoder with a video file:
|
||||||
|
```bash
|
||||||
|
./videnc -i <input_video_file>
|
||||||
|
```
|
||||||
|
4. When prompted, enter the IMDb ID for the video (e.g., tt0111161 for The
|
||||||
|
Shawshank Redemption)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
You can build the application in two ways:
|
||||||
|
|
||||||
|
### Using Make
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Deno Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno compile --allow-all --output ./videnc ./src/main.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Both methods will create a standalone executable named `videnc`.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Video Analysis**: Uses FFprobe to analyze the input video file and extract
|
||||||
|
technical information
|
||||||
|
2. **Metadata Fetching**: Optionally fetches metadata from OMDB API using the
|
||||||
|
provided IMDb ID
|
||||||
|
3. **Profile Selection**: Automatically selects encoding profiles based on video
|
||||||
|
resolution:
|
||||||
|
- DVD profile for videos with less than 1 megapixel
|
||||||
|
- BluRay profile for higher resolution videos
|
||||||
|
4. **Command Generation**: Creates an optimized FFmpeg command with appropriate
|
||||||
|
settings for:
|
||||||
|
- Video encoding (AV1 with libsvtav1)
|
||||||
|
- Audio encoding (Opus)
|
||||||
|
- Subtitle handling (copy)
|
||||||
|
- Metadata embedding
|
||||||
|
- Filters for interlaced and anamorphic content
|
||||||
|
5. **Script Creation**: Generates a shell script with the encoding command
|
||||||
|
|
||||||
|
## Encoding Profiles
|
||||||
|
|
||||||
|
### DVD Profile
|
||||||
|
|
||||||
|
- CRF: 29
|
||||||
|
- Preset: 2
|
||||||
|
- Film Grain: 5
|
||||||
|
|
||||||
|
### BluRay Profile
|
||||||
|
|
||||||
|
- CRF: 27
|
||||||
|
- Preset: 2
|
||||||
|
- Film Grain: 5
|
||||||
|
|
||||||
|
## Example Generated Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffmpeg \
|
||||||
|
-hide_banner \
|
||||||
|
-i "input_video.mp4" \
|
||||||
|
-map_metadata -1 \
|
||||||
|
-map 0:v \
|
||||||
|
-c:v libsvtav1 \
|
||||||
|
-pix_fmt yuv420p10le \
|
||||||
|
-crf 27 \
|
||||||
|
-svtav1-params keyint=120:tune=0:film-grain=5:scd=1:film-grain-denoise=1:enable-overlays=1:enable-qm=1:qm-min=0:qm-max=15 \
|
||||||
|
-preset 2 \
|
||||||
|
-vf "bwdif=mode=0,scale=iw*sar:ih:lanczos,setsar=1,scale=-2:ih:lanczos" \
|
||||||
|
-map 0:a:0 \
|
||||||
|
-c:a:0 libopus \
|
||||||
|
-metadata:s:a:0 language=eng \
|
||||||
|
-disposition:a:0 default \
|
||||||
|
-map 0:s:0 \
|
||||||
|
-c:s:0 copy \
|
||||||
|
-metadata:s:s:0 language=eng \
|
||||||
|
-disposition:s:0 -1 \
|
||||||
|
"output_video.av1.mkv"
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||||
|
for details.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Encoding Settings Changelog
|
||||||
|
|
||||||
|
This document tracks changes to the encoding settings used by videnc over time.
|
||||||
|
|
||||||
|
## Version 1.0 - Initial Release (2025-10-04)
|
||||||
|
|
||||||
|
### Universal Settings
|
||||||
|
|
||||||
|
These settings are applied to all videos regardless of source material:
|
||||||
|
|
||||||
|
- **Video Codec**: libsvtav1 (AV1)
|
||||||
|
- **Pixel Format**: yuv420p10le
|
||||||
|
- **Preset**: 4 (universal setting)
|
||||||
|
- **enable-overlays**: 1 (enabled for all encodings)
|
||||||
|
- **enable-qm**: 1 (enabled for all encodings)
|
||||||
|
- **qm-min**: 0
|
||||||
|
- **qm-max**: 15
|
||||||
|
- **scd**: 1 (scene change detection enabled)
|
||||||
|
- **film-grain-denoise**: 1 (enabled for all encodings)
|
||||||
|
- **tune**: 0 (default tuning)
|
||||||
|
|
||||||
|
### Variable Settings
|
||||||
|
|
||||||
|
These settings vary based on video characteristics:
|
||||||
|
|
||||||
|
#### Resolution-Based Settings
|
||||||
|
- **CRF**: 30 for videos < 1 MPixel, 28 for videos >= 1 MPixel
|
||||||
|
- **film-grain**: 10 for videos < 1 MPixel, 5 for videos >= 1 MPixel
|
||||||
|
|
||||||
|
### Source Material Dependent Settings
|
||||||
|
|
||||||
|
These settings are conditionally applied based on the characteristics of the source video:
|
||||||
|
|
||||||
|
- **Deinterlacing**: Applied when `isInterlaced` is detected
|
||||||
|
- Filter: `bwdif=mode=0`
|
||||||
|
|
||||||
|
- **Anamorphic Correction**: Applied when `isAnamorphic` is detected
|
||||||
|
- Filter chain: `scale=iw*sar:ih:lanczos,setsar=1,scale=-2:ih:lanczos`
|
||||||
|
|
||||||
|
- **Keyframe Interval**: Dynamically calculated based on video FPS
|
||||||
|
- Value: `Math.round(videoInfo.fps * 5)`
|
||||||
|
|
||||||
|
### Audio Settings
|
||||||
|
|
||||||
|
- **Audio Codec**: libopus for all audio streams
|
||||||
|
- **Channel Layout**: Preserved from source (with special handling for sideloaded audio)
|
||||||
|
- **Language Metadata**: Preserved for each stream
|
||||||
|
- **Disposition**: English audio tracks set as default
|
||||||
|
|
||||||
|
### Subtitle Settings
|
||||||
|
|
||||||
|
- **Subtitle Handling**: Copy (no re-encoding)
|
||||||
|
- **Language Metadata**: Preserved for each stream
|
||||||
|
- **Disposition**: Finnish subtitle tracks set as default
|
||||||
|
|
||||||
|
### Metadata Handling
|
||||||
|
|
||||||
|
- **Metadata Stripping**: Original metadata is stripped (`-map_metadata -1`)
|
||||||
|
- **Custom Metadata**: Added from OMDB API or user input
|
||||||
|
- **Encoding Version**: Added as `ENCODING_VERSION=1.0`
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.PHONY: build clean run
|
||||||
|
|
||||||
|
build:
|
||||||
|
@echo "Building..."
|
||||||
|
deno compile --allow-all --output ./videnc ./src/main.ts
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning..."
|
||||||
|
rm -f ./videnc
|
||||||
|
|
||||||
+13
-9
@@ -8,14 +8,14 @@ function escapeFilename(filename: string): string {
|
|||||||
|
|
||||||
const profiles: Record<string, profile> = {
|
const profiles: Record<string, profile> = {
|
||||||
"DVD": {
|
"DVD": {
|
||||||
"crf": 29,
|
"crf": 30,
|
||||||
"preset": 2,
|
"preset": 4,
|
||||||
"filmGrain": 5,
|
"filmGrain": 5,
|
||||||
},
|
},
|
||||||
"BluRay": {
|
"BluRay": {
|
||||||
"crf": 27,
|
"crf": 28,
|
||||||
"preset": 2,
|
"preset": 4,
|
||||||
"filmGrain": 5,
|
"filmGrain": 10,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ export function encodingCommandBuilder(videoInfo: parsedVideoInfo): string {
|
|||||||
const profile = videoInfo.MPix < 1 ? profiles.DVD : profiles.BluRay;
|
const profile = videoInfo.MPix < 1 ? profiles.DVD : profiles.BluRay;
|
||||||
const args = [
|
const args = [
|
||||||
"ffmpeg \\\n",
|
"ffmpeg \\\n",
|
||||||
"-v",
|
// "-v",
|
||||||
"quiet \\\n",
|
// "quiet \\\n",
|
||||||
"-hide_banner \\\n",
|
"-hide_banner \\\n",
|
||||||
"-i",
|
"-i",
|
||||||
`"${videoInfo.filename}" \\\n`,
|
`"${videoInfo.filename}" \\\n`,
|
||||||
@@ -45,7 +45,9 @@ export function encodingCommandBuilder(videoInfo: parsedVideoInfo): string {
|
|||||||
args.push("-crf", profile.crf + " \\\n");
|
args.push("-crf", profile.crf + " \\\n");
|
||||||
args.push(
|
args.push(
|
||||||
"-svtav1-params",
|
"-svtav1-params",
|
||||||
`tune=0:film-grain=${profile.filmGrain}:scd=1:film-grain-denoise=1 \\\n`,
|
`keyint=${
|
||||||
|
Math.round(videoInfo.fps * 5)
|
||||||
|
}:tune=0:film-grain=${profile.filmGrain}:scd=1:film-grain-denoise=1:enable-overlays=1:enable-qm=1:qm-min=0:qm-max=15 \\\n`,
|
||||||
);
|
);
|
||||||
args.push("-preset", profile.preset + " \\\n");
|
args.push("-preset", profile.preset + " \\\n");
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ export function encodingCommandBuilder(videoInfo: parsedVideoInfo): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push("-g", `${Math.round(videoInfo.fps * 5)} \\\n`);
|
// args.push("-g", `${Math.round(videoInfo.fps * 5)} \\\n`);
|
||||||
|
|
||||||
if (videoInfo.metadata) {
|
if (videoInfo.metadata) {
|
||||||
args.push("-metadata", `title="${videoInfo.metadata.title}" \\\n`);
|
args.push("-metadata", `title="${videoInfo.metadata.title}" \\\n`);
|
||||||
@@ -80,6 +82,8 @@ export function encodingCommandBuilder(videoInfo: parsedVideoInfo): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.push("-metadata", "ENCODING_VERSION=1.0 \\\n");
|
||||||
|
|
||||||
for (const [index, audioStream] of videoInfo.audioStreams?.entries() ?? []) {
|
for (const [index, audioStream] of videoInfo.audioStreams?.entries() ?? []) {
|
||||||
args.push(
|
args.push(
|
||||||
"-map",
|
"-map",
|
||||||
|
|||||||
+26
-5
@@ -5,11 +5,29 @@ import {
|
|||||||
OMDBMovieInfo,
|
OMDBMovieInfo,
|
||||||
OMDBSeriesInfo,
|
OMDBSeriesInfo,
|
||||||
parsedVideoInfo,
|
parsedVideoInfo,
|
||||||
|
settings,
|
||||||
VideoInfo,
|
VideoInfo,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
import { VideoParser } from "./videoParser.ts";
|
import { VideoParser } from "./videoParser.ts";
|
||||||
import { encodingCommandBuilder } from "./commandBuilder.ts";
|
import { encodingCommandBuilder } from "./commandBuilder.ts";
|
||||||
|
|
||||||
|
const settings_file = Deno.env.get("HOME") + "/.config/videnc/settings.json";
|
||||||
|
|
||||||
|
function loadSettings(): settings {
|
||||||
|
if (!exists(settings_file)) {
|
||||||
|
const settings: settings = { omdb_api_key: "" };
|
||||||
|
Deno.writeFileSync(
|
||||||
|
settings_file,
|
||||||
|
new TextEncoder().encode(JSON.stringify(settings)),
|
||||||
|
);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
const settings: settings = JSON.parse(
|
||||||
|
new TextDecoder().decode(Deno.readFileSync(settings_file)),
|
||||||
|
);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
function analyzeVideo(videoFile: string): VideoInfo {
|
function analyzeVideo(videoFile: string): VideoInfo {
|
||||||
const cmd = "ffprobe";
|
const cmd = "ffprobe";
|
||||||
const args = [
|
const args = [
|
||||||
@@ -70,23 +88,26 @@ async function writeShellScript(command: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Deno.writeTextFileSync(`./encode_${counter}.sh`, command);
|
Deno.writeTextFileSync(`./encode_${counter}.sh`, command);
|
||||||
|
Deno.chmod(`./encode_${counter}.sh`, 0o755);
|
||||||
console.log(`Created ./encode_${counter}.sh`);
|
console.log(`Created ./encode_${counter}.sh`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const script = `#!/bin/bash\n${command}`;
|
const script = `#!/bin/bash\n${command}`;
|
||||||
Deno.writeTextFileSync("./encode.sh", script);
|
Deno.writeTextFileSync("./encode.sh", script);
|
||||||
|
Deno.chmod("./encode.sh", 0o755);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
const args = parseArgs(Deno.args);
|
const args = parseArgs(Deno.args);
|
||||||
if (!args.i) {
|
if (!args.i) {
|
||||||
console.log("Usage: videnc -i <video_file> -k <omdb_api_key>");
|
console.log("Usage: videnc -i <video_file>");
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
if (!args.k) {
|
const app_settings = loadSettings();
|
||||||
|
if (!app_settings.omdb_api_key) {
|
||||||
const confirm = prompt(
|
const confirm = prompt(
|
||||||
"No key entered. Are you sure you want to continue? (y/N)",
|
"No key found. Are you sure you want to continue? (y/N)",
|
||||||
);
|
);
|
||||||
if (confirm !== "y") {
|
if (confirm !== "y") {
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
@@ -101,11 +122,11 @@ async function main() {
|
|||||||
// Analyze video
|
// Analyze video
|
||||||
const videoInfo = analyzeVideo(videoFile);
|
const videoInfo = analyzeVideo(videoFile);
|
||||||
videoInfo.format.filename = resolve(videoFile);
|
videoInfo.format.filename = resolve(videoFile);
|
||||||
if (args.k) {
|
if (app_settings.omdb_api_key) {
|
||||||
const imdbID = prompt("Enter IMDb ID. Leave empty to skip");
|
const imdbID = prompt("Enter IMDb ID. Leave empty to skip");
|
||||||
let jsonVideoInfo;
|
let jsonVideoInfo;
|
||||||
if (imdbID) {
|
if (imdbID) {
|
||||||
const movieInfo = await fetchMovieInfo(imdbID, args.k);
|
const movieInfo = await fetchMovieInfo(imdbID, app_settings.omdb_api_key);
|
||||||
jsonVideoInfo = buildVideoInfo(videoInfo, movieInfo);
|
jsonVideoInfo = buildVideoInfo(videoInfo, movieInfo);
|
||||||
} else {
|
} else {
|
||||||
jsonVideoInfo = buildVideoInfo(videoInfo);
|
jsonVideoInfo = buildVideoInfo(videoInfo);
|
||||||
|
|||||||
@@ -230,3 +230,7 @@ export interface profile {
|
|||||||
preset: number;
|
preset: number;
|
||||||
filmGrain: number;
|
filmGrain: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface settings {
|
||||||
|
omdb_api_key: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user