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 245a6ba..4aed856 100644
--- a/composer.json
+++ b/composer.json
@@ -27,8 +27,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..26aa59b
--- /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(
+ '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']);
+ }
+}