Skip to content

Instantly share code, notes, and snippets.

@ragboy
Last active February 11, 2017 11:51
Show Gist options
  • Save ragboy/71fa13cb31f32d2af2a4295be896e65b to your computer and use it in GitHub Desktop.
Save ragboy/71fa13cb31f32d2af2a4295be896e65b to your computer and use it in GitHub Desktop.
#!/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