📱 MKV → HEVC Batch Encoder for Smartphone

Windows 11 & CachyOS (Linux) — encode_hevc.bat / encode_hevc.sh
🌐 日本語
⬅ Return To CachyOS Tips

🎥 Recommended Player: VLC for Smartphone

VLC is a completely free, open-source media player that can decode almost any video format, including the HEVC (H.265) files produced by this script. It provides excellent hardware decoding, reliable subtitle support, and works great on both Android and iOS.

Installing FFmpeg

Both scripts require FFmpeg (which includes both ffmpeg and ffprobe) installed and available in your system PATH. Select your operating system below.

どちらのスクリプトもFFmpegffmpegずffprobeの䞡方を含むがシステムのPATHに入っおいる必芁がありたす。以䞋からOSを遞択しおください。

The recommended build for Windows is the Gyan.dev full release, which includes all necessary libraries: libass (subtitle rendering), hevc_amf (AMD), hevc_nvenc (NVIDIA), hevc_qsv (Intel), and libx265 (software).

Option 1 — winget (Recommended)

Open Command Prompt or PowerShell and run:

winget install Gyan.FFmpeg

After installation, close and reopen your terminal to refresh the PATH, then verify with:

ffmpeg -version
✔ Why winget? winget is built into Windows 11 and handles PATH setup automatically — no manual extraction or environment variable editing needed. Future updates are easy too: winget upgrade Gyan.FFmpeg

Option 2 — Manual Install

  1. Go to gyan.dev/ffmpeg/builds and download the release full ZIP — not "essentials", as the full build includes libass for subtitle rendering.
  2. Extract the ZIP to a permanent location such as C:\ffmpeg\.
  3. Add C:\ffmpeg\bin to your system PATH via System Properties → Environment Variables → Path → Edit → New.
  4. Open a new Command Prompt and run ffmpeg -version to confirm.

On CachyOS (Arch-based), FFmpeg is available from the official repositories and includes all required encoders and filters — including libass, hevc_nvenc, libx265, and VA-API support.

Install via pacman

sudo pacman -S ffmpeg

Verify the install:

ffmpeg -version
✔ AMD AMF on Linux (optional) If you choose the AMD AMF encoder (option 1 in the script), the AMF runtime library is required and must be installed from the AUR. The script will automatically detect if it is missing and offer to install it via yay. If you prefer to skip the extra setup, choose option 2 (AMD VA-API) instead — it works out of the box with no additional packages.
ℹ VA-API render device The script defaults to /dev/dri/renderD128, which covers most single-GPU systems. If you have both an AMD and an Intel GPU, Intel is usually on renderD129. You can change the VAAPI_DEVICE variable at the top of the script if needed.

Download the Script

Download the script for your operating system and place it directly inside the folder containing your .mkv files. The script will automatically process every single .mkv file in that same folder, so make sure the folder only contains the files you want to convert.

お䜿いのOSのスクリプトをダりンロヌドしお、.mkvファむルが入っおいるフォルダに盎接眮いおください。スクリプトはそのフォルダ内の党.mkvファむルを自動的に凊理したす。倉換したいファむルだけそのフォルダに入れおおいおください。

Download encode_hevc.bat and place it in the same folder as your MKV files.

⚠ Run via cmd.exe — NOT PowerShell Always launch this script by double-clicking it in Windows Explorer, or by running it from a cmd.exe window. Running from PowerShell will cause the loop variables to fail and nothing will encode.

Download encode_hevc.sh, place it in the same folder as your MKV files, and make it executable:

chmod +x encode_hevc.sh
ℹ Run from the script's directory Always cd into the folder containing your MKV files before running the script, or open a terminal directly in that folder. The script processes *.mkv in the current working directory.

What This Script Does

These scripts convert all MKV files in the same folder into smartphone-optimised MP4 files using HEVC (H.265) encoding. They are designed for watching anime and Japanese media on smartphones using a player such as VLC. Both scripts produce identical output — only the platform and available encoders differ.

これらのスクリプトは同じフォルダ内の党MKVファむルをスマヌトフォン最適化MP4に䞀括倉換したす。VLCなどのプレむダヌでスマヌトフォン䞊でアニメや日本語メディアを芖聎するこずを目的ずしお蚭蚈されおいたす。䞡スクリプトは同䞀の出力を生成したす — 違いはプラットフォヌムず利甚可胜な゚ンコヌダヌのみです。

Setting Value Notes
Video CodecHEVC / H.265Hardware (GPU) or software
Video QualityQP 31Good balance of quality and file size
Output Resolution1280×720 (16:9)
960×720 (4:3)
Auto-detected from source
Scale FilterLanczosHigh-quality downscaling
Sharpeningunsharp 3:3:0.3Subtle post-downscale crispness
Audio CodecAAC-LC stereoUniversal smartphone compatibility
Audio Bitrate128 kbpsGood quality for voice/music on mobile
Audio TrackJapanese (jpn) onlyAuto-selects first/default Japanese track
SubtitlesBurned inEnglish → default → first (auto fallback)
ChaptersPreservedOpening/ending markers carried over
MP4 Taghvc1Required for Apple device playback
FaststartEnabledInstant playback start, great for Wi-Fi streaming
Output Folderencoded_output/Created automatically in the script's folder

How to Use

  1. Install FFmpeg using winget or manually as described above.
  2. Place encode_hevc.bat inside the same folder as your MKV files. It will encode every .mkv it finds — the script does not recurse into subfolders.
  3. Double-click encode_hevc.bat in Windows Explorer. A Command Prompt window will open.
  4. A GPU selection menu will appear. Type the number for your hardware (1–4) and press Enter.
  5. The script analyses and encodes each file one by one, showing progress in the console.
  6. Encoded files appear in the encoded_output\ subfolder, ready to transfer to your smartphone.
  1. Install FFmpeg via sudo pacman -S ffmpeg as described above.
  2. Place encode_hevc.sh inside the same folder as your MKV files and make it executable with chmod +x encode_hevc.sh.
  3. Open a terminal in that folder and run ./encode_hevc.sh.
  4. A GPU selection menu will appear. Type the number for your hardware (1–5) and press Enter.
  5. The script analyses and encodes each file one by one, showing progress in the terminal.
  6. Encoded files appear in the encoded_output/ subfolder, ready to transfer to your smartphone.

GPU / Encoder Options

When you run the script you will see a GPU selection menu. The available options differ slightly between platforms.

スクリプトを実行するずGPU遞択メニュヌが衚瀺されたす。利甚可胜な遞択肢はプラットフォヌムによっお若干異なりたす。

  Select your GPU / encoder:

    1. AMD      (hevc_amf   - VCE hardware)
    2. NVIDIA   (hevc_nvenc - NVENC hardware)
    3. Intel    (hevc_qsv   - Quick Sync hardware)
    4. Software (libx265    - slow but universal)
# Encoder GPU Required Quality Mode Speed
1 AMD hevc_amf AMD RX 400 series or newer CQP 31 Very fast ⚡
2 NVIDIA hevc_nvenc GTX 950 or newer constqp 31 Very fast ⚡
3 INTEL hevc_qsv Intel 6th gen (Skylake) iGPU or newer ICQ 31 Fast ⚡
4 SOFTWARE libx265 None — CPU only CRF 22 Slow 🐢
  Select your GPU / encoder:

    1. AMD      (hevc_amf   - AMF hardware, fastest)
    2. AMD      (hevc_vaapi - VA-API hardware, no extra install)
    3. NVIDIA   (hevc_nvenc - NVENC hardware)
    4. Intel    (hevc_vaapi - VA-API hardware, no extra install)
    5. Software (libx265    - slow but universal)
# Encoder GPU Required Quality Mode Speed
1 AMD hevc_amf AMD GPU + AUR package CQP 31 Very fast ⚡
2 AMD hevc_vaapi AMD GPU (no extra packages) CQP 31 Very fast ⚡
3 NVIDIA hevc_nvenc GTX 950 or newer constqp 31 Very fast ⚡
4 INTEL hevc_vaapi Intel iGPU (no extra packages) CQP 31 Fast ⚡
5 SOFTWARE libx265 None — CPU only CRF 22 Slow 🐢
ℹ Quality equivalence across encoders All options are calibrated for approximately equivalent visual quality. Hardware encoders (AMD/NVIDIA/Intel) sacrifice a small amount of efficiency for dramatically faster encoding. The software encoder (libx265 CRF 22) produces the smallest files at the same quality but may take 10–20× longer.

Detailed Feature Breakdown

🎞 Automatic Aspect Ratio Detection

Before encoding, ffprobe reads the stored pixel dimensions and the Sample Aspect Ratio (SAR) to calculate the true display width. It then picks the correct output resolution:

゚ンコヌド前にffprobeが保存されたピクセル寞法ずサンプルアスペクト比SARを読み取り、実際の衚瀺幅を蚈算しお正しい出力解像床を遞びたす

Display AspectOutputTypical Source
16:91280×720BD/HD anime, modern TV
4:3960×720DVD anime, older TV releases
Otherauto width × 720Fallback for unusual sources

🔀 Automatic Subtitle Selection (3-tier fallback)

  1. English-tagged — finds the subtitle stream with language=eng metadata.
  2. Default-flagged — selects the subtitle stream marked disposition:default=1 by the file author.
  3. First available — last resort: uses the first subtitle track (s:0) regardless of language.

If a file has no subtitle tracks at all, it is skipped with a warning in the console.

字幕トラックが党くないファむルは譊告付きでスキップされたす。

🔊 Japanese Audio Selection

Only the Japanese (jpn) audio track is mapped to the output — commentary tracks, dubs, and other languages are automatically discarded. The script first looks for a default-flagged Japanese track, then falls back to the first Japanese track. If no Japanese track exists, the first audio track is used with a warning.

日本語jpn音声トラックのみが出力にマッピングされたす。コメンタリヌトラック、吹き替え、その他の蚀語は自動的に陀倖されたす。たずデフォルトフラグ付きの日本語トラックを探し、なければ最初の日本語トラックにフォヌルバック。日本語トラックが存圚しない堎合は譊告付きで最初の音声トラックが䜿甚されたす。

🎚 Video Quality Pipeline

Lanczos scaling — high-quality downscale algorithm that preserves sharpness better than the default bilinear filter. Applied before subtitle burn-in.

ランチョスダりンスケヌル — デフォルトのバむリニアフィルタヌより鮮明さを保持する高品質ダりンスケヌルアルゎリズム。字幕焌き蟌み前に適甚。

Unsharp mask (3:3:0.3) — subtle sharpening applied after scaling to recover crispness lost during downscaling. Conservative values to avoid artefacts.

アンシャヌプマスク3:3:0.3 — ダりンスケヌル埌に倱われる鮮明さを回埩するための控えめなシャヌプニング。アヌティファクトを避けるため控えめな倀に蚭定。

Subtitle burn-in (hardsubbing) — subtitles are permanently rendered into the video frames using libass, the same renderer used by mpv and VLC. Embedded fonts from the MKV are used automatically.

字幕焌き蟌みハヌドサブ — mpvやVLCず同じ高品質レンダラヌlibassを䜿っお字幕を映像フレヌムに氞続的にレンダリング。MKVに埋め蟌たれたフォントも自動的に䜿甚。

Chapters, title metadata, hvc1 tag, and faststart — chapter markers are preserved from the source MKV; the title tag is set from the filename; the hvc1 codec tag ensures correct playback on Apple devices; faststart moves metadata to the front of the file for instant playback.

チャプタヌ、タむトルメタデヌタ、hvc1タグ、ファストスタヌト — チャプタヌマヌカヌは゜ヌスMKVから保持。タむトルタグはファむル名から蚭定。hvc1コヌデックタグはAppleデバむスでの正しい再生を確保。ファストスタヌトはメタデヌタをファむルの先頭に移動しお即時再生を実珟。

When using a VA-API encoder (options 2 or 4), all filters run on the CPU first. The pipeline converts Hi10P sources to 8-bit (format=yuv420p), then converts to NV12 and uploads frames to the GPU (format=nv12,hwupload) just before the hardware encoder receives them. This is handled automatically — you do not need to change anything.

Smartphone Compatibility

Both scripts are optimised for playback on smartphones using VLC for iOS / Android. Output settings were specifically tested on an iPhone 11 with VLC.

䞡スクリプトはVLC for iOS / Androidでのスマヌトフォン再生に最適化されおいたす。出力蚭定はiPhone 11でVLCを䜿っお具䜓的にテストされおいたす。

Device / AppCompatibilityNotes
iPhone 11 + VLC✅ ExcellentHardware HEVC decode, directly tested
iPhone 11 + Native Player✅ Goodhvc1 tag required (included)
iPhone 12+✅ ExcellentHardware HEVC decode
Android + VLC✅ ExcellentHEVC hardware decode on most modern Android
Android Native Gallery⚠ VariesSamsung/Pixel generally fine
ℹ Why 8-bit instead of 10-bit? Many source files (especially Hi10P anime) are 10-bit. While technically supported, 10-bit HEVC can display with washed-out, faded colours on devices like the iPhone 11 when played via VLC. Converting to 8-bit (yuv420p) ensures correct, vivid colours on all devices with no loss in perceived quality at 720p.
✔ Expected file sizes At QP 31 with 128kbps audio, a typical 24-minute anime episode at 1280×720 will be approximately 150–250 MB, depending on animation complexity. This is a major reduction from typical 600 MB–1 GB MKV sources, making it practical to carry a full season on your phone.

Full Script Code

Copy and paste the code below into a new text file, then save it as encode_hevc.bat. In Notepad, set "Save as type" to "All Files (*.*)" to prevent it saving as .txt.

⚠ Run via cmd.exe — NOT PowerShell Always launch this script by double-clicking it in Windows Explorer, or by running it from a cmd.exe window. Running from PowerShell will cause the loop variables to fail and nothing will encode.
@echo off
setlocal enabledelayedexpansion

:: ============================================================
::  Universal MKV → HEVC 8-bit MP4 Batch Encoder
::  Supports: AMD VCE, NVIDIA NVENC, Intel QSV, Software (x265)
::  Audio:     First Japanese track only → AAC 128kbps stereo
::  Subtitles: Burned in with fallback priority:
::               1. English-tagged (eng)
::               2. Default-flagged subtitle in MKV
::               3. First available subtitle track
::               4. Skip file if no subtitles exist
::  Aspect:    4:3 source → 960x720 / 16:9 source → 1280x720
::  Chapters:  Preserved
::  Metadata:  Title set from filename
::
::  IMPORTANT: Run by double-clicking in Explorer or via cmd.exe
::             Do NOT run from PowerShell.
::
::  Requirements: ffmpeg + ffprobe must be in PATH
:: ============================================================

:: --- SETTINGS -----------------------------------------------

