added support for retrieving progress information via helpers
This commit is contained in:
parent
c40348a007
commit
36b0036285
10 changed files with 463 additions and 7 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,4 +2,5 @@
|
|||
/vendor/
|
||||
/docs/build
|
||||
composer.phar
|
||||
phpunit.xml
|
||||
|
||||
|
|
|
|||
15
README.md
15
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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
29
src/FFMpeg/Helper/AudioProgressHelper.php
Normal file
29
src/FFMpeg/Helper/AudioProgressHelper.php
Normal 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=(.*?) /';
|
||||
}
|
||||
}
|
||||
42
src/FFMpeg/Helper/HelperInterface.php
Normal file
42
src/FFMpeg/Helper/HelperInterface.php
Normal 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);
|
||||
}
|
||||
223
src/FFMpeg/Helper/ProgressHelper.php
Normal file
223
src/FFMpeg/Helper/ProgressHelper.php
Normal 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();
|
||||
|
||||
}
|
||||
29
src/FFMpeg/Helper/VideoProgressHelper.php
Normal file
29
src/FFMpeg/Helper/VideoProgressHelper.php
Normal 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=(.*?) /';
|
||||
}
|
||||
}
|
||||
70
tests/src/FFMpeg/Progress/ProgressTest.php
Normal file
70
tests/src/FFMpeg/Progress/ProgressTest.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue