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
- FFMpeg - H.265/HEVC Video Encoding Guide
- FFmpeg Ticket #4284 - FFmpeg doesn't pass -x265-params to the x265 encoder correctly.
First published: 2nd December 2023