Last active
December 11, 2015 18:38
-
-
Save razielgn/4642389 to your computer and use it in GitHub Desktop.
Remux h264 MKV into MP4 and transcode audio to AAC if needed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Requirements | |
# - mkvtoolnix | |
# - MP4Box | |
# - ffmpeg compiled with libfaac support (https://ffmpeg.org/trac/ffmpeg/wiki/UbuntuCompilationGuide has helped me) | |
# - mediainfo | |
require 'tmpdir' | |
require 'fileutils' | |
if ARGV[0].nil? || !File.exists?(ARGV[0]) | |
puts "Provide a valid input." | |
exit 1 | |
end | |
class CommandNotFoundError < StandardError; end | |
class InvalidMkvError < StandardError; end | |
class TranscodingUnsupportedError < StandardError; end | |
class Converter | |
attr_reader :file_path, :tmpdir | |
Track = Struct.new(:id, :type, :codec) do | |
def video?; type == 'video' end | |
def audio?; type == 'audio' end | |
def filename | |
"#{type}.#{extension}" | |
end | |
def extension | |
case codec | |
when 'V_MPEG4/ISO/AVC' then 'h264' | |
when 'A_AC3' then 'ac3' | |
when 'A_AAC' then 'aac' | |
when 'A_DTS' then 'dts' | |
else | |
'unknown' | |
end | |
end | |
def of_codec?(_codec) | |
codec == _codec | |
end | |
def path(dir) | |
File.join(dir, filename) | |
end | |
def label_for_extraction(dir) | |
"#{id}:#{path(dir)}" | |
end | |
end | |
MkvInfo = Struct.new(:file_path, :video_track, :audio_track) do | |
def tracks; [video_track, audio_track] end | |
end | |
DemInfo = Struct.new(:video_path, :audio_path) | |
def initialize(file_path) | |
@file_path = file_path | |
@tmpdir = Dir.mktmpdir | |
end | |
def perform! | |
check_for_dependencies! | |
mkv_info = identify_mkv(file_path) | |
demux_info = extract_mkv!(tmpdir, mkv_info) | |
possibly_transcode_audio!(mkv_info, demux_info) | |
mux_mp4 file_path, demux_info | |
end | |
def cleanup! | |
remove_dir! tmpdir | |
end | |
private | |
def mux_mp4(file_path, demux_info) | |
dest_path = file_path.gsub(/mkv$/, 'mp4') | |
fps = extract_fps(file_path) | |
system %{MP4Box -new -fps "#{fps}" -add "#{demux_info.video_path}" -add "#{demux_info.audio_path}" "#{dest_path}"} | |
end | |
def extract_fps(video_path) | |
output = %x{mediainfo "#{video_path}" | grep 'Frame rate' | grep 'fps'} | |
output.scan(/[0-9\.]+/).first | |
end | |
def identify_mkv(file_path) | |
output = %x{mkvmerge --identify "#{file_path}"} | |
puts output | |
raise InvalidMkvError if output =~ /^Error/ | |
tracks = output.scan(/Track ID (\d+): (\w+) \((.+)\)/).map do |(id, type, codec)| | |
Track.new(id, type, codec) | |
end | |
video_track = tracks.find(&:video?) | |
audio_track = tracks.find(&:audio?) | |
MkvInfo.new(file_path, video_track, audio_track) | |
end | |
def extract_mkv!(tmpdir, mkv_info) | |
video_track_label = mkv_info.video_track.label_for_extraction(tmpdir) | |
audio_track_label = mkv_info.audio_track.label_for_extraction(tmpdir) | |
system 'mkvextract', 'tracks', mkv_info.file_path, video_track_label, audio_track_label | |
video_track_path = mkv_info.video_track.path(tmpdir) | |
audio_track_path = mkv_info.audio_track.path(tmpdir) | |
DemInfo.new(video_track_path, audio_track_path) | |
end | |
def possibly_transcode_audio!(mkv_info, demux_info) | |
return if mkv_info.audio_track.of_codec? 'A_AAC' | |
new_audio_path = transcode_audio!(demux_info.audio_path, to: 'aac', channels: 2) | |
swap_audio_file!(demux_info, new_audio_path) | |
end | |
def transcode_audio!(audio_path, params = {}) | |
to = params.fetch(:to) | |
channels = params.fetch(:channels) | |
args = [%{ffmpeg -i "#{audio_path}"}] | |
codec = case to | |
when 'aac' then ['libfaac', '-ab 320k', "-ac #{channels}"] | |
else | |
raise TranscodingUnsupportedError, "Transcode for #{to} not yet implemented!" | |
end | |
args += ['-acodec'] + codec | |
dest_path = audio_path.gsub(/\.\w+$/, ".#{to}") | |
args << %{"#{dest_path}"} | |
puts args.join ' ' | |
system args.join ' ' | |
dest_path | |
end | |
def swap_audio_file!(demux_info, new_audio_path) | |
remove_file! demux_info.audio_path | |
demux_info.audio_path = new_audio_path | |
end | |
def remove_dir!(tmpdir) | |
FileUtils.rm_rf tmpdir | |
end | |
def remove_file!(file_path) | |
FileUtils.rm_f file_path | |
end | |
def check_for_dependencies! | |
check_for! 'mkvmerge' | |
check_for! 'mkvextract' | |
check_for! 'MP4Box' | |
check_for! 'ffmpeg' | |
check_for! 'mediainfo' | |
end | |
def check_for!(cmd) | |
`which #{cmd}` | |
raise CommandNotFoundError.new("#{cmd} not found") if not $?.success? | |
end | |
end | |
converter = Converter.new(ARGV[0]) | |
trap :INT do | |
puts 'Interrupted by the user' | |
exit 0 | |
end | |
begin | |
converter.perform! | |
rescue CommandNotFoundError, TranscodingUnsupportedError => ex | |
puts ex.message | |
exit 1 | |
rescue InvalidMkvError | |
exit 1 | |
ensure | |
converter.cleanup! | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Possible additional features: