Compare commits

..
9 Commits
Author SHA1 Message Date
Esa Kataja 63fda950e5 fix: swap film grain values between DVD and BluRay profiles to match quality standards 2025-10-04 18:30:30 +03:00
Esa Kataja 5b4b36c928 chore: tweak encoding version documentation format 2025-10-04 18:21:51 +03:00
Esa Kataja f3f88edc0f feat: mark encode script as executable 2025-10-04 18:21:29 +03:00
Esa Kataja 1eda5b676f docs: document encoding setting versions 2025-10-04 18:12:08 +03:00
Esa Kataja 702a92ccd5 change: tweak profile settings 2025-10-04 18:11:50 +03:00
Esa Kataja e78b2368ae chore: added makefile 2025-10-04 17:26:47 +03:00
Esa Kataja 71de57af38 doc: added Readme file 2025-10-04 17:26:26 +03:00
Esa Kataja fe2db3e1d1 feat: store OMDB API key in config file instead of command line argument 2025-10-04 17:24:11 +03:00
Esa Kataja df6087d0d8 feat: improve keyframe handling and encoding parameters
- Move keyframe interval from -g flag to SVT-AV1's keyint parameter
- Add enable-qm parameter for better quality management
- Disable quiet mode to show FFmpeg progress during encoding
2025-10-04 17:23:22 +03:00
6 changed files with 264 additions and 14 deletions
+151
View File
@@ -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.
+60
View File
@@ -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`
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+4
View File
@@ -230,3 +230,7 @@ export interface profile {
preset: number; preset: number;
filmGrain: number; filmGrain: number;
} }
export interface settings {
omdb_api_key: string;
}