Programster's Blog

Tutorials focusing on Linux, programming, and open-source

FFmpeg - HEVC Encoding

Two Pass Encoding

Two-pass encoding produces the best results, but takes longer. I always recommend performing two-pass encoding whenever possible.

Basic Version

Below is a basic version of a two-pass encode that targets an average bitrate for the video.

INPUT_FILE="/path/to/input.mkv"
OUTPUT_FILENAME="/path/to/output.mkv"
VIDEO_BITRATE="3000K"

ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -b:v $VIDEO_BITRATE \
  -x265-params "pass=1" \
  -preset medium \
  -f mp4 /dev/null \
  && \
  ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -b:v $VIDEO_BITRATE \
  -x265-params "pass=2" \
  -preset medium \
  $OUTPUT_FILENAME

Single Threaded

If you wish to modify the command, so that ffmpeg will only use one thread, then one needs to pass the -threads option along with specifying the number of pools.

INPUT_FILE="/path/to/input.mkv"
OUTPUT_FILENAME="/path/to/output.mkv"
VIDEO_BITRATE="3000K"

ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -b:v $VIDEO_BITRATE \
  -x265-params "pass=1:pools=1" \
  -threads 1 \
  -preset medium \
  -f mp4 /dev/null \
  && \
  ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -b:v $VIDEO_BITRATE \
  -x265-params "pass=2:pools=1" \
  -threads 1 \
  -preset medium \
  $OUTPUT_FILENAME

Copy Audio

The previous commands will convert the audio, which may be required. If you wish to just copy the audio stream, then specify -c:a copy like so:

INPUT_FILE="/path/to/input.mkv"
OUTPUT_FILENAME="/path/to/output.mkv"
VIDEO_BITRATE="3000K"

ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -b:v $VIDEO_BITRATE \
  -x265-params "pass=1" \
  -preset medium \
  -c:a copy \
  -f mp4 /dev/null \
  && \
  ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -b:v $VIDEO_BITRATE \
  -x265-params "pass=2" \
  -preset medium \
  -c:a copy \
  $OUTPUT_FILENAME

PHP Script

I have a server that I run parallel single-threaded encodes on, and I use the following PHP script to generate and run the FFmpeg commands, so I don't have to worry about funky filenames and path issues.

<?php

$inputFile = __DIR__ . '/' . 'input.mp4';
$desiredBitrate = 3000; // specify desired output kbps
$numThreads=1;


set_time_limit(0); // infinite time


function main(string $inputFile, int $desiredBitrate, int $numThreads)
{
    if (file_exists($inputFile) === false)
    {
        die("invalid input filename");
    }

    $parts = pathinfo($inputFile);
    $filename = $parts['filename']; // doesnt include extension
    $extension = $parts['extension'];
    $dir = $parts['dirname'];

    # Write the original filename to a file in case we need to recover.
    file_put_contents("{$dir}/original-filename.txt", "original filename: {$filename}.{$extension}" . PHP_EOL);

    $fullOutputFilename = "{$dir}/{$filename}.hevc.{$desiredBitrate}kbps.{$extension}";
    $tempInputName = "{$dir}/temp.{$extension}";
    $tempOutputName = "{$dir}/temp.hevc.{$desiredBitrate}kbps.{$extension}";

    print "renaming to temporary input filename for ffmpeg command." . PHP_EOL;
    rename($inputFile, $tempInputName);


    $pass1CmdParts = [];
    $pass1CmdParts[] = "ffmpeg -i {$tempInputName}";
    $pass1CmdParts[] = "-c:v libx265";
    $pass1CmdParts[] = "-b:v {$desiredBitrate}K";
    $pass1CmdParts[] = "-x265-params \"pass=1:pools=1\"";
    $pass1CmdParts[] = "-threads {$numThreads}";
    $pass1CmdParts[] = "-preset medium";
    $pass1CmdParts[] = "-c:a copy";
    $pass1CmdParts[] = "-f mp4";
    $pass1CmdParts[] = "/dev/null";

    $pass2CmdParts = [];
    $pass2CmdParts[] = "ffmpeg -i {$tempInputName}";
    $pass2CmdParts[] = "-c:v libx265";
    $pass2CmdParts[] = "-b:v {$desiredBitrate}K";
    $pass2CmdParts[] = "-x265-params \"pass=2:pools=1\"";
    $pass2CmdParts[] = "-threads {$numThreads}";
    $pass2CmdParts[] = "-preset medium";
    $pass2CmdParts[] = "-c:a copy";
    $pass2CmdParts[] = $tempOutputName;

    $cmd1 = implode(" ", $pass1CmdParts);
    $cmd2 = implode(" ", $pass2CmdParts);

    $cmd = "{$cmd1} && {$cmd2}";

    shell_exec($cmd);

    print "renaming from temporary output name to full output name." . PHP_EOL;
    rename($tempOutputName, $fullOutputFilename);
    rename($tempInputName, $inputFile);

    # This is where I would send myself a notification

    print PHP_EOL . "done!" . PHP_EOL;
}

main($inputFile, $desiredBitrate, $numThreads);

One Pass CRF Encode

The following command will perform a single-pass encode and try to maintain a certain percievable quality level using the constant-rate-factor (CRF). With CRF, the lower the number, the better the quality, but the filesize increases.

INPUT_FILE="/path/to/input.mp4"
OUTPUT_FILENAME="/path/to/output.mkv"
CRF=26

ffmpeg \
  -i $INPUT_FILE \
  -c:v libx265 \
  -crf $CRF \
  -preset fast \
  -c:a aac \
  -b:a 128k \
  $OUTPUT_FILENAME

HEVC - mp4 vs mkv Video Containers

The mp4 container can contain HEVC video streams, but they won't be able to play in most browsers (compatibility checker), so I prefer to change the container to mkv. This way, I don't make the mistake of thinking the video can just be put on the web.

References

Last updated: 2nd December 2023
First published: 2nd December 2023