set QP=31
set AUDIO_BITRATE=128k
set OUTPUT_DIR=encoded_output
set SHARPEN=3:3:0.3

:: -------------------------------------------------------

:: ================================================================
:: GPU SELECTION MENU
:: Equivalent quality settings across encoders matching CQP 31:
::   AMD    — hevc_amf,   -rc cqp       qp 31
::   NVIDIA — hevc_nvenc, -rc constqp   qp 31
::   Intel  — hevc_qsv,   -q 31         (ICQ mode)
::   x265   — libx265,    -crf 22       (CRF 22 ≈ CQP 31 for HEVC)
:: ================================================================

echo ============================================================
echo  Universal HEVC 8-bit Encoder
echo  QP=31  Audio=128kbps  4:3=960x720  16:9=1280x720
echo ============================================================
echo.
echo  Select your GPU / encoder:
echo.
echo    1. AMD      ^(hevc_amf   - VCE hardware^)
echo    2. NVIDIA   ^(hevc_nvenc - NVENC hardware^)
echo    3. Intel    ^(hevc_qsv   - Quick Sync hardware^)
echo    4. Software ^(libx265    - slow but universal^)
echo.
set /p GPU_CHOICE="  Enter choice (1-4): "

if "%GPU_CHOICE%" == "1" (
    set ENCODER=hevc_amf
    set ENC_LABEL=AMD VCE
    set ENC_PARAMS=-rc cqp -qp_i %QP% -qp_p %QP% -qp_b %QP% -quality quality -profile:v main -pix_fmt yuv420p
)
if "%GPU_CHOICE%" == "2" (
    set ENCODER=hevc_nvenc
    set ENC_LABEL=NVIDIA NVENC
    set ENC_PARAMS=-rc constqp -qp %QP% -preset p4 -profile:v main -pix_fmt yuv420p
)
if "%GPU_CHOICE%" == "3" (
    set ENCODER=hevc_qsv
    set ENC_LABEL=Intel Quick Sync
    set ENC_PARAMS=-q %QP% -preset slower -profile:v main -pix_fmt nv12
)
if "%GPU_CHOICE%" == "4" (
    set ENCODER=libx265
    set ENC_LABEL=Software x265
    set ENC_PARAMS=-crf 22 -preset slow -profile:v main -pix_fmt yuv420p
)

if not defined ENCODER (
    echo.
    echo  Invalid choice. Please enter 1, 2, 3 or 4.
    pause
    exit /b 1
)

echo.
echo  Using encoder: %ENC_LABEL% ^(%ENCODER%^)
echo  Output folder: %OUTPUT_DIR%
echo ============================================================
echo.

if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"

set TMPFILE=%TEMP%\ffprobe_tmp.txt

set ENCODED=0
set FAILED=0
set SKIPPED=0

for %%F in (*.mkv) do (
    echo -------------------------------------------------------
    echo [%%F] Analysing streams...

    :: ================================================================
    :: SUBTITLE SELECTION — three-tier fallback
    :: ================================================================
    set SUB_INDEX=
    set SUB_METHOD=
    set SUB_REL_COUNTER=0

    ffprobe -v error -select_streams s -show_entries stream=index,disposition=default:stream_tags=language -of csv=p=0 "%%F" > "%TMPFILE%" 2>&1

    :: ---- Tier 1: English-tagged subtitle ----
    for /f "usebackq delims=" %%S in ("%TMPFILE%") do (
        if not defined SUB_INDEX (
            set LINE=%%S
            echo !LINE! | findstr /i ",eng" >nul 2>&1
            if !errorlevel! == 0 (
                set SUB_INDEX=!SUB_REL_COUNTER!
                set SUB_METHOD=English-tagged
            )
            set /a SUB_REL_COUNTER+=1
        ) else (
            set /a SUB_REL_COUNTER+=1
        )
    )

    :: ---- Tier 2: Default-flagged subtitle ----
    if not defined SUB_INDEX (
        set SUB_REL_COUNTER=0
        for /f "usebackq delims=" %%S in ("%TMPFILE%") do (
            if not defined SUB_INDEX (
                set LINE=%%S
                echo !LINE! | findstr /r ",1," >nul 2>&1
                if !errorlevel! == 0 (
                    set SUB_INDEX=!SUB_REL_COUNTER!
                    set SUB_METHOD=default-flagged
                )
            )
            set /a SUB_REL_COUNTER+=1
        )
    )

    :: ---- Tier 3: First available subtitle ----
    if not defined SUB_INDEX (
        for /f "usebackq delims=" %%S in ("%TMPFILE%") do (
            if not defined SUB_INDEX (
                set SUB_INDEX=0
                set SUB_METHOD=first available
            )
        )
    )

    :: ================================================================
    :: All remaining processing in if/else — no goto inside loop
    :: ================================================================
    if not defined SUB_INDEX (
        echo [%%F] WARNING: No subtitle tracks found. Skipping file.
        set /a SKIPPED+=1
    ) else (
        echo [%%F] Subtitle selected: index !SUB_INDEX! ^(!SUB_METHOD!^)

        :: ================================================================
        :: RESOLUTION — 4:3 → 960x720, 16:9 → 1280x720
        :: ================================================================
        set STORED_W=
        set STORED_H=
        set SAR_N=1
        set SAR_D=1

        ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=nw=1:nk=1 "%%F" > "%TMPFILE%" 2>&1
        for /f "usebackq tokens=1" %%V in ("%TMPFILE%") do if not defined STORED_W set STORED_W=%%V

        ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "%%F" > "%TMPFILE%" 2>&1
        for /f "usebackq tokens=1" %%V in ("%TMPFILE%") do if not defined STORED_H set STORED_H=%%V

        ffprobe -v error -select_streams v:0 -show_entries stream=sample_aspect_ratio -of default=nw=1:nk=1 "%%F" > "%TMPFILE%" 2>&1
        for /f "usebackq tokens=1,2 delims=:" %%A in ("%TMPFILE%") do (
            if not "%%A" == "N/A" if not "%%A" == "0" (
                set SAR_N=%%A
                set SAR_D=%%B
            )
        )

        if "!SAR_D!" == "0" set SAR_D=1
        if "!SAR_N!" == "0" set SAR_N=1

        set /a DISP_W=!STORED_W! * !SAR_N! / !SAR_D!

        set SCALE_FILTER=scale=1280:720:flags=lanczos
        set RES_LABEL=1280x720 ^(16:9 default^)

        if defined DISP_W if defined STORED_H (
            set /a RATIO10=!DISP_W! * 10 / !STORED_H!
            if !RATIO10! GEQ 14 (
                set SCALE_FILTER=scale=1280:720:flags=lanczos
                set RES_LABEL=1280x720 ^(16:9^)
            ) else (
                set SCALE_FILTER=scale=960:720:flags=lanczos
                set RES_LABEL=960x720 ^(4:3^)
            )
        )

        echo [%%F] Display: !DISP_W!x!STORED_H! -^> !RES_LABEL!

        :: ================================================================
        :: JAPANESE AUDIO — find first/default jpn track, map by index
        :: ================================================================
        set JPN_ABS=
        set JPN_REL=0
        set JPN_FOUND=

        ffprobe -v error -select_streams a -show_entries stream=index,disposition=default:stream_tags=language -of csv=p=0 "%%F" > "%TMPFILE%" 2>&1

        :: First pass: default-flagged jpn audio
        for /f "usebackq delims=" %%S in ("%TMPFILE%") do (
            if not defined JPN_ABS (
                set LINE=%%S
                echo !LINE! | findstr /i "jpn" >nul 2>&1
                if !errorlevel! == 0 (
                    echo !LINE! | findstr /r ",1" >nul 2>&1
                    if !errorlevel! == 0 (
                        for /f "tokens=1 delims=," %%I in ("!LINE!") do set JPN_ABS=%%I
                    )
                )
            )
        )

        :: Second pass: first jpn audio track
        if not defined JPN_ABS (
            for /f "usebackq delims=" %%S in ("%TMPFILE%") do (
                if not defined JPN_ABS (
                    set LINE=%%S
                    echo !LINE! | findstr /i "jpn" >nul 2>&1
                    if !errorlevel! == 0 (
                        for /f "tokens=1 delims=," %%I in ("!LINE!") do set JPN_ABS=%%I
                    )
                )
            )
        )

        :: Convert absolute index to audio-relative index
        if defined JPN_ABS (
            set JPN_REL=0
            ffprobe -v error -select_streams a -show_entries stream=index -of csv=p=0 "%%F" > "%TMPFILE%" 2>&1
            for /f "usebackq delims=" %%S in ("%TMPFILE%") do (
                if not defined JPN_FOUND (
                    for /f "tokens=1 delims=," %%I in ("%%S") do (
                        if %%I == !JPN_ABS! (
                            set JPN_FOUND=1
                        ) else if not defined JPN_FOUND (
                            set /a JPN_REL+=1
                        )
                    )
                )
            )
            echo [%%F] Japanese audio: stream !JPN_ABS! ^(audio index !JPN_REL!^)
            set JPN_MAP=0:a:!JPN_REL!
        ) else (
            echo [%%F] WARNING: No Japanese audio found, using first audio track.
            set JPN_MAP=0:a:0
        )

        :: ================================================================
        :: ENCODE
        :: ================================================================
        set TITLE=%%~nF
        echo [%%F] Starting encode ^(%ENC_LABEL%^)...

        ffmpeg -y ^
            -i "%%F" ^
            -vf "!SCALE_FILTER!,subtitles='%%F':si=!SUB_INDEX!,unsharp=%SHARPEN%" ^
            -c:v %ENCODER% ^
            %ENC_PARAMS% ^
            -tag:v hvc1 ^
            -movflags +faststart ^
            -c:a aac ^
            -b:a %AUDIO_BITRATE% ^
            -ac 2 ^
            -map 0:v:0 ^
            -map !JPN_MAP! ^
            -map_chapters 0 ^
            -metadata title="!TITLE!" ^
            "%OUTPUT_DIR%\%%~nF.mp4"

        if !errorlevel! == 0 (
            echo [%%F] Done ^> %OUTPUT_DIR%\%%~nF.mp4
            set /a ENCODED+=1
        ) else (
            echo [%%F] FAILED - check output above for errors
            set /a FAILED+=1
        )
    )

    set JPN_ABS=
    set JPN_FOUND=
    set STORED_W=
    set STORED_H=
    set SAR_N=1
    set SAR_D=1
    echo.
)

if exist "%TMPFILE%" del "%TMPFILE%"

echo ============================================================
echo  Finished!  Encoder: %ENC_LABEL%
echo  Encoded: %ENCODED%   Failed: %FAILED%   Skipped: %SKIPPED%
echo ============================================================
pause

Copy and paste the code below into a new text file and save it as encode_hevc.sh. Then make it executable with chmod +x encode_hevc.sh.

#!/usr/bin/env bash
# ============================================================
#  Universal MKV → HEVC 8-bit MP4 Batch Encoder
#  Supports: AMD AMF, AMD VA-API, NVIDIA NVENC, Intel VA-API, Software (x265)
#  Audio:     First Japanese track only → AAC 128kbps stereo
#  Subtitles: Burned in with fallback priority:
#               1. English-tagged (eng)
#               2. Default-flagged subtitle in MKV
#               3. First available subtitle track
#               4. Skip file if no subtitles exist
#  Aspect:    4:3 source → 960x720 / 16:9 source → 1280x720
#  Chapters:  Preserved
#  Metadata:  Title set from filename
#
#  Requirements: ffmpeg + ffprobe must be in PATH
# ============================================================

set -euo pipefail

# --- SETTINGS -----------------------------------------------

QP=31
AUDIO_BITRATE=128k
OUTPUT_DIR="encoded_output"
SHARPEN="3:3:0.3"

# ============================================================

echo "============================================================"
echo " Universal HEVC 8-bit Encoder"
echo " QP=31  Audio=128kbps  4:3=960x720  16:9=1280x720"
echo "============================================================"
echo
echo " Select your GPU / encoder:"
echo
echo "   1. AMD      (hevc_amf   - AMF hardware, fastest)"
echo "   2. AMD      (hevc_vaapi - VA-API hardware, no extra install)"
echo "   3. NVIDIA   (hevc_nvenc - NVENC hardware)"
echo "   4. Intel    (hevc_vaapi - VA-API hardware, no extra install)"
echo "   5. Software (libx265    - slow but universal)"
echo
read -rp "  Enter choice (1-5): " GPU_CHOICE

# VA-API render device — /dev/dri/renderD128 covers most single-GPU systems.
# If you have both AMD and Intel GPUs, Intel is usually renderD129.
VAAPI_DEVICE="/dev/dri/renderD128"

case "$GPU_CHOICE" in
    1)
        ENCODER="hevc_amf"
        ENC_LABEL="AMD AMF"
        ENC_PARAMS="-rc cqp -qp_i $QP -qp_p $QP -quality quality -profile:v main -pix_fmt yuv420p"
        USE_VAAPI=0

        # Check for AMF runtime and offer to install if missing
        if ! ldconfig -p 2>/dev/null | grep -q "libamfrt64.so" && \
           ! find /usr/lib /usr/local/lib 2>/dev/null | grep -q "libamfrt64.so"; then
            echo
            echo " WARNING: AMF runtime (libamfrt64.so) not found."
            echo " The package 'amf-amdgpu-pro' is required from the AUR."
            echo
            read -rp " Install it now via yay? (y/n): " INSTALL_AMF
            if [[ "${INSTALL_AMF,,}" == "y" ]]; then
                echo
                echo " Installing amf-amdgpu-pro..."
                if ! command -v yay &>/dev/null; then
                    echo " ERROR: yay not found. Please install amf-amdgpu-pro manually."
                    exit 1
                fi
                yay -S --noconfirm amf-amdgpu-pro
                echo " AMF runtime installed."
            else
                echo
                echo " Aborting. Re-run the script and choose option 2 (VA-API) instead,"
                echo " which works without any additional packages."
                exit 1
            fi
        fi
        ;;
    2)
        ENCODER="hevc_vaapi"
        ENC_LABEL="AMD VA-API"
        ENC_PARAMS="-rc_mode CQP -qp $QP -profile:v main"
        USE_VAAPI=1
        ;;
    3)
        ENCODER="hevc_nvenc"
        ENC_LABEL="NVIDIA NVENC"
        ENC_PARAMS="-rc constqp -qp $QP -preset p4 -profile:v main -pix_fmt yuv420p"
        USE_VAAPI=0
        ;;
    4)
        ENCODER="hevc_vaapi"
        ENC_LABEL="Intel VA-API"
        ENC_PARAMS="-rc_mode CQP -qp $QP -profile:v main"
        USE_VAAPI=1
        ;;
    5)
        ENCODER="libx265"
        ENC_LABEL="Software x265"
        ENC_PARAMS="-crf 22 -preset slow -profile:v main -pix_fmt yuv420p"
        USE_VAAPI=0
        ;;
    *)
        echo
        echo " Invalid choice. Please enter 1, 2, 3, 4 or 5."
        exit 1
        ;;
esac

echo
echo " Using encoder: $ENC_LABEL ($ENCODER)"
echo " Output folder: $OUTPUT_DIR"
echo "============================================================"
echo

mkdir -p "$OUTPUT_DIR"

TMPFILE="$(mktemp /tmp/ffprobe_tmp.XXXXXX)"
trap 'rm -f "$TMPFILE"' EXIT

ENCODED=0
FAILED=0
SKIPPED=0

shopt -s nullglob
MKV_FILES=(*.mkv)

if [[ ${#MKV_FILES[@]} -eq 0 ]]; then
    echo " No .mkv files found in current directory."
    exit 0
fi

for FILEPATH in "${MKV_FILES[@]}"; do
    FILENAME="$FILEPATH"
    BASENAME="${FILEPATH%.mkv}"

    echo "-------------------------------------------------------"
    echo "[$FILENAME] Analysing streams..."

    # ================================================================
    # SUBTITLE SELECTION — three-tier fallback
    # ================================================================
    SUB_INDEX=""
    SUB_METHOD=""

    ffprobe -v error -select_streams s \
        -show_entries stream=index,disposition=default:stream_tags=language \
        -of csv=p=0 "$FILENAME" > "$TMPFILE" 2>&1

    # ---- Tier 1: English-tagged subtitle ----
    SUB_REL_COUNTER=0
    while IFS= read -r LINE; do
        if [[ -z "$SUB_INDEX" ]]; then
            if echo "$LINE" | grep -qi ",eng"; then
                SUB_INDEX="$SUB_REL_COUNTER"
                SUB_METHOD="English-tagged"
            fi
        fi
        (( SUB_REL_COUNTER++ )) || true
    done < "$TMPFILE"

    # ---- Tier 2: Default-flagged subtitle ----
    if [[ -z "$SUB_INDEX" ]]; then
        SUB_REL_COUNTER=0
        while IFS= read -r LINE; do
            if [[ -z "$SUB_INDEX" ]]; then
                if echo "$LINE" | grep -qP ",1,|,1$"; then
                    SUB_INDEX="$SUB_REL_COUNTER"
                    SUB_METHOD="default-flagged"
                fi
            fi
            (( SUB_REL_COUNTER++ )) || true
        done < "$TMPFILE"
    fi

    # ---- Tier 3: First available subtitle ----
    if [[ -z "$SUB_INDEX" ]] && [[ -s "$TMPFILE" ]]; then
        SUB_INDEX=0
        SUB_METHOD="first available"
    fi

    # ================================================================
    # Skip if no subtitles found
    # ================================================================
    if [[ -z "$SUB_INDEX" ]]; then
        echo "[$FILENAME] WARNING: No subtitle tracks found. Skipping file."
        (( SKIPPED++ )) || true
        continue
    fi

    echo "[$FILENAME] Subtitle selected: index $SUB_INDEX ($SUB_METHOD)"

    # ================================================================
    # RESOLUTION — 4:3 → 960x720, 16:9 → 1280x720
    # ================================================================
    STORED_W=""
    STORED_H=""
    SAR_N=1
    SAR_D=1

    STORED_W=$(ffprobe -v error -select_streams v:0 \
        -show_entries stream=width -of default=nw=1:nk=1 "$FILENAME" 2>/dev/null | head -1)

    STORED_H=$(ffprobe -v error -select_streams v:0 \
        -show_entries stream=height -of default=nw=1:nk=1 "$FILENAME" 2>/dev/null | head -1)

    SAR_RAW=$(ffprobe -v error -select_streams v:0 \
        -show_entries stream=sample_aspect_ratio -of default=nw=1:nk=1 "$FILENAME" 2>/dev/null | head -1)

    if [[ "$SAR_RAW" != "N/A" && "$SAR_RAW" =~ ^([0-9]+):([0-9]+)$ ]]; then
        SAR_N="${BASH_REMATCH[1]}"
        SAR_D="${BASH_REMATCH[2]}"
        [[ "$SAR_N" == "0" ]] && SAR_N=1
        [[ "$SAR_D" == "0" ]] && SAR_D=1
    fi

    DISP_W=$(( STORED_W * SAR_N / SAR_D ))

    SCALE_FILTER="scale=1280:720:flags=lanczos"
    RES_LABEL="1280x720 (16:9 default)"

    if [[ -n "$DISP_W" && -n "$STORED_H" && "$STORED_H" -gt 0 ]]; then
        RATIO10=$(( DISP_W * 10 / STORED_H ))
        if [[ "$RATIO10" -ge 14 ]]; then
            SCALE_FILTER="scale=1280:720:flags=lanczos"
            RES_LABEL="1280x720 (16:9)"
        else
            SCALE_FILTER="scale=960:720:flags=lanczos"
            RES_LABEL="960x720 (4:3)"
        fi
    fi

    echo "[$FILENAME] Display: ${DISP_W}x${STORED_H} -> $RES_LABEL"

    # ================================================================
    # JAPANESE AUDIO — find first/default jpn track, map by index
    # ================================================================
    JPN_ABS=""
    JPN_REL=0

    ffprobe -v error -select_streams a \
        -show_entries stream=index,disposition=default:stream_tags=language \
        -of csv=p=0 "$FILENAME" > "$TMPFILE" 2>&1

    # First pass: default-flagged jpn audio
    while IFS= read -r LINE; do
        if [[ -z "$JPN_ABS" ]]; then
            if echo "$LINE" | grep -qi "jpn"; then
                if echo "$LINE" | grep -qP ",1,|,1$"; then
                    JPN_ABS=$(echo "$LINE" | cut -d',' -f1)
                fi
            fi
        fi
    done < "$TMPFILE"

    # Second pass: first jpn audio track
    if [[ -z "$JPN_ABS" ]]; then
        while IFS= read -r LINE; do
            if [[ -z "$JPN_ABS" ]]; then
                if echo "$LINE" | grep -qi "jpn"; then
                    JPN_ABS=$(echo "$LINE" | cut -d',' -f1)
                fi
            fi
        done < "$TMPFILE"
    fi

    # Convert absolute stream index to audio-relative index
    if [[ -n "$JPN_ABS" ]]; then
        JPN_REL=0
        ffprobe -v error -select_streams a \
            -show_entries stream=index -of csv=p=0 "$FILENAME" > "$TMPFILE" 2>&1
        JPN_FOUND=""
        while IFS= read -r LINE; do
            IDX=$(echo "$LINE" | cut -d',' -f1)
            if [[ "$IDX" == "$JPN_ABS" ]]; then
                JPN_FOUND=1
                break
            fi
            (( JPN_REL++ )) || true
        done < "$TMPFILE"
        echo "[$FILENAME] Japanese audio: stream $JPN_ABS (audio index $JPN_REL)"
        JPN_MAP="0:a:$JPN_REL"
    else
        echo "[$FILENAME] WARNING: No Japanese audio found, using first audio track."
        JPN_MAP="0:a:0"
    fi

    # ================================================================
    # ENCODE
    # ================================================================
    TITLE="$BASENAME"
    echo "[$FILENAME] Starting encode ($ENC_LABEL)..."

    # Escape the filename for the subtitles filter (colons and special chars)
    ESCAPED_PATH=$(printf '%s' "$FILENAME" | sed "s/'/'\\\\''/g; s/:/\\\\:/g")

    set +e
    if [[ "$USE_VAAPI" -eq 1 ]]; then
        # VA-API: CPU-side filters run first. format=yuv420p converts Hi10P
        # sources to 8-bit, then format=nv12,hwupload hands frames to the GPU.
        # shellcheck disable=SC2086
        ffmpeg -y \
            -vaapi_device "$VAAPI_DEVICE" \
            -i "$FILENAME" \
            -vf "${SCALE_FILTER},subtitles='${ESCAPED_PATH}':si=${SUB_INDEX},unsharp=${SHARPEN},format=yuv420p,format=nv12,hwupload" \
            -c:v "$ENCODER" \
            $ENC_PARAMS \
            -tag:v hvc1 \
            -movflags +faststart \
            -c:a aac \
            -b:a "$AUDIO_BITRATE" \
            -ac 2 \
            -map 0:v:0 \
            -map "$JPN_MAP" \
            -map_chapters 0 \
            -metadata title="$TITLE" \
            "${OUTPUT_DIR}/${BASENAME}.mp4"
    else
        # AMF, NVENC, libx265: format=yuv420p in the filter chain handles
        # Hi10P sources by converting to 8-bit before encoding.
        # shellcheck disable=SC2086
        ffmpeg -y \
            -i "$FILENAME" \
            -vf "${SCALE_FILTER},subtitles='${ESCAPED_PATH}':si=${SUB_INDEX},unsharp=${SHARPEN},format=yuv420p" \
            -c:v "$ENCODER" \
            $ENC_PARAMS \
            -tag:v hvc1 \
            -movflags +faststart \
            -c:a aac \
            -b:a "$AUDIO_BITRATE" \
            -ac 2 \
            -map 0:v:0 \
            -map "$JPN_MAP" \
            -map_chapters 0 \
            -metadata title="$TITLE" \
            "${OUTPUT_DIR}/${BASENAME}.mp4"
    fi
    EXIT_CODE=$?
    set -e

    if [[ $EXIT_CODE -eq 0 ]]; then
        echo "[$FILENAME] Done > ${OUTPUT_DIR}/${BASENAME}.mp4"
        (( ENCODED++ )) || true
    else
        echo "[$FILENAME] FAILED - check output above for errors"
        (( FAILED++ )) || true
    fi

    echo
done

echo "============================================================"
echo " Finished!  Encoder: $ENC_LABEL"
echo " Encoded: $ENCODED   Failed: $FAILED   Skipped: $SKIPPED"
echo "============================================================"