Last active
February 11, 2017 11:51
-
-
Save ragboy/71fa13cb31f32d2af2a4295be896e65b to your computer and use it in GitHub Desktop.
This file contains hidden or 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/php | |
<?php | |
//this requires php-ffmpeg installed with composer | |
//https://github.com/PHP-FFMpeg/PHP-FFMpeg | |
//requires handbrake for cropping and ffmpeg 3.0+ with lib_fdk and eac3 easily installed with brew. | |
require_once 'vendor/autoload.php'; | |
date_default_timezone_set('America/Los_Angeles'); | |
$cloud = false; | |
$home_dir = getenv('HOME'); | |
$log_file = "$home_dir/mkv-2-m4v.log"; | |
//set path | |
$sh_path = getenv("PATH"); | |
$res = putenv("PATH=$home_dir/bin:/usr/local/bin:$sh_path"); | |
$args = getopt("i:n:c:q:"); | |
if (isset($args['i'])) | |
{ | |
$tmp = $args['i']; | |
if (!file_exists($tmp)) { | |
echo "Error: Input file ($tmp) does not exist.\n"; | |
return 1; | |
} | |
$file_in = new stdClass(); | |
$file_in->param = $args['i']; | |
$file_in->splinfo = new SplFileInfo($args['i']); | |
$work_dir = $file_in->splinfo->getPath(); | |
$ext = $file_in->splinfo->getExtension(); | |
$work_tmp = "$work_dir/tmp"; | |
//create tmp dir in mkv path for output | |
if (!checkdir("$work_tmp")) { | |
$message = "ERROR: Unable to create tmp dir for output ($work_tmp)"; | |
echo $message."\n"; | |
} | |
$log_file = "$work_dir/mkv-2-m4v.log"; | |
} | |
else { | |
echo <<<EOT | |
Usage: | |
-i for input file | |
-n for name of file, default to input file name | |
EOT; | |
return 1; | |
} | |
//get quality arg | |
if (isset($args['q'])) | |
{ | |
$qual_arg = $args['q']; | |
} | |
else { | |
$qual_arg = '18'; | |
} | |
if (isset($args['n'])) | |
{ | |
$out_name = $args['n']; | |
$file_out = new stdClass(); | |
$file_out->param = "$out_name.mkv"; | |
$file_out->splinfo = new SplFileInfo("$work_tmp/$out_name.mkv"); | |
} | |
else { | |
$ext = $file_in->splinfo->getExtension(); | |
$out_name = $file_in->splinfo->getBasename(".$ext"); | |
$file_out = new stdClass(); | |
$file_out->param = "$out_name.mkv"; | |
$file_out->splinfo = new SplFileInfo("$work_tmp/$out_name.mkv"); | |
} | |
//test ffprobe | |
$ffprobe = FFMpeg\FFProbe::create(); | |
$movie = $ffprobe->format($file_in->splinfo); | |
$vid_streams = $ffprobe->streams($file_in->splinfo)->videos(); | |
$audio_streams = $ffprobe->streams($file_in->splinfo)->audios(); | |
$sub_streams = $ffprobe->streams($file_in->splinfo)->all(); | |
try { | |
//first lets get the contents of mkv | |
$cmd = "mkvmerge --identify-verbose \"$file_in->splinfo\""; | |
$mkv_identify = shell_exec($cmd); | |
file_put_contents("$work_tmp/mkv-identify.txt", $mkv_identify); | |
$tracks_v = null; | |
$tracks_a = null; | |
$tracks_s = null; | |
if ($mkv_identify <> null && substr_count($mkv_identify, 'Matroska')) { | |
//find tracks | |
$pattern = '/^Track ID (\d): video \((.*)\).*display_dimensions:(\d{1,4})x(\d{1,4}).*]/m'; | |
$res = preg_match_all($pattern, $mkv_identify, $matches); | |
if ($res) { | |
$tracks_v = new stdClass(); | |
$tracks_v->line = $matches[0]; | |
$tracks_v->ids = $matches[1]; | |
$tracks_v->codec = $matches[2]; | |
$tracks_v->width = $matches[3]; | |
$tracks_v->height = $matches[4]; | |
} | |
$pattern = '/^Track ID (\d): audio \((.*)\) \[.*audio_channels:(\d).*\]/m'; | |
$res = preg_match_all($pattern, $mkv_identify, $matches); | |
if ($res) { | |
$tracks_a = new stdClass(); | |
$tracks_a->line = $matches[0]; | |
$tracks_a->ids = $matches[1]; | |
$tracks_a->codec = $matches[2]; | |
$tracks_a->channels = $matches[3]; | |
} | |
$pattern = '/^Track ID (\d): subtitles \((.*)\) (\[.*\])/m'; | |
$res = preg_match_all($pattern, $mkv_identify, $matches); | |
if ($res) { | |
$tracks_s = new stdClass(); | |
$tracks_s->line = $matches[0]; | |
$tracks_s->ids = $matches[1]; | |
$tracks_s->codec = $matches[2]; | |
$tracks_s->data = $matches[3]; | |
} | |
} | |
else { | |
throw new Exception("Unable to identify mkv file ($file_in->splinfo)"); | |
} | |
if (!$tracks_v) { | |
throw new Exception("No video tracks found, exiting."); | |
} | |
if (!$tracks_a) { | |
throw new Exception("No audio tracks found, exiting."); | |
} | |
//extract each flac and ac3 track | |
$audio_files = array(); | |
for ($i = 0; $i < count($tracks_a->ids); $i++) { | |
$ext = strtolower(explode("/", $tracks_a->codec[$i])[0]); | |
if ($ext == 'ac-3') | |
$ext = 'ac3'; | |
if ($tracks_a->channels[$i] > 2) { | |
$tmp_out_file = "$work_tmp/hdaudio_" . $i . ".$ext"; | |
} | |
else { | |
$tmp_out_file = "$work_tmp/audio_" . $i . ".$ext"; | |
} | |
$cmd = "mkvextract tracks \"$file_in->splinfo\" " . $tracks_a->ids[$i] . ":\"$tmp_out_file\""; | |
$cmd_rtn = runit($cmd); | |
if ($cmd_rtn == 0) { | |
$audio_files[] = $tmp_out_file; | |
} | |
} | |
$tracks_a->files = $audio_files; | |
$audio_files = null; | |
//extract each PGS track | |
$subs_files = array(); | |
for ($i = 0; $i < count($tracks_s->ids); $i++) { | |
$ext = strtolower($tracks_s->codec[$i]); | |
if ($ext == 'hdmv pgs') | |
$ext = 'pgs'; | |
$cmd = "mkvextract tracks \"$file_in->splinfo\" " . $tracks_s->ids[$i] . ":\"$work_tmp/hdsub_" . $i . ".$ext\""; | |
$cmd_rtn = runit($cmd); | |
if ($cmd_rtn == 0) { | |
$subs_files[] = "$work_tmp/hdsub_" . $i . ".$ext"; | |
} | |
} | |
$tracks_s->files = $subs_files; | |
$subs_files = null; | |
//now make aac audio from audio, we will use xld | |
$audio_out_files = array(); | |
$wait_for_ec3 = array(); | |
foreach ($tracks_a->files as $audio_file) { | |
$path_parts = pathinfo($audio_file); | |
$tmp_in = $audio_file; | |
$tmp_out = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.m4a'; | |
if ($path_parts['extension'] == 'flac') { | |
switch (flac_get_channels($audio_file)) { | |
//@TODO we need to actually parse the source layout, instaead of assuming 8 is 7.1 ad 7 is 6.1 | |
//Channel layout reference: https://developer.apple.com/library/mac/documentation/MusicAudio/Reference/CoreAudioDataTypesRef/#//apple_ref/c/tdef/AudioChannelLayoutTag | |
//https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html | |
//https://github.com/nu774/qaac/wiki/Multichannel--handling | |
//https://www.ffmpeg.org/ffmpeg-utils.html#channel-layout-syntax | |
case '8': | |
$message = "Detected 8 Channels (Assuming 7.1), settings layouts accordingly."; | |
logit($message); | |
$layout_args = ' -l MPEG_7_1_A -l MPEG_7_1_B'; | |
//first we need to get flac to wav | |
$tmp_wav = $path_parts['filename'] . ".mov"; | |
$cmd = "ffmpeg -i \"$tmp_in\" -y -c:a pcm_s24le \"$work_tmp/$tmp_wav\""; | |
runit($cmd); | |
$tmp_out = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.ec3'; | |
$wait_for_ec3[] = $tmp_out; | |
/*$cmd = "afconvert -f m4af -o \"$tmp_out\" -s 3 -q 127 -v$layout_args \"$work_tmp/$tmp_wav\""; | |
runit($cmd); | |
$cmd = "rm -rfv \"$work_tmp/$tmp_wav\""; | |
runit($cmd);*/ | |
break; | |
case '7': | |
$message = "Detected 7 Channels (Assuming 6.1), settings layouts accordingly."; | |
logit($message); | |
$layout_args = ' -l MPEG_6_1_A -l AAC_6_1'; | |
//first we need to get flac to wav | |
$tmp_wav = $path_parts['filename'] . ".mov"; | |
$cmd = "ffmpeg -i \"$tmp_in\" -y -channel_layout \"FL+FR+FC+LFE+BL+BR+BC\" -c:a pcm_s24le \"$work_tmp/$tmp_wav\""; | |
runit($cmd); | |
$tmp_out = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.ec3'; | |
$wait_for_ec3[] = $tmp_out; | |
/*$cmd = "afconvert -f m4af -o \"$tmp_out\" -s 3 -q 127 -v$layout_args \"$work_tmp/$tmp_wav\""; | |
runit($cmd); | |
$cmd = "rm -rfv \"$work_tmp/$tmp_wav\""; | |
runit($cmd);*/ | |
break; | |
case '6': | |
$message = "Detected 6 Channels, use ffmpeg to do eac3."; | |
logit($message); | |
$tmp_out = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.eac3'; | |
$cmd = "ffmpeg -i \"$tmp_in\" -y -c:a eac3 -ab 1024k \"$tmp_out\""; | |
$res = runit($cmd); | |
break; | |
default: | |
$layout_args = ''; | |
break; | |
} | |
} | |
else { | |
$cmd = "ffmpeg -i \"$tmp_in\" -y -c:a libfdk_aac -vbr 4 \"$tmp_out\""; | |
$res = runit($cmd); | |
} | |
$audio_out_files[] = $tmp_out; | |
} | |
$tracks_a->out_files = $audio_out_files; | |
$audio_out_files = null; | |
//lets pull out the chapters | |
$pattern = '/^Chapters: (\d*)/m'; | |
$res = preg_match_all($pattern, $mkv_identify, $matches); | |
if ($res) | |
{ | |
$message = "Extracting chapters to a file, chapters.xml"; | |
logit($message); | |
$cmd = "mkvextract chapters \"$file_in->splinfo\" > \"$work_tmp/chapters.xml\""; | |
$res = runit($cmd); | |
} | |
//lets move the ec3 files to a watch folder | |
//get crop arg | |
if (isset($args['c'])) | |
{ | |
$crop_arg = " -filter:v \"crop=".$args['c']."\""; | |
$message = "Completed Crop Input Parse: $crop_arg."; | |
logit($message); | |
} | |
else { | |
//lets detect crop | |
$cmd = "HandBrakeCLI --scan -i \"$file_in->splinfo\" - 2>&1 | grep crop"; | |
$res = system($cmd); | |
preg_match('|\+ autocrop: (\d{1,4}/\d{1,4}/\d{1,4}/\d{1,4})|',$res,$match); | |
$crop_vals = explode('/',$match[1]); | |
$crop_w = $tracks_v->width[0] - ($crop_vals[2] + $crop_vals[3]); | |
$crop_h = $tracks_v->height[0] - ($crop_vals[0] + $crop_vals[1]); | |
$crop_x = $crop_vals[2]; | |
$crop_y = $crop_vals[0]; | |
$crop_arg = " -filter:v \"crop=$crop_w:$crop_h:$crop_x:$crop_y\""; | |
$message = "Completed Crop Calculation: $crop_arg."; | |
logit($message); | |
} | |
//now transcode video | |
$qual_arg = " -crf $qual_arg"; | |
$out = $work_tmp.'/'.'video'.'.h264'; | |
$cmd = "ffmpeg -y -i \"$file_in->splinfo\"$crop_arg -c:v libx264 -preset veryfast -profile:v high -level 4.2$qual_arg -maxrate 15000k -an \"$out\""; | |
$res = runit($cmd); | |
$message = "Completed video transcoding."; | |
logit($message); | |
//before we put this in one clean mkv, we have to wait to make sure the EC3 file is done encoding until we can do INLINE. | |
if ($wait_for_ec3) { | |
end($wait_for_ec3); | |
while (!file_exists($wait_for_ec3[key($wait_for_ec3)])) | |
{ | |
logit("Waiting for EC3 files to be available: " . $wait_for_ec3[key($wait_for_ec3)]); | |
sleep(2); | |
} | |
} | |
//nows lets put in one clean mkv | |
$cmd = "mkvmerge --default-language eng -o \"$file_out->splinfo\" \"$work_tmp/video.h264\""; | |
foreach ($tracks_a->out_files as $audio_file) { | |
$cmd .= " \"$audio_file\""; | |
} | |
foreach ($tracks_s->files as $sub_file) { | |
$cmd .= " \"$sub_file\""; | |
} | |
if(file_exists("$work_tmp/chapters.xml")) | |
{ | |
$cmd .= " --chapters \"$work_tmp/chapters.xml\""; | |
} | |
$res = runit($cmd); | |
//now lets save and clear the log | |
$cmd = "mv \"$log_file\" \"$work_tmp/mkv-2-m4v.log\""; | |
passthru($cmd); | |
} catch (Exception $e) { | |
logit($e->getMessage()); | |
} | |
//clean up | |
//$cmd = "rm -rfv $work_tmp"; | |
//$cmd_rtn = runit($cmd); | |
function flac_get_channels($file) { | |
$cmd = "metaflac --show-channels \"$file\""; | |
$res = shell_exec($cmd); | |
return trim($res); | |
} | |
function runit($cmd,$logit=true) { | |
global $log_file; | |
global $home_user; | |
$cmd_rtn = 0; | |
if ($logit) { | |
logit('Starting CMD: '.$cmd); | |
passthru("$cmd 2>&1 | tee -a $log_file",$cmd_rtn); | |
} | |
else { | |
passthru("$cmd",$cmd_rtn); | |
} | |
return $cmd_rtn; | |
} | |
function logit($message) { | |
global $log_file; | |
global $cloud; | |
$log_date = new DateTime(); | |
$log_date_txt = $log_date->format('Y-m-d H:i:s'); | |
$log_text = "[ $log_date_txt ] $message\n"; | |
file_put_contents($log_file, $log_text, FILE_APPEND | LOCK_EX); | |
if ($cloud === false) { | |
echo $log_text; | |
} | |
} | |
function checkdir($path) { | |
$rtn = false; | |
if (!file_exists($path)) | |
{ | |
return mkdir($path,0777,true); | |
} | |
elseif (file_exists($path) && !is_dir($path)) | |
{ | |
unlink($path); | |
return mkdir($path,0777,true); | |
} | |
elseif (file_exists($path) && is_dir($path)) | |
{ | |
return true; | |
} | |
return $rtn; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment