From 36b0036285534ad9071252d2726ef2a32b92d732 Mon Sep 17 00:00:00 2001 From: Robert Gruendler Date: Sun, 25 Nov 2012 15:40:20 +0100 Subject: [PATCH] added support for retrieving progress information via helpers --- .gitignore | 1 + README.md | 15 ++ composer.json | 5 +- src/FFMpeg/FFMpeg.php | 48 ++++- src/FFMpeg/FFProbe.php | 8 +- src/FFMpeg/Helper/AudioProgressHelper.php | 29 +++ src/FFMpeg/Helper/HelperInterface.php | 42 ++++ src/FFMpeg/Helper/ProgressHelper.php | 223 +++++++++++++++++++++ src/FFMpeg/Helper/VideoProgressHelper.php | 29 +++ tests/src/FFMpeg/Progress/ProgressTest.php | 70 +++++++ 10 files changed, 463 insertions(+), 7 deletions(-) create mode 100644 src/FFMpeg/Helper/AudioProgressHelper.php create mode 100644 src/FFMpeg/Helper/HelperInterface.php create mode 100644 src/FFMpeg/Helper/ProgressHelper.php create mode 100644 src/FFMpeg/Helper/VideoProgressHelper.php create mode 100644 tests/src/FFMpeg/Progress/ProgressTest.php diff --git a/.gitignore b/.gitignore index 0971f29..f474a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /vendor/ /docs/build composer.phar +phpunit.xml diff --git a/README.md b/README.md index 4aaee43..0c68d66 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,21 @@ $ffmpeg->open('Video.mpeg') ->close(); ``` +##Getting progress information + + +```php +$progressHelper = new FFMpeg\Helper\AudioProgressHelper(function($percent, $remaining, $rate) { + echo "Current progress: " . $percent "%\n"; + echo "Remaining time: " . $remaining " seconds\n"; +}); + +$ffmpeg->open('Audio.wav') + ->attachHelper($progressHelper) + ->encode(new Mp3(), 'file.mp3') + ->close(); +``` + ##Using with Silex Microframework ```php diff --git a/composer.json b/composer.json index 825ebad..c40110e 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,9 @@ "minimum-stability": "dev", "require-dev": { "fabpot/php-cs-fixer": "master", - "sami/sami": "dev-master", - "silex/silex": "dev-master" + "sami/sami": "dev-master", + "silex/silex": "dev-master", + "phpunit/phpunit": "3.7.*" }, "autoload": { "psr-0": { diff --git a/src/FFMpeg/FFMpeg.php b/src/FFMpeg/FFMpeg.php index 167b05f..200dbbc 100644 --- a/src/FFMpeg/FFMpeg.php +++ b/src/FFMpeg/FFMpeg.php @@ -16,6 +16,7 @@ use FFMpeg\Exception\LogicException; use FFMpeg\Exception\RuntimeException; use FFMpeg\Format\Audio; use FFMpeg\Format\Video; +use FFMpeg\Helper\HelperInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessBuilder; @@ -35,6 +36,11 @@ class FFMpeg extends Binary protected $prober; protected $threads = 1; + /** + * @var HelperInterface[] + */ + protected $helpers = array(); + /** * Destructor */ @@ -44,6 +50,24 @@ class FFMpeg extends Binary parent::__destruct(); } + /** + * @param HelperInterface $helper + * @return \FFMpeg\FFMpeg + */ + public function attachHelper(HelperInterface $helper) + { + $this->helpers[] = $helper; + $helper->setProber($this->prober); + + // ensure the helpers have the path to the file in case + // they need to probe for format information + if ($this->pathfile !== null) { + $helper->open($this->pathfile); + } + + return $this; + } + public function setThreads($threads) { if ($threads > 64 || $threads < 1) { @@ -76,9 +100,12 @@ class FFMpeg extends Binary } $this->logger->addInfo(sprintf('FFmpeg opens %s', $pathfile)); - $this->pathfile = $pathfile; + foreach ($this->helpers as $helper) { + $helper->open($pathfile); + } + return $this; } @@ -135,7 +162,7 @@ class FFMpeg extends Binary $this->logger->addInfo(sprintf('FFmpeg executes command %s', $process->getCommandline())); try { - $process->run(); + $process->run(array($this, 'transcodeCallback')); } catch (\RuntimeException $e) { } @@ -218,7 +245,7 @@ class FFMpeg extends Binary $this->logger->addInfo(sprintf('FFmpeg executes command %s', $process->getCommandLine())); try { - $process->run(); + $process->run(array($this, 'transcodeCallback')); } catch (\RuntimeException $e) { } @@ -344,7 +371,7 @@ class FFMpeg extends Binary $this->logger->addInfo(sprintf('FFmpeg executes command %s', $process->getCommandline())); try { - $process->run(); + $process->run(array($this, 'transcodeCallback')); } catch (\RuntimeException $e) { break; } @@ -365,6 +392,19 @@ class FFMpeg extends Binary return $this; } + /** + * The main transcoding callback, delegates the content to the helpers. + * + * @param string $channel (stdio|stderr) + * @param string $content the current line of the ffmpeg output + */ + public function transcodeCallback($channel, $content) + { + foreach ($this->helpers as $helper) { + $helper->transcodeCallback($channel, $content); + } + } + /** * Removes unnecessary file * diff --git a/src/FFMpeg/FFProbe.php b/src/FFMpeg/FFProbe.php index 65ddabd..0303f55 100644 --- a/src/FFMpeg/FFProbe.php +++ b/src/FFMpeg/FFProbe.php @@ -24,6 +24,8 @@ use Symfony\Component\Process\ProcessBuilder; class FFProbe extends Binary { + protected $cachedFormats = array(); + /** * Probe the format of a given file * @@ -39,6 +41,10 @@ class FFProbe extends Binary throw new InvalidArgumentException($pathfile); } + if (isset($this->cachedFormats[$pathfile])) { + return $this->cachedFormats[$pathfile]; + } + $builder = ProcessBuilder::create(array( $this->binary, $pathfile, '-show_format' )); @@ -69,7 +75,7 @@ class FFProbe extends Binary $ret[$key] = $value; } - return json_encode($ret); + return $this->cachedFormats[$pathfile] = json_encode($ret); } /** diff --git a/src/FFMpeg/Helper/AudioProgressHelper.php b/src/FFMpeg/Helper/AudioProgressHelper.php new file mode 100644 index 0000000..5044469 --- /dev/null +++ b/src/FFMpeg/Helper/AudioProgressHelper.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Helper; + +/** + * Parses ffmpeg stderr progress information. An example: + * + *
+ *       size=    3552kB time=00:03:47.29 bitrate= 128.0kbits/s
+ * 
+ * + * @author Robert Gruendler + */ +class AudioProgressHelper extends ProgressHelper +{ + public function getPattern() + { + return '/size=(.*?) time=(.*?) /'; + } +} diff --git a/src/FFMpeg/Helper/HelperInterface.php b/src/FFMpeg/Helper/HelperInterface.php new file mode 100644 index 0000000..e7778c0 --- /dev/null +++ b/src/FFMpeg/Helper/HelperInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Helper; + +use FFMpeg\FFProbe; + +/** + * @author Robert Gruendler + */ +interface HelperInterface +{ + /** + * The callback from the ffmpeg process. + * + * @param string $channel (stdio|stderr) + * @param string $content the current line of the ffmpeg output + */ + function transcodeCallback($channel, $content); + + /** + * The helper has access to a prober instance if available. + * + * @param FFProbe $prober + */ + function setProber(FFProbe $prober); + + /** + * Called when the input file is opened. + * + * @param string $pathfile + */ + function open($pathfile); +} diff --git a/src/FFMpeg/Helper/ProgressHelper.php b/src/FFMpeg/Helper/ProgressHelper.php new file mode 100644 index 0000000..a44a068 --- /dev/null +++ b/src/FFMpeg/Helper/ProgressHelper.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Helper; + +use FFMpeg\FFProbe; + +/** + * @author Robert Gruendler + */ +abstract class ProgressHelper implements HelperInterface +{ + /** + * @var number + */ + protected $duration = null; + + /** + * transcoding rate in kb/s + * + * @var number + */ + protected $rate; + + /** + * @var array + */ + protected $format; + + /** + * @var number + */ + protected $totalSize; + + /** + * @var number + */ + protected $currentSize; + + /** + * @var number + */ + protected $currentTime; + + /** + * @var double + */ + protected $lastOutput = null; + + /** + * Percentage of transcoding progress (0 - 100) + * + * @var number + */ + protected $percent = 0; + + /** + * Time remaining (seconds) + * + * @var number + */ + protected $remaining = null; + + /** + * @var FFProbe + */ + protected $prober; + + /** + * @var Closure|string|array + */ + protected $callback; + + /** + * @param mixed $callback + */ + public function __construct($callback) + { + $this->callback = $callback; + } + + /** + * Used to ease testing. + * + * @param number $duration + */ + public function setDuration($duration) + { + $this->duration = $duration; + } + + /* + * {@inheritdoc} + */ + public function transcodeCallback($channel, $content) + { + $progress = $this->parseProgress($content); + + if (is_array($progress)) { + call_user_func_array($this->callback, $progress); + } + } + + /* + * {@inheritdoc} + */ + public function setProber(FFProbe $prober) + { + $this->prober = $prober; + } + + /* + * {@inheritdoc} + */ + public function open($pathfile) + { + if ($this->prober === null) { + throw new \RuntimeException('Unable to report audio progress without a prober'); + } + + $format = json_decode($this->prober->probeFormat($pathfile), true); + + if ($format === null || count($format) === 0 || isset($format['size']) === false) { + throw new \RuntimeException('Unable to probe format for ' . $pathfile); + } + + $this->format = $format; + $this->totalSize = $format['size'] / 1024; + $this->duration = $format['duration']; + } + + /** + * @param string $progress A ffmpeg stderr progress output + * @return array the progressinfo array or null if there's no progress available yet. + */ + public function parseProgress($progress) + { + $matches = array(); + + if (preg_match($this->getPattern(), $progress, $matches) !== 1) { + return null; + } + + $currentDuration = $this->convertDuration($matches[2]); + $currentTime = $this->microtimeFloat(); + $currentSize = trim(str_replace('kb', '', strtolower(($matches[1])))); + $percent = $currentDuration/ $this->duration; + + if ($this->lastOutput !== null) { + $delta = $currentTime - $this->lastOutput; + $deltaSize = $currentSize - $this->currentSize; + $rate = $deltaSize * $delta; + $totalDuration = $this->totalSize / $rate; + $this->remaining = floor($totalDuration - ($totalDuration * $percent)); + $this->rate = floor($rate); + } + + $this->percent = floor($percent * 100); + $this->lastOutput = $currentTime; + $this->currentSize = (int) $currentSize; + $this->currentTime = $currentDuration; + + return $this->getProgressInfo(); + } + + /** + * + * @param string $rawDuration in the format 00:00:00.00 + * @return number + */ + protected function convertDuration($rawDuration) + { + $ar = array_reverse(explode(":", $rawDuration)); + $duration = floatval($ar[0]); + if (!empty($ar[1])) $duration += intval($ar[1]) * 60; + if (!empty($ar[2])) $duration += intval($ar[2]) * 60 * 60; + + return $duration; + } + + /** + * @return array + */ + public function getProgressInfo() + { + if ($this->remaining === null) { + return null; + } + + return array( + /* + 'currentSize' => $this->currentSize, + 'currentTime' => $this->currentTime, + */ + 'percent' => $this->percent, + 'remaining' => $this->remaining, + 'rate' => $this->rate + ); + } + + /** + * @return number + */ + protected function microtimeFloat() + { + list($usec, $sec) = explode(" ", microtime()); + return ((float)$usec + (float)$sec); + } + + /** + * Get the regex pattern to match a ffmpeg stderr status line + */ + abstract function getPattern(); + +} diff --git a/src/FFMpeg/Helper/VideoProgressHelper.php b/src/FFMpeg/Helper/VideoProgressHelper.php new file mode 100644 index 0000000..7322abf --- /dev/null +++ b/src/FFMpeg/Helper/VideoProgressHelper.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Helper; + +/** + * Parses ffmpeg stderr progress information for video files. An example: + * + *
+ *       frame=  171 fps=0.0 q=10.0 size=      18kB time=00:00:05.72 bitrate=  26.4kbits/s dup=8 drop=0
+ * 
+ * + * @author Robert Gruendler + */ +class VideoProgressHelper extends ProgressHelper +{ + public function getPattern() + { + return '/size=(.*?) time=(.*?) /'; + } +} diff --git a/tests/src/FFMpeg/Progress/ProgressTest.php b/tests/src/FFMpeg/Progress/ProgressTest.php new file mode 100644 index 0000000..bd20f2f --- /dev/null +++ b/tests/src/FFMpeg/Progress/ProgressTest.php @@ -0,0 +1,70 @@ +object->open(__DIR__ . '/../../../files/Audio.mp3'); + $this->object->attachHelper($audioProgress); + $this->object->encode(new Mp3(), $dest); + + $this->assertGreaterThanOrEqual(3, $progressInfo); + } + + /** + * @covers FFMpeg\Helper\AudioProgressHelper::getPattern + */ + public function testAudioProgressHelper() + { + $audioProgress = new AudioProgressHelper(function($percent, $remaining, $rate) { }); + $audioProgress->setDuration(500); + + $line = "size= 712kB time=00:00:45.50 bitrate= 128.1kbits/s"; + $audioProgress->parseProgress($line); + + sleep(1); + + $line = "size= 4712kB time=00:01:45.50 bitrate= 128.1kbits/s"; + $progress = $audioProgress->parseProgress($line); + + $this->assertEquals('21.0', $progress['percent']); + } + + /** + * @covers FFMpeg\Helper\VideoProgressHelper::getPattern + */ + public function testVideoProgress() + { + $videoProgress = new VideoProgressHelper(function($percent, $remaining, $rate) {}); + $videoProgress->setDuration(500); + + $line = "frame= 206 fps=202 q=10.0 size= 571kB time=00:00:07.12 bitrate= 656.8kbits/s dup=9 drop=0"; + $videoProgress->parseProgress($line); + + sleep(1); + + $line = "frame= 854 fps=113 q=20.0 size= 4430kB time=00:00:33.04 bitrate=1098.5kbits/s dup=36 drop=0"; + $progress = $videoProgress->parseProgress($line); + + $this->assertEquals('6.0', $progress['percent']); + } +}