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

This blog is created by Stuart Page

I'm a freelance web developer and technology consultant based in Surrey, UK, with over 10 years experience in web development, DevOps, Linux Administration, and IT solutions.

Need support with your infrastructure or web services?

Get in touch