Skip to content

Instantly share code, notes, and snippets.

@razielgn
Last active December 11, 2015 18:38
Show Gist options
  • Save razielgn/4642389 to your computer and use it in GitHub Desktop.
Save razielgn/4642389 to your computer and use it in GitHub Desktop.
Remux h264 MKV into MP4 and transcode audio to AAC if needed.
#!/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
@razielgn
Copy link
Author

Possible additional features:

  • Multiple audio tracks
  • Handling subtitles

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment