Convert MKV to MP4 on macOS

published on in category macos , Tags: macos ffmpeg

Table of contents

I recently started to convert all of my movies from MKV to MP4. The main reason for this is that I want to be able to play back videos from all my Apple devices without the need for additional software, like VLC or IINA.

For many people not that deep into video file formats I want to explain what the supported formats for video on macOS are and how to get there from almost any source material.

First off, neither MKV nor MP4 are video codecs. Instead, both are video container formats. Means that you can put multiple streams of data (video, audio, subtitles) with different codecs into a single MKV or MP4 file. The streams themselves are encoded using their respective codecs like H.264 for video or AAC for audio.

macOS can natively decode different types of video and audio streams, here is what I prefer and what I want my video files to look like:

  • Video: H.265 (on newer Macs or iDevices > 2016) or H.264
  • Audio: AAC (since it’s Apple native) or AC3

ffmpeg, besides having a cryptic command line interface, makes it relatively easy to get from anything to MP4. But there are some things to consider: Almost all video codecs are lossy. This means that every time you re-encode video, you will lose some information, normally ending up in worse quality. So the key is to either always directly encode from the original material (e.g. from your Bluray to H.264) or leaving the already encoded material untouched if possible. If you’re lucky and the codecs for all the streams inside the container can be natively read by macOS, you can basically just copy the streams from one container format to another one. This is a lossless operation and only takes couple of seconds compared to actually re-encoding video or audio streams which often takes couple of minutes to hours, depending on your source material, desired quality and compute power.

I’ve written a small shell script to help me convert my videos from any source that ffmpeg supports to H.264 and any supported audio format, taking into account the codecs used in the source material, to do as few re-encodes as possible.

#!/usr/bin/env bash
set -eu

video_codec() {
    # Let ffprobe export the bits of information we need to environment variables
    # Select the first video stream (v:0) and get the codec_name property
    eval $(ffprobe -v error -of flat=s=_ -select_streams v:0 -show_entries stream=codec_name $1)
    echo $streams_stream_0_codec_name
}

audio_codec() {
    eval $(ffprobe -v error -of flat=s=_ -select_streams a:0 -show_entries stream=codec_name $1)
    echo $streams_stream_0_codec_name
}


# Fix looping for filenames with spaces inside
IFS='\\'

# Iterate over all arguments passed to the script
while test ${#} -gt 0
do
    # Assemble the ffmpeg command, the first part is always the same
    # -map 0:m:language:ger -> Only select german audio tracks
    # -map_metadata 0 -> Do not map metadata
    # -sn -> Do not copy subtitles
    FFMPEG_CMD="ffmpeg -i $1 -map 0 -map 0:m:language:ger -map_metadata 0 -sn "

    if [[ "$(video_codec $1)" == "h264" ]]
    then
        # If the primary video stream is h264 already, there is no need to re-encode it
        FFMPEG_CMD+="-c:v copy "
    else
        # Otherwise use h264_videotoolbox (which has hardware-encoding support on macOS)
        # with the desired quality options to re-encode the video streams
        FFMPEG_CMD+="-c:v h264_videotoolbox -profile high -level 4.2 "
    fi

    if [[ "$(audio_codec $1)" == "aac" ]]
    then
        # Simple copy all audio streams if they're AAC
        FFMPEG_CMD+="-c:a copy "
    else
        # Otherwise re-encode it to AAC with 320k bitrate
        FFMPEG_CMD+="-c:a aac -b:a 320k "
    fi

    # Replace the original extension witih mp4 for the target file
    FFMPEG_CMD+="${1%.*}.mp4"

    # Execute the command assembled before
    eval $FFMPEG_CMD

    # Move to the next file
    shift
done