Last active
May 17, 2019 15:23
-
-
Save divinity76/3bafc6516ecc54237d610ded03a8065d to your computer and use it in GitHub Desktop.
video cutter
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
<?php | |
declare (strict_types = 1); | |
stream_set_blocking(STDIN, true); | |
if ($argc >= 2) { | |
$in_file = $argv; | |
unset($in_file[0]); | |
$in_file = implode("", $in_file); | |
} else { | |
echo "choose path to media file: "; | |
$in_file = fgets(STDIN); | |
} | |
$in_file = trim(strtr($in_file, array('"' => '', "'" => ''))); | |
if (!is_file($in_file)) { | |
echo "error: \"{$in_file}\" is not a valid file!\n"; | |
pause(); | |
return 1; | |
} | |
$in_file = _cygwinify_filepath($in_file); | |
$in_file = realpath($in_file); | |
//var_dump($in_file); | |
echo "filepath parsed: \"{$in_file}\"\n"; | |
$dir = dirname($in_file); | |
if (!chdir($dir)) { | |
die("error: failed to chdir \"{$dir}\""); | |
} | |
$in_file_name = basename($in_file); | |
$total_secs = get_seconds($in_file); | |
$key_frames = get_key_frames($in_file); | |
echo "media length is \"{$total_secs}\" seconds (aka \"" . number_format($total_secs / 60, 1) . "\" minutes)\n"; | |
echo "number of keyframes: " . count($key_frames) . "\r\n"; | |
echo "write how many seconds you want each clip to be, and press enter: "; | |
$clip_sec = fgets(STDIN); | |
$clip_sec = trim($clip_sec); | |
if (false === ($clip_sec = filter_var($clip_sec, FILTER_VALIDATE_INT))) { | |
die("ERROR: could not parse input as number!"); | |
} | |
$secs = 0; | |
$i = 0; | |
for (;;) { | |
++$i; | |
$remaining = $total_secs - $secs; | |
if ($remaining <= 0) { | |
break; | |
} | |
$next = $secs + $clip_sec; | |
if($next<=0){ | |
break; | |
} | |
$next=get_next_keyframe($key_frames, $next, $total_secs); | |
$cmd = implode(" ", array( | |
"ffmpeg", | |
"-y", | |
"-noaccurate_seek", | |
"-i " . escapeshellarg(_uncygwinify_filepath($in_file)), | |
"-c:v copy", | |
"-c:a copy", | |
"-ss " . escapeshellarg((string)($secs-0.1)), // -0.1 is to work around keyframe issues >.< | |
"-t " . escapeshellarg((string)($next-$secs)), | |
escapeshellarg("clip." . $i . "." . $in_file_name) | |
)); | |
$secs=$next; | |
echo "executing: "; | |
var_dump($cmd); | |
passthru($cmd, $ret); | |
if ($ret !== 0) { | |
die("FFMPEG ERROR: FFMPEG DID NOT RETURN 0! returned {$ret}.......\n"); | |
} | |
if($next===$total_secs){ | |
break; | |
} | |
} | |
function get_next_keyframe(array &$keyframes, float $secs, float $total_secs) | |
{ | |
if (0) { | |
if (empty($keyframes)) { | |
throw new \LogicException("keyframes empty!"); | |
} | |
} | |
foreach ($keyframes as $key => $frame) { | |
if ($frame >= $secs) { | |
return $frame; | |
} else { | |
unset($keyframes[$key]); | |
} | |
} | |
//.. | |
return $total_secs; | |
} | |
function get_seconds(string $file): float | |
{ | |
// -noaccurate_seek | |
// ffprobe -print_format json -show_format | |
my_exec("ffprobe " . escapeshellarg(_uncygwinify_filepath($file)) . " -print_format json -show_format", "", $stdout, $stderr); | |
$parsed = json_decode($stdout, true); | |
return ((float)$parsed['format']['duration']); | |
} | |
function get_key_frames(string $file): array | |
{ | |
// ffprobe -select_streams v -skip_frame nokey -show_frames -show_entries frame=pkt_pts_time -print_format json "file" | |
my_exec("ffprobe -select_streams v -skip_frame nokey -show_frames -show_entries frame=pkt_pts_time -print_format json " . escapeshellarg(_uncygwinify_filepath($file)), "", $stdout, $stderr); | |
$parsed = json_decode($stdout, true); | |
$ret = array(); | |
foreach ($parsed['frames'] as $keyframe) { | |
$ret[] = (double)$keyframe['pkt_pts_time']; | |
} | |
sort($ret, SORT_NUMERIC); | |
return $ret; | |
} | |
function pause() | |
{ | |
echo "press enter to continue.."; | |
fgets(STDIN); | |
return; | |
} | |
function _uncygwinify_filepath(string $path): string | |
{ | |
static $is_cygwin_cache = null; | |
if ($is_cygwin_cache === null) { | |
$is_cygwin_cache = (false !== stripos(PHP_OS, "cygwin")); | |
} | |
if ($is_cygwin_cache) { | |
return trim(shell_exec("cygpath -aw " . escapeshellarg($path))); | |
} else { | |
return $path; | |
} | |
} | |
function _cygwinify_filepath(string $path): string | |
{ | |
static $is_cygwin_cache = null; | |
if ($is_cygwin_cache === null) { | |
$is_cygwin_cache = (false !== stripos(PHP_OS, "cygwin")); | |
} | |
if ($is_cygwin_cache) { | |
return trim(shell_exec("cygpath -a " . escapeshellarg($path))); | |
//return "/cygdrive/" . strtr($path, array(':' => '', '\\' => '/')); | |
} else { | |
return $path; | |
} | |
} | |
function my_exec(string $cmd, string $stdin = "", string &$stdout = null, string &$stderr = null): int | |
{ | |
$stdouth = tmpfile(); | |
$stderrh = tmpfile(); | |
$descriptorspec = array( | |
0 => array("pipe", "rb"), // stdin | |
1 => array("file", stream_get_meta_data($stdouth)['uri'], 'ab'), | |
2 => array("file", stream_get_meta_data($stderrh)['uri'], 'ab') | |
); | |
$pipes = array(); | |
$proc = proc_open($cmd, $descriptorspec, $pipes); | |
while (strlen($stdin) > 0) { | |
$written_now = fwrite($pipes[0], $stdin); | |
if ($written_now <= 0) { | |
//... can add more error checking here, but probably means that target program | |
// exited or crashed before consuming all of stdin. | |
// (could also have explicitly closed stdin before consuming all) | |
break; | |
} | |
$stdin = substr($stdin, $written_now); | |
} | |
fclose($pipes[0]); | |
unset($stdin, $pipes[0]); | |
$proc_ret = proc_close($proc); // this line will stall until the process has exited. | |
$stdout = stream_get_contents($stdouth); | |
$stderr = stream_get_contents($stderrh); | |
fclose($stdouth); | |
fclose($stderrh); | |
return $proc_ret; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment