MKV to MP4 Remuxing Script
This is based off thewinchester's script posted in his answer here. I started off with his and it eventually morphed into a rewrite of most of it, but the concept is the same.
The basic function is to take an MKV file containing h.264 video and some audio and convert it to an MP4/M4V file.
My goal is to make the resulting files compatible with the iPad and AppleTV, but it should work with anything that can handle MP4/M4V files. Video is not transcoded, so quality remains the same, audio is converted if necessary, but higher quality tracks are kept if possible.
Changes
A partial list of the changes I've made:
- Handling multiple files or directories passed as arguments
- Uses (and requires) the higher quality NeroAAC to encode AAC audio from AC3 or DTS
- Handling multiple audio tracks in accordance with the AppleTV standard to allow for proper playback and surround sound on AppleTV:
- 160kB/s stereo AAC
- (Optional) AC-3 (typically surround sound) as the second track, disabled
- Converting DTS to AC-3
- Handling multiple tracks regardless of order (i.e. AAC, Video, AC3)
- Various coding changes to make it easier to modify later
Requirements
The script requires the aforementioned NeroAAC, as well as Aften (AC3 encode) and libdca/dcadec (DTS decode). As with the original script, ffmpeg is also required.
Known Issues
The script is fairly bulletproof - if it runs into something it can't deal with, it's typically ignored or quits. The original files are left intact, so there's little to no chance of data loss.
I've used it on a lot of my media library and many of the changes have been to fix bugs I've run into.
That said, there are a few things to be aware of, and room for improvement:
- Handling multiple audio tracks of the same type - 1 AC3 and 1 AAC is fine, but 2 AAC tracks won't work. This shouldn't come up often, but keep it in mind.
- No support for non-audio/video tracks. Subtitles, chapters and artwork are all ignored.
- No detection of surround sound AAC. The script copies any AAC track, it won't down mix to stereo. Technically for iPad and AppleTV support, the main audio track should be stereo AAC, but I think in most cases an iPad or AppleTV can handle it gracefully.
Code and use
I'm posting it here hoping that someone will find it useful - please feel free to use and modify as you see fit for non-commercial uses. If you come up with an interesting modification it might be nice if you shared it here or elsewhere.
#!/bin/bash
#Close stdin - avoid accidental keypresses causing problems
exec 0>&-
# Find MKV files
for file in "$@";
do
find "$file" -type f -not -name ".*" | grep .mkv$ | while read file
do
fileProper=$(readlink -f "$file") # full path of file
pathNoExt=${fileProper%.*} # full path minus extension
#Check if M4V already exists
if [ -f "$pathNoExt".m4v ]; then
echo "M4V already exists, stopping"
else
# Get number of tracks
numberOfTracks=`mkvmerge -i "$fileProper" | grep "Track ID" | wc -l`
echo "Found $numberOfTracks Tracks"
# Set base extraction command
extractCmd+=(mkvextract tracks "$fileProper")
# Determine type of tracks
for (( i=1; i<=$numberOfTracks; i++ ))
do
trackType=`mkvmerge -i "$fileProper" | grep "Track ID $i" | sed -e 's/^.*: //'`
if [[ "$trackType" == *video* ]]; then
echo "Track $i is Video"
extractCmd+=( $i:"$pathNoExt".264)
fps=`mkvinfo "$fileProper" | grep duration | sed -e 's/.*(//' -e 's/f.*//' | sed -n ${i}p`
elif [[ "$trackType" == "audio (A_AAC)" ]]; then
echo "Track $i is AAC"
extractCmd+=( $i:"$pathNoExt".aac)
elif [[ "$trackType" == "audio (A_AC3)" ]]; then
echo "Track $i is AC3"
extractCmd+=( $i:"$pathNoExt".ac3)
elif [[ "$trackType" == "audio (A_DTS)" ]]; then
echo "Track $i is DTS"
extractCmd+=( $i:"$pathNoExt".dts)
fi
# Insert cases for handling other audio and non-AV tracks here
done
"${extractCmd[@]}" # Extract Tracks
# Check files and encode audio if neccessary
if [ -f "$pathNoExt".264 ]; then
# Video file exists
mp4BoxCmd+=(MP4Box -new "$pathNoExt".m4v -add "$pathNoExt".264 -fps $fps)
if [ -f "$pathNoExt".aac ]; then
# AAC exists
mp4BoxCmd+=( -add "$pathNoExt".aac)
if [ -f "$pathNoExt".ac3 ]; then
mp4BoxCmd+=( -add "$pathNoExt".ac3:disable)
elif [ -f "$pathNoExt".dts ]; then
# Encode DTS to AC3
dcadec -o wavall "$pathNoExt".dts | aften -v 0 - "$pathNoExt".ac3
mp4BoxCmd+=( -add "$pathNoExt".ac3:disable)
fi
else # Encode AAC from AC3 or DTS
if [ -f "$pathNoExt".ac3 ]; then
ffmpeg -i "$pathNoExt".ac3 -acodec pcm_s16le -ac 2 -f wav - | neroAacEnc -lc -br 160000 -ignorelength -if - -of "$pathNoExt".aac
mp4BoxCmd+=( -add "$pathNoExt".aac -add "$pathNoExt".ac3:disable)
elif [ -f "$pathNoExt".dts ]; then
ffmpeg -i "$pathNoExt".dts -acodec pcm_s16le -ac 2 -f wav - | neroAacEnc -lc -br 160000 -ignorelength -if - -of "$pathNoExt".aac
# Encode DTS to AC3
dcadec -o wavall "$pathNoExt".dts | aften -v 0 - "$pathNoExt".ac3
mp4BoxCmd+=( -add "$pathNoExt".aac -add "$pathNoExt".ac3:disable)
else
echo "Warning: no audio file found"
fi
fi
# Create m4v
"${mp4BoxCmd[@]}"
else
echo "Error: no video file found"
fi
#remove temporary track files
rm -f "$pathNoExt".aac "$pathNoExt".dts "$pathNoExt".ac3 "$pathNoExt".264 "$pathNoExt".wav
fi
done
done
Here are two PowerShell scripts to split long videos into smaller chapters by black scenes .
Save them as Detect_black.ps1 and Cut_black.ps1. Download ffmpeg for Windows and tell the script the path to your ffmpeg.exe and your video folder under the option section.
Both scripts won't touch existing video files, they remain untouched.
However, you will get a couple of new files at the same place where your input videos are
- A logfile per video with the console output for both used ffmpeg commands
- A CSV file per video with all timestamps of black scenes for manual fine tuning
- A couple of new videos depending on how many black scenes are previously detected
First script to run: Detect_black.ps1
### Options __________________________________________________________________________________________________________
$ffmpeg = ".\ffmpeg.exe" # Set path to your ffmpeg.exe; Build Version: git-45581ed (2014-02-16)
$folder = ".\Videos\*" # Set path to your video folder; '\*' must be appended
$filter = @("*.mov","*.mp4") # Set which file extensions should be processed
$dur = 4 # Set the minimum detected black duration (in seconds)
$pic = 0.98 # Set the threshold for considering a picture as "black" (in percent)
$pix = 0.15 # Set the threshold for considering a pixel "black" (in luminance)
### Main Program ______________________________________________________________________________________________________
foreach ($video in dir $folder -include $filter -exclude "*_???.*" -r){
### Set path to logfile
$logfile = "$($video.FullName)_ffmpeg.log"
### analyse each video with ffmpeg and search for black scenes
& $ffmpeg -i $video -vf blackdetect=d=`"$dur`":pic_th=`"$pic`":pix_th=`"$pix`" -an -f null - 2> $logfile
### Use regex to extract timings from logfile
$report = @()
Select-String 'black_start:.*black_end:' $logfile | % {
$black = "" | Select start, end, cut
# extract start time of black scene
$start_s = $_.line -match '(?<=black_start:)\S*(?= black_end:)' | % {$matches[0]}
$start_ts = [timespan]::fromseconds($start_s)
$black.start = "{0:HH:mm:ss.fff}" -f ([datetime]$start_ts.Ticks)
# extract duration of black scene
$end_s = $_.line -match '(?<=black_end:)\S*(?= black_duration:)' | % {$matches[0]}
$end_ts = [timespan]::fromseconds($end_s)
$black.end = "{0:HH:mm:ss.fff}" -f ([datetime]$end_ts.Ticks)
# calculate cut point: black start time + black duration / 2
$cut_s = ([double]$start_s + [double]$end_s) / 2
$cut_ts = [timespan]::fromseconds($cut_s)
$black.cut = "{0:HH:mm:ss.fff}" -f ([datetime]$cut_ts.Ticks)
$report += $black
}
### Write start time, duration and the cut point for each black scene to a seperate CSV
$report | Export-Csv -path "$($video.FullName)_cutpoints.csv" –NoTypeInformation
}
How does it work
The first script iterates through all video files which matches a specified extension and doesn't match the pattern *_???.*
, since new video chapters were named <filename>_###.<ext>
and we want to exclude them.
It searches all black scenes and writes the start timestamp and black scene duration to a new CSV file named <video_name>_cutpoints.txt
It also calculates cut points as shown: cutpoint = black_start + black_duration / 2
. Later, the video gets segmented at these timestamps.
The cutpoints.txt file for your sample video would show:
start end cut
00:03:56.908 00:04:02.247 00:03:59.578
00:08:02.525 00:08:10.233 00:08:06.379
After a run, you can manipulate the cut points manually if wished. If you run the script again, all old content gets overwritten. Be careful when manually editing and save your work elsewhere.
For the sample video the ffmpeg command to detect black scenes is
$ffmpeg -i "Tape_10_3b.mp4" -vf blackdetect=d=4:pic_th=0.98:pix_th=0.15 -an -f null
There are 3 important numbers which are editable in the script's option section
d=4
means only black scenes longer than 4 seconds are detected
pic_th=0.98
is the threshold for considering a picture as "black" (in percent)
pix=0.15
sets the threshold for considering a pixel as "black" (in luminance). Since you have old VHS videos, you don't have completely black scenes in your videos. The default value 10 won't work and I had to increase the threshold slightly
If anything goes wrong, check the corresponding logfile called <video_name>__ffmpeg.log
. If the following lines are missing, increase the numbers mentioned above until you detect all black scenes:
[blackdetect @ 0286ec80]
black_start:236.908 black_end:242.247 black_duration:5.33877
Second script to run: cut_black.ps1
### Options __________________________________________________________________________________________________________
$ffmpeg = ".\ffmpeg.exe" # Set path to your ffmpeg.exe; Build Version: git-45581ed (2014-02-16)
$folder = ".\Videos\*" # Set path to your video folder; '\*' must be appended
$filter = @("*.mov","*.mp4") # Set which file extensions should be processed
### Main Program ______________________________________________________________________________________________________
foreach ($video in dir $folder -include $filter -exclude "*_???.*" -r){
### Set path to logfile
$logfile = "$($video.FullName)_ffmpeg.log"
### Read in all cutpoints from *_cutpoints.csv; concat to string e.g "00:03:23.014,00:06:32.289,..."
$cuts = ( Import-Csv "$($video.FullName)_cutpoints.csv" | % {$_.cut} ) -join ","
### put together the correct new name, "%03d" is a generic number placeholder for ffmpeg
$output = $video.directory.Fullname + "\" + $video.basename + "_%03d" + $video.extension
### use ffmpeg to split current video in parts according to their cut points
& $ffmpeg -i $video -f segment -segment_times $cuts -c copy -map 0 $output 2> $logfile
}
How does it work
The second script iterates over all video files in the same way the first script has done. It reads in only the cut timestamps from the corresponding cutpoints.txt
of a video.
Next, it puts together a suitable filename for chapter files and tells ffmpeg to segment the video. Currently the videos are sliced without re-encoding (superfast and lossless). Due to this, there might be 1-2s inaccuracy with cut point timestamps because ffmpeg can only cut at key_frames. Since we just copy and don't re-encode, we cannot insert key_frames on our own.
The command for the sample video would be
$ffmpeg -i "Tape_10_3b.mp4" -f segment -segment_times "00:03:59.578,00:08:06.379" -c copy -map 0 "Tape_10_3b_(%03d).mp4"
If anything goes wrong, have a look at the corresponding ffmpeg.log
References
Todo
Ask OP if CSV format is better than a text file as cut point file, so you can edit them with Excel a little bit easier
» Implemented
Implement a way to format timestamps as [hh]:[mm]:[ss],[milliseconds] rather than only seconds
» Implemented
Implement a ffmpeg command to create mosaik png files for each chapter
» Implemented
Elaborate if -c copy
is enough for OP's scenario or of we need to fully re-encode.
Seems like Ryan is already on it.
Best Answer
I’ve found the HiLight tags: they are stored in the MP4 files themselves.
In particular, the tags are stored in a box with type
HMMT
in the User Data Box (udta
) of the Movie Box (moov
) of the MPEG-4 container. See ISO/IEC 14496-12 for details on these “boxes”.The
HMMT
box seems to be a non-standard (GoPro-specific) ISO/IEC 14496-12 box. Its data consists of one or more 32-bit integers. The first integer contains the number of available HiLight tags. All subsequent integers resemble an ordered list of HiLight tags. Each HiLight tag is represented as a millisecond value.