added support for retrieving progress information via helpers

This commit is contained in:
Robert Gruendler 2012-11-25 15:40:20 +01:00
commit 36b0036285
10 changed files with 463 additions and 7 deletions

1
.gitignore vendored
View file

@ -2,4 +2,5 @@
/vendor/
/docs/build
composer.phar
phpunit.xml

View file

@ -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

View file

@ -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": {

View file

@ -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
*

View file

@ -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);
}
/**

View file

@ -0,0 +1,29 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <info@alchemy.fr>
*
* 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:
*
* <pre>
* size= 3552kB time=00:03:47.29 bitrate= 128.0kbits/s
* </pre>
*
* @author Robert Gruendler <r.gruendler@gmail.com>
*/
class AudioProgressHelper extends ProgressHelper
{
public function getPattern()
{
return '/size=(.*?) time=(.*?) /';
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <info@alchemy.fr>
*
* 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 <r.gruendler@gmail.com>
*/
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);
}

View file

@ -0,0 +1,223 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <info@alchemy.fr>
*
* 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 <r.gruendler@gmail.com>
*/
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();
}

View file

@ -0,0 +1,29 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <info@alchemy.fr>
*
* 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:
*
* <pre>
* frame= 171 fps=0.0 q=10.0 size= 18kB time=00:00:05.72 bitrate= 26.4kbits/s dup=8 drop=0
* </pre>
*
* @author Robert Gruendler <r.gruendler@gmail.com>
*/
class VideoProgressHelper extends ProgressHelper
{
public function getPattern()
{
return '/size=(.*?) time=(.*?) /';
}
}

View file

@ -0,0 +1,70 @@
<?php
use FFMpeg\Helper\AudioProgressHelper;
use FFMpeg\FFMpegTest;
use FFMpeg\Format\Audio\Mp3;
use FFMpeg\Helper\VideoProgressHelper;
class ProgressTest extends FFMpegTest
{
/**
* @covers FFMpeg\Helper\ProgressHelper::parseProgress
* @covers FFMpeg\Helper\ProgressHelper::convertDuration
* @covers FFMpeg\Helper\ProgressHelper::getProgressInfo
* @covers FFMpeg\Helper\ProgressHelper::microtimeFloat
* @covers FFMpeg\Helper\AudioProgressHelper::getPattern
*/
public function testProgressHelper()
{
$progressInfo = array();
$audioProgress = new AudioProgressHelper(function($percent, $remaining, $rate) use ($progressInfo ) {
$progressInfo[] = $percent;
});
$dest = __DIR__ . '/../../../files/encode_test.mp3';
$this->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']);
}
}