Version 0.3
This commit is contained in:
parent
0d69145ec3
commit
ad3a5af623
130 changed files with 7283 additions and 2627 deletions
|
|
@ -1,128 +0,0 @@
|
|||
<?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;
|
||||
|
||||
use FFMpeg\Exception\BinaryNotFoundException;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\Process\ExecutableFinder;
|
||||
|
||||
/**
|
||||
* Binary abstract class
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
abstract class Binary implements AdapterInterface
|
||||
{
|
||||
protected $binary;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Logger
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* @var Integer
|
||||
*/
|
||||
protected $timeout;
|
||||
|
||||
/**
|
||||
* Binary constructor
|
||||
*
|
||||
* @param type $binary The path file to the binary
|
||||
* @param Logger $logger A logger
|
||||
* @param Integer $timeout The timout for the underlying process, 0 means no timeout
|
||||
*/
|
||||
public function __construct($binary, Logger $logger, $timeout = 60)
|
||||
{
|
||||
if (!is_executable($binary)) {
|
||||
throw new \FFMpeg\Exception\BinaryNotFoundException(sprintf('`%s` is not a valid binary', $binary));
|
||||
}
|
||||
|
||||
$this->binary = $binary;
|
||||
$this->logger = $logger;
|
||||
$this->setTimeout($timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timeout for underlying processes.
|
||||
*
|
||||
* @return integer|float
|
||||
*/
|
||||
public function getTimeout()
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout for the underlying processes, use 0 to disable timeout.
|
||||
*
|
||||
* @param integer|float $timeout
|
||||
*
|
||||
* @return Binary
|
||||
*/
|
||||
public function setTimeout($timeout)
|
||||
{
|
||||
if (0 > $timeout) {
|
||||
throw new InvalidArgumentException('Timeout must be a non-negative value');
|
||||
}
|
||||
|
||||
$this->timeout = $timeout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->binary = $binary = $this->logger = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param Logger $logger A logger
|
||||
* @param Integer $timeout The timout for the underlying process, 0 means no timeout
|
||||
*
|
||||
* @return Binary The binary
|
||||
*
|
||||
* @throws Exception\BinaryNotFoundException
|
||||
*/
|
||||
public static function load(Logger $logger, $timeout = 60)
|
||||
{
|
||||
$finder = new ExecutableFinder();
|
||||
$binary = null;
|
||||
|
||||
foreach (static::getBinaryName() as $candidate) {
|
||||
if (null !== $binary = $finder->find($candidate)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $binary) {
|
||||
throw new BinaryNotFoundException('Binary not found');
|
||||
}
|
||||
|
||||
return new static($binary, $logger, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the binary name
|
||||
*/
|
||||
protected static function getBinaryName()
|
||||
{
|
||||
throw new \Exception('Should be implemented');
|
||||
}
|
||||
}
|
||||
248
src/FFMpeg/Coordinate/AspectRatio.php
Normal file
248
src/FFMpeg/Coordinate/AspectRatio.php
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Coordinate;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
// see http://en.wikipedia.org/wiki/List_of_common_resolutions
|
||||
class AspectRatio
|
||||
{
|
||||
// named 4:3 or 1.33:1 Traditional TV
|
||||
const AR_4_3 = '4/3';
|
||||
// named 16:9 or 1.77:1 HD video standard
|
||||
const AR_16_9 = '16/9';
|
||||
|
||||
// named 3:2 or 1.5:1 see http://en.wikipedia.org/wiki/135_film
|
||||
const AR_3_2 = '3/2';
|
||||
// named 5:3 or 1.66:1 see http://en.wikipedia.org/wiki/Super_16_mm
|
||||
const AR_5_3 = '5/3';
|
||||
|
||||
// mostly used in Photography
|
||||
const AR_5_4 = '5/4';
|
||||
const AR_1_1 = '1/1';
|
||||
|
||||
// 1.85:1 US widescreen cinema standard see http://en.wikipedia.org/wiki/Widescreen#Film
|
||||
const AR_1_DOT_85_1 = '1.85:1';
|
||||
// 2.39:1 or 2.40:1 Current widescreen cinema standard see http://en.wikipedia.org/wiki/Anamorphic_format
|
||||
const AR_2_DOT_39_1 = '2.39:1';
|
||||
|
||||
// Rotated constants
|
||||
|
||||
// Rotated 4:3
|
||||
const AR_ROTATED_3_4 = '3/4';
|
||||
// Rotated 16:9
|
||||
const AR_ROTATED_9_16 = '9/16';
|
||||
|
||||
// Rotated 3:2
|
||||
const AR_ROTATED_2_3 = '2/3';
|
||||
// Rotated 5:3
|
||||
const AR_ROTATED_3_5 = '3/5';
|
||||
|
||||
// Rotated 5:4
|
||||
const AR_ROTATED_4_5 = '4/5';
|
||||
|
||||
// Rotated 1.85
|
||||
const AR_ROTATED_1_DOT_85 = '1/1.85';
|
||||
// Rotated 2.39
|
||||
const AR_ROTATED_2_DOT_39 = '1/2.39';
|
||||
|
||||
/** @var float */
|
||||
private $ratio;
|
||||
|
||||
public function __construct($ratio)
|
||||
{
|
||||
$this->ratio = $ratio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the ratio.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->ratio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the best width for given height and modulus.
|
||||
*
|
||||
* @param Integer $height
|
||||
* @param Integer $modulus
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public function calculateWidth($height, $modulus = 1)
|
||||
{
|
||||
$maxPossibleWidth = $this->getMultipleUp(ceil($this->ratio * $height), $modulus);
|
||||
$minPossibleWidth = $this->getMultipleDown(floor($this->ratio * $height), $modulus);
|
||||
|
||||
$maxRatioDiff = abs($this->ratio - ($maxPossibleWidth / $height));
|
||||
$minRatioDiff = abs($this->ratio - ($minPossibleWidth / $height));
|
||||
|
||||
return $maxRatioDiff < $minRatioDiff ? $maxPossibleWidth : $minPossibleWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the best height for given width and modulus.
|
||||
*
|
||||
* @param Integer $width
|
||||
* @param Integer $modulus
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public function calculateHeight($width, $modulus = 1)
|
||||
{
|
||||
$maxPossibleHeight = $this->getMultipleUp(ceil($width / $this->ratio), $modulus);
|
||||
$minPossibleHeight = $this->getMultipleDown(floor($width / $this->ratio), $modulus);
|
||||
|
||||
$maxRatioDiff = abs($this->ratio - ($width / $maxPossibleHeight));
|
||||
$minRatioDiff = abs($this->ratio - ($width / $minPossibleHeight));
|
||||
|
||||
return $maxRatioDiff < $minRatioDiff ? $maxPossibleHeight : $minPossibleHeight;
|
||||
}
|
||||
|
||||
private function getMultipleUp($value, $multiple)
|
||||
{
|
||||
while (0 !== $value % $multiple) {
|
||||
$value++;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function getMultipleDown($value, $multiple)
|
||||
{
|
||||
while (0 !== $value % $multiple) {
|
||||
$value--;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ratio based on Dimension.
|
||||
*
|
||||
* The strategy parameter forces by default to use standardized ratios. If
|
||||
* custom ratio need to be used, disable it.
|
||||
*
|
||||
* @param Dimension $dimension
|
||||
* @param Boolean $forceStandards Whether to force or not standard ratios
|
||||
*
|
||||
* @return AspectRatio
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function create(Dimension $dimension, $forceStandards = true)
|
||||
{
|
||||
$incoming = $dimension->getWidth() / $dimension->getHeight();
|
||||
|
||||
if ($forceStandards) {
|
||||
return new static(static::nearestStrategy($incoming));
|
||||
} else {
|
||||
return new static(static::customStrategy($incoming));
|
||||
}
|
||||
}
|
||||
|
||||
private static function valueFromName($name)
|
||||
{
|
||||
switch ($name) {
|
||||
case static::AR_4_3:
|
||||
return 4 / 3;
|
||||
case static::AR_16_9:
|
||||
return 16 / 9;
|
||||
case static::AR_1_1:
|
||||
return 1 / 1;
|
||||
case static::AR_1_DOT_85_1:
|
||||
return 1.85;
|
||||
case static::AR_2_DOT_39_1:
|
||||
return 2.39;
|
||||
case static::AR_3_2:
|
||||
return 3 / 2;
|
||||
case static::AR_5_3:
|
||||
return 5 / 3;
|
||||
case static::AR_5_4:
|
||||
return 5 / 4;
|
||||
case static::AR_ROTATED_3_4:
|
||||
return 3 / 4;
|
||||
case static::AR_ROTATED_9_16:
|
||||
return 9 / 16;
|
||||
case static::AR_ROTATED_2_3:
|
||||
return 2 / 3;
|
||||
case static::AR_ROTATED_3_5:
|
||||
return 3 / 5;
|
||||
case static::AR_ROTATED_4_5:
|
||||
return 4 / 5;
|
||||
case static::AR_ROTATED_1_DOT_85:
|
||||
return 1 / 1.85;
|
||||
case static::AR_ROTATED_2_DOT_39:
|
||||
return 1 / 2.39;
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('Unable to find value for %s', $name));
|
||||
}
|
||||
}
|
||||
|
||||
private static function customStrategy($incoming)
|
||||
{
|
||||
$try = static::nearestStrategy($incoming);
|
||||
|
||||
if (abs($try - $incoming) < $try * 0.05) {
|
||||
return $try;
|
||||
}
|
||||
|
||||
return $incoming;
|
||||
}
|
||||
|
||||
private static function nearestStrategy($incoming)
|
||||
{
|
||||
$availables = array(
|
||||
static::AR_4_3 => static::valueFromName(static::AR_4_3),
|
||||
static::AR_16_9 => static::valueFromName(static::AR_16_9),
|
||||
static::AR_1_1 => static::valueFromName(static::AR_1_1),
|
||||
static::AR_1_DOT_85_1 => static::valueFromName(static::AR_1_DOT_85_1),
|
||||
static::AR_2_DOT_39_1 => static::valueFromName(static::AR_2_DOT_39_1),
|
||||
static::AR_3_2 => static::valueFromName(static::AR_3_2),
|
||||
static::AR_5_3 => static::valueFromName(static::AR_5_3),
|
||||
static::AR_5_4 => static::valueFromName(static::AR_5_4),
|
||||
|
||||
// Rotated
|
||||
static::AR_ROTATED_4_5 => static::valueFromName(static::AR_ROTATED_4_5),
|
||||
static::AR_ROTATED_9_16 => static::valueFromName(static::AR_ROTATED_9_16),
|
||||
static::AR_ROTATED_2_3 => static::valueFromName(static::AR_ROTATED_2_3),
|
||||
static::AR_ROTATED_3_5 => static::valueFromName(static::AR_ROTATED_3_5),
|
||||
static::AR_ROTATED_3_4 => static::valueFromName(static::AR_ROTATED_3_4),
|
||||
static::AR_ROTATED_1_DOT_85 => static::valueFromName(static::AR_ROTATED_1_DOT_85),
|
||||
static::AR_ROTATED_2_DOT_39 => static::valueFromName(static::AR_ROTATED_2_DOT_39),
|
||||
);
|
||||
asort($availables);
|
||||
|
||||
$previous = $current = null;
|
||||
|
||||
foreach ($availables as $name => $value) {
|
||||
$current = $value;
|
||||
if ($incoming <= $value) {
|
||||
break;
|
||||
}
|
||||
$previous = $value;
|
||||
}
|
||||
|
||||
if (null === $previous) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
if (($current - $incoming) < ($incoming - $previous)) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
return $previous;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,19 +9,17 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Format;
|
||||
namespace FFMpeg\Coordinate;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Dimension object, used for manipulating width and height couples
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class Dimension
|
||||
{
|
||||
protected $width;
|
||||
protected $height;
|
||||
private $width;
|
||||
private $height;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
|
@ -59,4 +57,16 @@ class Dimension
|
|||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ratio
|
||||
*
|
||||
* @param type $forceStandards Whether or not force the use of standards ratios;
|
||||
*
|
||||
* @return AspectRatio
|
||||
*/
|
||||
public function getRatio($forceStandards = true)
|
||||
{
|
||||
return AspectRatio::create($this, $forceStandards);
|
||||
}
|
||||
}
|
||||
36
src/FFMpeg/Coordinate/FrameRate.php
Normal file
36
src/FFMpeg/Coordinate/FrameRate.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?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\Coordinate;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
class FrameRate
|
||||
{
|
||||
private $value;
|
||||
|
||||
public function __construct($value)
|
||||
{
|
||||
if ($value <= 0) {
|
||||
throw new InvalidArgumentException('Invalid frame rate, must be positive value.');
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
40
src/FFMpeg/Coordinate/Point.php
Normal file
40
src/FFMpeg/Coordinate/Point.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?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\Coordinate;
|
||||
|
||||
class Point
|
||||
{
|
||||
private $x;
|
||||
private $Y;
|
||||
|
||||
public function __construct($x, $y)
|
||||
{
|
||||
$this->x = (int) $x;
|
||||
$this->y = (int) $y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getX()
|
||||
{
|
||||
return $this->x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getY()
|
||||
{
|
||||
return $this->y;
|
||||
}
|
||||
}
|
||||
66
src/FFMpeg/Coordinate/TimeCode.php
Normal file
66
src/FFMpeg/Coordinate/TimeCode.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?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\Coordinate;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
class TimeCode
|
||||
{
|
||||
//see http://www.dropframetimecode.org/
|
||||
private $hours;
|
||||
private $minutes;
|
||||
private $seconds;
|
||||
private $frames;
|
||||
|
||||
public function __construct($hours, $minutes, $seconds, $frames)
|
||||
{
|
||||
$this->hours = $hours;
|
||||
$this->minutes = $minutes;
|
||||
$this->seconds = $seconds;
|
||||
$this->frames = $frames;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%02d:%02d:%02d.%02d', $this->hours, $this->minutes, $this->seconds, $this->frames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates timecode from string
|
||||
*
|
||||
* @param string $timecode
|
||||
*
|
||||
* @return TimeCode
|
||||
*
|
||||
* @throws InvalidArgumentException In case an invalid timecode is supplied
|
||||
*/
|
||||
public static function fromString($timecode)
|
||||
{
|
||||
$days = 0;
|
||||
|
||||
if (preg_match('/^[0-9]+:[0-9]+:[0-9]+:[0-9]+\.[0-9]+$/', $timecode)) {
|
||||
list($days, $hours, $minutes, $seconds, $frames) = sscanf($timecode, '%d:%d:%d:%d.%d');
|
||||
} elseif (preg_match('/^[0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9]+$/', $timecode)) {
|
||||
list($days, $hours, $minutes, $seconds, $frames) = sscanf($timecode, '%d:%d:%d:%d:%d');
|
||||
} elseif (preg_match('/^[0-9]+:[0-9]+:[0-9]+\.[0-9]+$/', $timecode)) {
|
||||
list($hours, $minutes, $seconds, $frames) = sscanf($timecode, '%d:%d:%d.%s');
|
||||
} elseif (preg_match('/^[0-9]+:[0-9]+:[0-9]+:[0-9]+$/', $timecode)) {
|
||||
list($hours, $minutes, $seconds, $frames) = sscanf($timecode, '%d:%d:%d:%s');
|
||||
} else {
|
||||
throw new InvalidArgumentException(sprintf('Unable to parse timecode %s', $timecode));
|
||||
}
|
||||
|
||||
$hours += $days * 24;
|
||||
|
||||
return new static($hours, $minutes, $seconds, $frames);
|
||||
}
|
||||
}
|
||||
40
src/FFMpeg/Driver/FFMpegDriver.php
Normal file
40
src/FFMpeg/Driver/FFMpegDriver.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?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\Driver;
|
||||
|
||||
use Alchemy\BinaryDriver\AbstractBinary;
|
||||
use Alchemy\BinaryDriver\Configuration;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FFMpegDriver extends AbstractBinary
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'ffmpeg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an FFMpegDriver.
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @param array|Configuration $configuration
|
||||
*
|
||||
* @return FFMpegDriver
|
||||
*/
|
||||
public static function create(LoggerInterface $logger, $configuration)
|
||||
{
|
||||
return static::load(array('avconv', 'ffmpeg'), $logger, $configuration);
|
||||
}
|
||||
}
|
||||
47
src/FFMpeg/Driver/FFProbeDriver.php
Normal file
47
src/FFMpeg/Driver/FFProbeDriver.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?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\Driver;
|
||||
|
||||
use Alchemy\BinaryDriver\AbstractBinary;
|
||||
use Alchemy\BinaryDriver\Configuration;
|
||||
use Alchemy\BinaryDriver\ConfigurationInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FFProbeDriver extends AbstractBinary
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'ffprobe';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an FFProbeDriver
|
||||
*
|
||||
* @param array|ConfigurationInterface $configuration
|
||||
* @param LoggerInterface $logger
|
||||
*
|
||||
* @return FFProbeDriver
|
||||
*/
|
||||
public static function create($configuration, LoggerInterface $logger = null)
|
||||
{
|
||||
if (!$configuration instanceof ConfigurationInterface) {
|
||||
$configuration = new Configuration($configuration);
|
||||
}
|
||||
|
||||
$binaries = $configuration->get('ffprobe.binaries', array('avprobe', 'ffprobe'));
|
||||
|
||||
return static::load($binaries, $logger, $configuration);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,5 +13,4 @@ namespace FFMpeg\Exception;
|
|||
|
||||
interface ExceptionInterface
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,4 @@ namespace FFMpeg\Exception;
|
|||
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,4 @@ namespace FFMpeg\Exception;
|
|||
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,466 +11,126 @@
|
|||
|
||||
namespace FFMpeg;
|
||||
|
||||
use Alchemy\BinaryDriver\ConfigurationInterface;
|
||||
use FFMpeg\Driver\FFMpegDriver;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Exception\LogicException;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Format\Video\Resamplable as VideoResamplable;
|
||||
use FFMpeg\Format\Video\Resizable as VideoResizable;
|
||||
use FFMpeg\Format\Video\Transcodable as VideoTranscodable;
|
||||
use FFMpeg\Format\Audio\Resamplable as AudioResamplable;
|
||||
use FFMpeg\Format\Audio\Transcodable as AudioTranscodable;
|
||||
use FFMpeg\Helper\HelperInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\ProcessBuilder;
|
||||
use FFMpeg\Media\Audio;
|
||||
use FFMpeg\Media\Video;
|
||||
use Alchemy\BinaryDriver\Configuration;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* FFMpeg driver
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class FFMpeg extends Binary
|
||||
class FFMpeg
|
||||
{
|
||||
protected $pathfile;
|
||||
/** @var FFMpegDriver */
|
||||
private $driver;
|
||||
/** @var FFProbe */
|
||||
private $ffprobe;
|
||||
|
||||
public function __construct(FFMpegDriver $ffmpeg, FFProbe $ffprobe)
|
||||
{
|
||||
$this->driver = $ffmpeg;
|
||||
$this->ffprobe = $ffprobe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets ffprobe
|
||||
*
|
||||
* @var FFProbe
|
||||
* @param FFProbe
|
||||
*
|
||||
* @return FFMpeg
|
||||
*/
|
||||
protected $prober;
|
||||
protected $threads = 1;
|
||||
|
||||
/**
|
||||
* @var HelperInterface[]
|
||||
*/
|
||||
protected $helpers = array();
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
public function setFFProbe(FFProbe $ffprobe)
|
||||
{
|
||||
$this->prober = null;
|
||||
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);
|
||||
}
|
||||
$this->ffprobe = $ffprobe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setThreads($threads)
|
||||
/**
|
||||
* Gets FFProbe
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function getFFProbe()
|
||||
{
|
||||
if ($threads > 64 || $threads < 1) {
|
||||
throw new InvalidArgumentException('Invalid `threads` value ; threads must fit in range 1 - 64');
|
||||
}
|
||||
return $this->ffprobe;
|
||||
}
|
||||
|
||||
$this->threads = (int) $threads;
|
||||
/**
|
||||
* Sets ffmpeg driver
|
||||
*
|
||||
* @return FFMpeg
|
||||
*/
|
||||
public function setFFMpegDriver(FFMpegDriver $ffmpeg)
|
||||
{
|
||||
$this->driver = $ffmpeg;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getThreads()
|
||||
/**
|
||||
* Gets the ffmpeg driver
|
||||
*
|
||||
* @return FFMpegDriver
|
||||
*/
|
||||
public function getFFMpegDriver()
|
||||
{
|
||||
return $this->threads;
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file in order to be processed
|
||||
*
|
||||
* @param string $pathfile A pathfile
|
||||
* @return \FFMpeg\FFMpeg
|
||||
* @param string $pathfile A pathfile
|
||||
*
|
||||
* @return Audio|Video
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function open($pathfile)
|
||||
{
|
||||
if (!file_exists($pathfile)) {
|
||||
$this->logger->addError(sprintf('FFmpeg failed to open %s', $pathfile));
|
||||
|
||||
throw new InvalidArgumentException(sprintf('File %s does not exist', $pathfile));
|
||||
throw new InvalidArgumentException(sprintf('File %s does not exists', $pathfile));
|
||||
}
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg opens %s', $pathfile));
|
||||
$this->pathfile = $pathfile;
|
||||
$streams = $this->ffprobe->streams($pathfile);
|
||||
|
||||
foreach ($this->helpers as $helper) {
|
||||
$helper->open($pathfile);
|
||||
if (0 < count($streams->videos())) {
|
||||
return new Video($pathfile, $this->driver, $this->ffprobe);
|
||||
} elseif (0 < count($streams->audios())) {
|
||||
return new Audio($pathfile, $this->driver, $this->ffprobe);
|
||||
}
|
||||
|
||||
return $this;
|
||||
throw new InvalidArgumentException('Unable to detect file format, only audio and video supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a prober
|
||||
* Creates a new FFMpeg instance
|
||||
*
|
||||
* @return \FFMpeg\FFMpeg
|
||||
* @param array|ConfigurationInterface $configuration
|
||||
* @param LoggerInterface $logger
|
||||
* @param FFProbe $probe
|
||||
*
|
||||
* @return FFMpeg
|
||||
*/
|
||||
public function setProber(FFProbe $prober)
|
||||
public static function create($configuration = array(), LoggerInterface $logger = null, FFProbe $probe = null)
|
||||
{
|
||||
$this->prober = $prober;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a file
|
||||
*
|
||||
* @return \FFMpeg\FFMpeg
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->logger->addInfo(sprintf('FFmpeg closes %s', $this->pathfile));
|
||||
|
||||
$this->pathfile = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an image from a media file
|
||||
*
|
||||
* @param integer|string $time The time where to take the snapshot, time could either be in second or in hh:mm:ss[.xxx] form.
|
||||
* @param string $output The pathfile where to write
|
||||
* @param Boolean $accurate Whether to decode the whole video until position or seek and extract. See -ss option in FFMpeg manual (http://ffmpeg.org/ffmpeg.html#Main-options)
|
||||
*
|
||||
* @return \FFMpeg\FFMpeg
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function extractImage($time, $output, $accurate = false)
|
||||
{
|
||||
if (!$this->pathfile) {
|
||||
throw new LogicException('No file open');
|
||||
if (!$configuration instanceof ConfigurationInterface) {
|
||||
$configuration = new Configuration($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://ffmpeg.org/ffmpeg.html#Main-options
|
||||
*/
|
||||
if (!$accurate) {
|
||||
$options = array(
|
||||
$this->binary, '-ss', $time,
|
||||
'-i', $this->pathfile,
|
||||
'-vframes', '1',
|
||||
'-f', 'image2', $output
|
||||
);
|
||||
} else {
|
||||
$options = array(
|
||||
$this->binary,
|
||||
'-i', $this->pathfile,
|
||||
'-vframes', '1', '-ss', $time,
|
||||
'-f', 'image2', $output
|
||||
);
|
||||
$binaries = $configuration->get('ffmpeg.binaries', array('avconv', 'ffmpeg'));
|
||||
|
||||
if (!$configuration->has('timeout')) {
|
||||
$configuration->set('timeout', 300);
|
||||
}
|
||||
|
||||
$builder = ProcessBuilder::create($options);
|
||||
$process = $builder->getProcess();
|
||||
$process->setTimeout($this->timeout);
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg executes command %s', $process->getCommandline()));
|
||||
|
||||
try {
|
||||
$process->run(array($this, 'transcodeCallback'));
|
||||
} catch (\RuntimeException $e) {
|
||||
$driver = FFMpegDriver::load($binaries, $logger, $configuration);
|
||||
|
||||
if (null === $probe) {
|
||||
$probe = FFProbe::create($configuration, $logger, null);
|
||||
}
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
$this->logger->addError(sprintf('FFmpeg command failed: %s', $process->getErrorOutput()));
|
||||
|
||||
$this->cleanupTemporaryFile($output);
|
||||
|
||||
throw new RuntimeException('Failed to extract image');
|
||||
}
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg command successful'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the file to the specified format
|
||||
*
|
||||
* @param AudioInterface $format The output format
|
||||
* @param string $outputPathfile The pathfile where to write
|
||||
* @return \FFMpeg\FFMpeg
|
||||
* @throws RuntimeException
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function encode(AudioInterface $format, $outputPathfile)
|
||||
{
|
||||
if (!$this->pathfile) {
|
||||
throw new LogicException('No file open');
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case $format instanceof VideoInterface:
|
||||
$this->encodeVideo($format, $outputPathfile);
|
||||
break;
|
||||
default:
|
||||
case $format instanceof AudioInterface:
|
||||
$this->encodeAudio($format, $outputPathfile);
|
||||
break;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode to audio
|
||||
*
|
||||
* @param Audio $format The output format
|
||||
* @param string $outputPathfile The pathfile where to write
|
||||
* @return \FFMpeg\FFMpeg
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function encodeAudio(AudioInterface $format, $outputPathfile)
|
||||
{
|
||||
$builder = ProcessBuilder::create(array(
|
||||
$this->binary,
|
||||
'-y', '-i',
|
||||
$this->pathfile,
|
||||
'-threads', $this->threads,
|
||||
'-ab', $format->getKiloBitrate() . 'k ',
|
||||
));
|
||||
|
||||
foreach ($format->getExtraParams() as $parameter) {
|
||||
$builder->add($parameter);
|
||||
}
|
||||
|
||||
if ($format instanceof AudioTranscodable) {
|
||||
$builder->add('-acodec')->add($format->getAudioCodec());
|
||||
}
|
||||
|
||||
if ($format instanceof AudioResamplable) {
|
||||
$builder->add('-ac')->add(2)->add('-ar')->add($format->getAudioSampleRate());
|
||||
}
|
||||
|
||||
$builder->add($outputPathfile);
|
||||
|
||||
$process = $builder->getProcess();
|
||||
|
||||
$process->setTimeout($this->timeout);
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg executes command %s', $process->getCommandLine()));
|
||||
|
||||
try {
|
||||
$process->run(array($this, 'transcodeCallback'));
|
||||
} catch (\RuntimeException $e) {
|
||||
|
||||
}
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
$this->logger->addInfo(sprintf('FFmpeg command failed'));
|
||||
throw new RuntimeException(sprintf('Encoding failed: %s', $process->getErrorOutput()));
|
||||
}
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg command successful'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode to video
|
||||
*
|
||||
* @param VideoInterface $format The output format
|
||||
* @param string $outputPathfile The pathfile where to write
|
||||
* @return \FFMpeg\FFMpeg
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function encodeVideo(VideoInterface $format, $outputPathfile)
|
||||
{
|
||||
$builder = ProcessBuilder::create(array(
|
||||
$this->binary, '-y', '-i',
|
||||
$this->pathfile
|
||||
));
|
||||
|
||||
foreach ($format->getExtraParams() as $parameter) {
|
||||
$builder->add($parameter);
|
||||
}
|
||||
|
||||
if ($format instanceof VideoResizable) {
|
||||
if (!$this->prober) {
|
||||
throw new LogicException('You must set a valid prober if you use a resizable format');
|
||||
}
|
||||
|
||||
$result = json_decode($this->prober->probeStreams($this->pathfile), true);
|
||||
|
||||
$originalWidth = $originalHeight = null;
|
||||
|
||||
foreach ($result as $stream) {
|
||||
foreach ($stream as $name => $value) {
|
||||
if ($name == 'width') {
|
||||
$originalWidth = $value;
|
||||
continue;
|
||||
}
|
||||
if ($name == 'height') {
|
||||
$originalHeight = $value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($originalHeight !== null && $originalWidth !== null) {
|
||||
$this->logger->addInfo(sprintf('Read dimension for resizing succesful : %s x %s', $originalWidth, $originalHeight));
|
||||
} else {
|
||||
$this->logger->addInfo(sprintf('Read dimension for resizing failed !'));
|
||||
}
|
||||
|
||||
if ($originalHeight !== null && $originalWidth !== null) {
|
||||
$dimensions = $format->getComputedDimensions($originalWidth, $originalHeight);
|
||||
$width = $this->getMultiple($dimensions->getWidth(), $format->getModulus());
|
||||
$height = $this->getMultiple($dimensions->getHeight(), $format->getModulus());
|
||||
|
||||
$builder->add('-s')->add($width . 'x' . $height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($format instanceof VideoResamplable) {
|
||||
$builder->add('-r')->add($format->getFrameRate());
|
||||
|
||||
/**
|
||||
* @see http://sites.google.com/site/linuxencoding/x264-ffmpeg-mapping
|
||||
*/
|
||||
if ($format->supportBFrames()) {
|
||||
$builder->add('-b_strategy')
|
||||
->add('1')
|
||||
->add('-bf')
|
||||
->add('3')
|
||||
->add('-g')
|
||||
->add($format->getGOPSize());
|
||||
}
|
||||
}
|
||||
|
||||
if ($format instanceof VideoTranscodable) {
|
||||
$builder->add('-vcodec')->add($format->getVideoCodec());
|
||||
}
|
||||
|
||||
$builder->add('-b:v')->add($format->getKiloBitrate() . 'k')
|
||||
->add('-threads')->add($this->threads)
|
||||
->add('-refs')->add('6')
|
||||
->add('-coder')->add('1')
|
||||
->add('-sc_threshold')->add('40')
|
||||
->add('-flags')->add('+loop')
|
||||
->add('-me_range')->add('16')
|
||||
->add('-subq')->add('7')
|
||||
->add('-i_qfactor')->add('0.71')
|
||||
->add('-qcomp')->add('0.6')
|
||||
->add('-qdiff')->add('4')
|
||||
->add('-trellis')->add('1')
|
||||
->add('-b:a')->add('92k');
|
||||
|
||||
if ($format instanceof AudioTranscodable) {
|
||||
$builder->add('-acodec')->add($format->getAudioCodec());
|
||||
}
|
||||
|
||||
$passPrefix = uniqid('pass-');
|
||||
|
||||
$pass1 = $builder;
|
||||
$pass2 = clone $builder;
|
||||
|
||||
$passes[] = $pass1
|
||||
->add('-pass')->add('1')
|
||||
->add('-passlogfile')->add($passPrefix)
|
||||
->add('-an')->add($outputPathfile)
|
||||
->getProcess();
|
||||
$passes[] = $pass2
|
||||
->add('-pass')->add('2')
|
||||
->add('-passlogfile')->add($passPrefix)
|
||||
->add('-ac')->add('2')
|
||||
->add('-ar')->add('44100')->add($outputPathfile)
|
||||
->getProcess();
|
||||
|
||||
foreach ($passes as $process) {
|
||||
|
||||
$process->setTimeout($this->timeout);
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg executes command %s', $process->getCommandline()));
|
||||
|
||||
try {
|
||||
$process->run(array($this, 'transcodeCallback'));
|
||||
} catch (\RuntimeException $e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cleanupTemporaryFile(getcwd() . '/' . $passPrefix . '-0.log');
|
||||
$this->cleanupTemporaryFile(getcwd() . '/' . $passPrefix . '-0.log');
|
||||
$this->cleanupTemporaryFile(getcwd() . '/' . $passPrefix . '-0.log.mbtree');
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
$this->logger->addInfo(sprintf('FFmpeg command failed'));
|
||||
throw new RuntimeException(sprintf('Encoding failed : %s', $process->getErrorOutput()));
|
||||
}
|
||||
|
||||
$this->logger->addInfo(sprintf('FFmpeg command successful'));
|
||||
|
||||
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
|
||||
*
|
||||
* @param string $pathfile
|
||||
*/
|
||||
protected function cleanupTemporaryFile($pathfile)
|
||||
{
|
||||
if (file_exists($pathfile) && is_writable($pathfile)) {
|
||||
unlink($pathfile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nearest multiple for a value
|
||||
*
|
||||
* @param integer $value
|
||||
* @param integer $multiple
|
||||
* @return integer
|
||||
*/
|
||||
protected function getMultiple($value, $multiple)
|
||||
{
|
||||
while (0 !== $value % $multiple) {
|
||||
$value++;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getBinaryName()
|
||||
{
|
||||
return array('avconv', 'ffmpeg');
|
||||
return new static($driver, $probe);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,49 +11,52 @@
|
|||
|
||||
namespace FFMpeg;
|
||||
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use FFMpeg\FFMpeg;
|
||||
use FFMpeg\FFProbe;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Silex\Application;
|
||||
use Silex\ServiceProviderInterface;
|
||||
|
||||
class FFMpegServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
|
||||
public function register(Application $app)
|
||||
{
|
||||
if (isset($app['monolog'])) {
|
||||
$app['ffmpeg.logger'] = function() use ($app) {
|
||||
return $app['monolog'];
|
||||
};
|
||||
} else {
|
||||
$app['ffmpeg.logger'] = $app->share(function(Application $app) {
|
||||
$logger = new Logger('FFMpeg logger');
|
||||
$logger->pushHandler(new NullHandler());
|
||||
$app['ffmpeg.configuration'] = array();
|
||||
$app['ffmpeg.default.configuration'] = array(
|
||||
'ffmpeg.threads' => 4,
|
||||
'ffmpeg.timeout' => 300,
|
||||
'ffmpeg.binaries' => array('avconv', 'ffmpeg'),
|
||||
'ffprobe.timeout' => 30,
|
||||
'ffprobe.binaries' => array('avprobe', 'ffprobe'),
|
||||
);
|
||||
$app['ffmpeg.logger'] = null;
|
||||
|
||||
return $logger;
|
||||
});
|
||||
}
|
||||
$app['ffmpeg.configuration.build'] = $app->share(function (Application $app) {
|
||||
return array_replace($app['ffmpeg.default.configuration'], $app['ffmpeg.configuration']);
|
||||
});
|
||||
|
||||
$app['ffmpeg.ffmpeg'] = $app->share(function(Application $app) {
|
||||
if (isset($app['ffmpeg.ffmpeg.binary'])) {
|
||||
$ffmpeg = new FFMpeg($app['ffmpeg.ffmpeg.binary'], $app['ffmpeg.logger']);
|
||||
} else {
|
||||
$ffmpeg = FFMpeg::load($app['ffmpeg.logger']);
|
||||
$app['ffmpeg'] = $app['ffmpeg.ffmpeg'] = $app->share(function(Application $app) {
|
||||
$configuration = $app['ffmpeg.configuration.build'];
|
||||
|
||||
if (isset($configuration['ffmpeg.timeout'])) {
|
||||
$configuration['timeout'] = $configuration['ffmpeg.timeout'];
|
||||
}
|
||||
|
||||
return $ffmpeg
|
||||
->setProber($app['ffmpeg.ffprobe'])
|
||||
->setThreads(isset($app['ffmpeg.threads']) ? $app['ffmpeg.threads'] : 1);
|
||||
return FFMpeg::create($configuration, $app['ffmpeg.logger'], $app['ffmpeg.ffprobe']);
|
||||
});
|
||||
|
||||
$app['ffprobe.cache'] = $app->share(function () {
|
||||
return new ArrayCache();
|
||||
});
|
||||
|
||||
$app['ffmpeg.ffprobe'] = $app->share(function(Application $app) {
|
||||
if (isset($app['ffmpeg.ffprobe.binary'])) {
|
||||
return new FFProbe($app['ffmpeg.ffprobe.binary'], $app['ffmpeg.logger']);
|
||||
} else {
|
||||
return FFProbe::load($app['ffmpeg.logger']);
|
||||
$configuration = $app['ffmpeg.configuration.build'];
|
||||
|
||||
if (isset($configuration['ffmpeg.timeout'])) {
|
||||
$configuration['timeout'] = $configuration['ffprobe.timeout'];
|
||||
}
|
||||
|
||||
return FFProbe::create($configuration, $app['ffmpeg.logger'], $app['ffprobe.cache']);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,163 +11,258 @@
|
|||
|
||||
namespace FFMpeg;
|
||||
|
||||
use Alchemy\BinaryDriver\ConfigurationInterface;
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use FFMpeg\Driver\FFProbeDriver;
|
||||
use FFMpeg\FFProbe\DataMapping\Format;
|
||||
use FFMpeg\FFProbe\Mapper;
|
||||
use FFMpeg\FFProbe\MapperInterface;
|
||||
use FFMpeg\FFProbe\OptionsTester;
|
||||
use FFMpeg\FFProbe\OptionsTesterInterface;
|
||||
use FFMpeg\FFProbe\OutputParser;
|
||||
use FFMpeg\FFProbe\OutputParserInterface;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\ProcessBuilder;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* FFProbe driver
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class FFProbe extends Binary
|
||||
class FFProbe
|
||||
{
|
||||
const TYPE_STREAMS = 'streams';
|
||||
const TYPE_FORMAT = 'format';
|
||||
|
||||
protected $cachedFormats = array();
|
||||
/** @var Cache */
|
||||
private $cache;
|
||||
/** @var OptionsTesterInterface */
|
||||
private $optionsTester;
|
||||
/** @var OutputParserInterface */
|
||||
private $parser;
|
||||
/** @var FFProbeDriver */
|
||||
private $ffprobe;
|
||||
/** @var MapperInterface */
|
||||
private $mapper;
|
||||
|
||||
public function __construct(FFProbeDriver $ffprobe, Cache $cache)
|
||||
{
|
||||
$this->ffprobe = $ffprobe;
|
||||
$this->optionsTester = new OptionsTester($ffprobe, $cache);
|
||||
$this->parser = new OutputParser();
|
||||
$this->mapper = new Mapper();
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OutputParserInterface
|
||||
*/
|
||||
public function getParser()
|
||||
{
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputParserInterface $parser
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function setParser(OutputParserInterface $parser)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FFProbeDriver
|
||||
*/
|
||||
public function getFFProbeDriver()
|
||||
{
|
||||
return $this->ffprobe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FFProbeDriver $ffprobe
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function setFFProbeDriver(FFProbeDriver $ffprobe)
|
||||
{
|
||||
$this->ffprobe = $ffprobe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsTesterInterface $tester
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function setOptionsTester(OptionsTesterInterface $tester)
|
||||
{
|
||||
$this->optionsTester = $tester;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OptionsTesterInterface
|
||||
*/
|
||||
public function getOptionsTester()
|
||||
{
|
||||
return $this->optionsTester;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cache $cache
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function setCache(Cache $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Cache
|
||||
*/
|
||||
public function getCache()
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MapperInterface
|
||||
*/
|
||||
public function getMapper()
|
||||
{
|
||||
return $this->mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MapperInterface $mapper
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function setMapper(MapperInterface $mapper)
|
||||
{
|
||||
$this->mapper = $mapper;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* Probe the format of a given file
|
||||
*
|
||||
* @param string $pathfile
|
||||
* @return string A Json object containing the key/values of the probe output
|
||||
* @param string $pathfile
|
||||
*
|
||||
* @return Format A Format object
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function probeFormat($pathfile)
|
||||
public function format($pathfile)
|
||||
{
|
||||
if ( ! is_file($pathfile)) {
|
||||
throw new InvalidArgumentException($pathfile);
|
||||
}
|
||||
|
||||
if (isset($this->cachedFormats[$pathfile])) {
|
||||
return $this->cachedFormats[$pathfile];
|
||||
}
|
||||
|
||||
$builder = ProcessBuilder::create(array(
|
||||
$this->binary, $pathfile, '-show_format'
|
||||
));
|
||||
|
||||
$output = $this->executeProbe($builder->getProcess());
|
||||
|
||||
$ret = array();
|
||||
|
||||
foreach (explode(PHP_EOL, $output) as $line) {
|
||||
|
||||
if (in_array($line, array('[FORMAT]', '[/FORMAT]'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chunks = explode('=', $line);
|
||||
$key = array_shift($chunks);
|
||||
|
||||
if ('' === trim($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim(implode('=', $chunks));
|
||||
|
||||
if (ctype_digit($value)) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
$ret[$key] = $value;
|
||||
}
|
||||
|
||||
return $this->cachedFormats[$pathfile] = json_encode($ret);
|
||||
return $this->probe($pathfile, '-show_format', static::TYPE_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* Probe the streams contained in a given file
|
||||
*
|
||||
* @param string $pathfile
|
||||
* @return array An array of streams array
|
||||
* @param string $pathfile
|
||||
*
|
||||
* @return StreamCollection A collection of streams
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function probeStreams($pathfile)
|
||||
public function streams($pathfile)
|
||||
{
|
||||
if ( ! is_file($pathfile)) {
|
||||
throw new InvalidArgumentException($pathfile);
|
||||
}
|
||||
|
||||
$builder = ProcessBuilder::create(array(
|
||||
$this->binary, $pathfile, '-show_streams'
|
||||
));
|
||||
|
||||
$output = explode(PHP_EOL, $this->executeProbe($builder->getProcess()));
|
||||
|
||||
$ret = array();
|
||||
$n = 0;
|
||||
|
||||
foreach ($output as $line) {
|
||||
|
||||
if ($line == '[STREAM]') {
|
||||
$n ++;
|
||||
$ret[$n] = array();
|
||||
continue;
|
||||
}
|
||||
if ($line == '[/STREAM]') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chunks = explode('=', $line);
|
||||
$key = array_shift($chunks);
|
||||
|
||||
if ('' === trim($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim(implode('=', $chunks));
|
||||
|
||||
if (ctype_digit($value)) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
$ret[$n][$key] = $value;
|
||||
}
|
||||
|
||||
return json_encode(array_values($ret));
|
||||
return $this->probe($pathfile, '-show_streams', static::TYPE_STREAMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @param Process $process
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
* @param array|ConfigurationInterface $configuration
|
||||
* @param LoggerInterface $logger
|
||||
* @param Cache $cache
|
||||
*
|
||||
* @return FFProbe
|
||||
*/
|
||||
protected function executeProbe(Process $process)
|
||||
public static function create($configuration = array(), LoggerInterface $logger = null, Cache $cache = null)
|
||||
{
|
||||
$this->logger->addInfo(sprintf('FFprobe executes command %s', $process->getCommandline()));
|
||||
|
||||
try {
|
||||
$process->run();
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->logger->addInfo('FFprobe command failed');
|
||||
|
||||
throw new RuntimeException(sprintf('Failed to run the given command %s', $process->getCommandline()));
|
||||
if (null === $cache) {
|
||||
$cache = new ArrayCache();
|
||||
}
|
||||
|
||||
if ( ! $process->isSuccessful()) {
|
||||
$this->logger->addInfo('FFprobe command failed');
|
||||
|
||||
throw new RuntimeException(sprintf('Failed to probe %s', $process->getCommandline()));
|
||||
}
|
||||
|
||||
$this->logger->addInfo('FFprobe command successful');
|
||||
|
||||
return $process->getOutput();
|
||||
return new static(FFProbeDriver::create($configuration, $logger), $cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getBinaryName()
|
||||
private function probe($pathfile, $command, $type, $allowJson = true)
|
||||
{
|
||||
return array('avprobe', 'ffprobe');
|
||||
if (!is_file($pathfile)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid filepath %s, unable to read.', $pathfile
|
||||
));
|
||||
}
|
||||
|
||||
$id = sprintf('%s-%s', $command, $pathfile);
|
||||
|
||||
if ($this->cache->contains($id)) {
|
||||
return $this->cache->fetch($id);
|
||||
}
|
||||
|
||||
if (!$this->optionsTester->has($command)) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'This version of ffprobe is too old and '
|
||||
. 'does not support `%s` option, please upgrade', $command
|
||||
));
|
||||
}
|
||||
|
||||
$commands = array($pathfile, $command);
|
||||
|
||||
$parseIsToDo = false;
|
||||
|
||||
if ($allowJson && $this->optionsTester->has('-print_format')) {
|
||||
$commands[] = '-print_format';
|
||||
$commands[] = 'json';
|
||||
} else {
|
||||
$parseIsToDo = true;
|
||||
}
|
||||
|
||||
$output = $this->ffprobe->command($commands);
|
||||
|
||||
if ($parseIsToDo) {
|
||||
$data = $this->parser->parse($type, $output);
|
||||
} else {
|
||||
try {
|
||||
// Malformed json may be retrieved
|
||||
$data = $this->parseJson($output);
|
||||
} catch (RuntimeException $e) {
|
||||
return $this->probe($pathfile, $command, $type, false);
|
||||
}
|
||||
}
|
||||
|
||||
$ret = $this->mapper->map($type, $data);
|
||||
|
||||
$this->cache->save($id, $ret);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function parseJson($data)
|
||||
{
|
||||
$ret = @json_decode($data, true);
|
||||
|
||||
if (JSON_ERROR_NONE !== json_last_error()) {
|
||||
throw new RuntimeException(sprintf('Unable to parse json %s', $ret));
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
src/FFMpeg/FFProbe/DataMapping/AbstractData.php
Normal file
80
src/FFMpeg/FFProbe/DataMapping/AbstractData.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?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\FFProbe\DataMapping;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
abstract class AbstractData implements \Countable
|
||||
{
|
||||
private $properties;
|
||||
|
||||
public function __construct(array $properties)
|
||||
{
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if data has property
|
||||
*
|
||||
* @param string $property
|
||||
* @return Boolean
|
||||
*/
|
||||
public function has($property)
|
||||
{
|
||||
return isset($this->properties[$property]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the property value given its name
|
||||
*
|
||||
* @param string $property
|
||||
* @return mixed
|
||||
*
|
||||
* @throws InvalidArgumentException In case the data does not have the property
|
||||
*/
|
||||
public function get($property)
|
||||
{
|
||||
if (!isset($this->properties[$property])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid property `%s`.', $property));
|
||||
}
|
||||
|
||||
return $this->properties[$property];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all property names
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function keys()
|
||||
{
|
||||
return array_keys($this->properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all properties and their values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->properties);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,8 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Exception;
|
||||
namespace FFMpeg\FFProbe\DataMapping;
|
||||
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
class Format extends AbstractData
|
||||
{
|
||||
|
||||
}
|
||||
35
src/FFMpeg/FFProbe/DataMapping/Stream.php
Normal file
35
src/FFMpeg/FFProbe/DataMapping/Stream.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?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\FFProbe\DataMapping;
|
||||
|
||||
class Stream extends AbstractData
|
||||
{
|
||||
/**
|
||||
* Returns true if the stream is an audio stream
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function isAudio()
|
||||
{
|
||||
return $this->has('codec_type') ? 'audio' === $this->get('codec_type') : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the stream is a video stream
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function isVideo()
|
||||
{
|
||||
return $this->has('codec_type') ? 'video' === $this->get('codec_type') : false;
|
||||
}
|
||||
}
|
||||
99
src/FFMpeg/FFProbe/DataMapping/StreamCollection.php
Normal file
99
src/FFMpeg/FFProbe/DataMapping/StreamCollection.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?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\FFProbe\DataMapping;
|
||||
|
||||
class StreamCollection implements \Countable, \IteratorAggregate
|
||||
{
|
||||
private $streams;
|
||||
|
||||
public function __construct(array $streams = array())
|
||||
{
|
||||
$this->streams = array_values($streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first stream of the collection, null if the collection is
|
||||
* empty.
|
||||
*
|
||||
* @return null|Stream
|
||||
*/
|
||||
public function first()
|
||||
{
|
||||
$stream = reset($this->streams);
|
||||
|
||||
return $stream ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a stream to the collection
|
||||
*
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return StreamCollection
|
||||
*/
|
||||
public function add(Stream $stream)
|
||||
{
|
||||
$this->streams[] = $stream;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new StreamCollection with only video streams
|
||||
*
|
||||
* @return StreamCollection
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
return new static(array_filter($this->streams, function (Stream $stream) {
|
||||
return $stream->isVideo();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new StreamCollection with only audio streams
|
||||
*
|
||||
* @return StreamCollection
|
||||
*/
|
||||
public function audios()
|
||||
{
|
||||
return new static(array_filter($this->streams, function (Stream $stream) {
|
||||
return $stream->isAudio();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of contained streams
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->streams;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new \ArrayIterator($this->streams);
|
||||
}
|
||||
}
|
||||
54
src/FFMpeg/FFProbe/Mapper.php
Normal file
54
src/FFMpeg/FFProbe/Mapper.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?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\FFProbe;
|
||||
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\FFProbe\DataMapping\Format;
|
||||
use FFMpeg\FFProbe\DataMapping\StreamCollection;
|
||||
use FFMpeg\FFProbe\DataMapping\Stream;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
class Mapper implements MapperInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function map($type, $data)
|
||||
{
|
||||
switch ($type) {
|
||||
case FFProbe::TYPE_FORMAT:
|
||||
return $this->mapFormat($data);
|
||||
case FFProbe::TYPE_STREAMS:
|
||||
return $this->mapStreams($data);
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid type `%s`.', $type
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFormat($data)
|
||||
{
|
||||
return new Format($data['format']);
|
||||
}
|
||||
|
||||
private function mapStreams($data)
|
||||
{
|
||||
$streams = new StreamCollection();
|
||||
|
||||
foreach ($data['streams'] as $properties) {
|
||||
$streams->add(new Stream($properties));
|
||||
}
|
||||
|
||||
return $streams;
|
||||
}
|
||||
}
|
||||
27
src/FFMpeg/FFProbe/MapperInterface.php
Normal file
27
src/FFMpeg/FFProbe/MapperInterface.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?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\FFProbe;
|
||||
|
||||
interface MapperInterface
|
||||
{
|
||||
/**
|
||||
* Maps data given its type
|
||||
*
|
||||
* @param string $type One of FFProbe::TYPE_* constant
|
||||
* @param string $data The data
|
||||
*
|
||||
* @return Format|Stream
|
||||
*
|
||||
* @throws InvalidArgumentException In case the type is not supported
|
||||
*/
|
||||
public function map($type, $data);
|
||||
}
|
||||
64
src/FFMpeg/FFProbe/OptionsTester.php
Normal file
64
src/FFMpeg/FFProbe/OptionsTester.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?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\FFProbe;
|
||||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use FFMpeg\Driver\FFProbeDriver;
|
||||
|
||||
class OptionsTester implements OptionsTesterInterface
|
||||
{
|
||||
/** @var FFProbeDriver */
|
||||
private $ffprobe;
|
||||
/** @var Cache */
|
||||
private $cache;
|
||||
|
||||
public function __construct(FFProbeDriver $ffprobe, Cache $cache)
|
||||
{
|
||||
$this->ffprobe = $ffprobe;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
$id = sprintf('option-%s', $name);
|
||||
|
||||
if ($this->cache->contains($id)) {
|
||||
return $this->cache->fetch($id);
|
||||
}
|
||||
|
||||
$output = $this->retrieveHelpOutput();
|
||||
|
||||
$ret = (Boolean) preg_match('/^'.$name.'/m', $output);
|
||||
|
||||
$this->cache->save($id, $ret);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function retrieveHelpOutput()
|
||||
{
|
||||
$id = 'help';
|
||||
|
||||
if ($this->cache->contains($id)) {
|
||||
return $this->cache->fetch($id);
|
||||
}
|
||||
|
||||
$output = $this->ffprobe->command(array('-help', '-loglevel', 'quiet'));
|
||||
|
||||
$this->cache->save($id, $output);
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,20 +9,16 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg;
|
||||
namespace FFMpeg\FFProbe;
|
||||
|
||||
use Monolog\Logger;
|
||||
|
||||
/**
|
||||
* FFMpeg Adapter interface
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface AdapterInterface
|
||||
interface OptionsTesterInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Loads the adapter
|
||||
* Tells if the given option is supported by ffprobe
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public static function load(Logger $logger);
|
||||
public function has($name);
|
||||
}
|
||||
125
src/FFMpeg/FFProbe/OutputParser.php
Normal file
125
src/FFMpeg/FFProbe/OutputParser.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?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\FFProbe;
|
||||
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
class OutputParser implements OutputParserInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($type, $data)
|
||||
{
|
||||
switch ($type) {
|
||||
case FFProbe::TYPE_FORMAT:
|
||||
return $this->parseFormat($data);
|
||||
break;
|
||||
case FFProbe::TYPE_STREAMS:
|
||||
return $this->parseStreams($data);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('Unknown data type %s', $type));
|
||||
}
|
||||
}
|
||||
|
||||
private function parseFormat($data)
|
||||
{
|
||||
$ret = array();
|
||||
|
||||
foreach (explode(PHP_EOL, $data) as $line) {
|
||||
|
||||
if (in_array($line, array('[FORMAT]', '[/FORMAT]'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chunks = explode('=', $line);
|
||||
$key = array_shift($chunks);
|
||||
|
||||
if ('' === trim($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim(implode('=', $chunks));
|
||||
|
||||
if ('nb_streams' === $key) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
if (0 === strpos($key, 'TAG:')) {
|
||||
if (!isset($ret['tags'])) {
|
||||
$ret['tags'] = array();
|
||||
}
|
||||
$ret['tags'][substr($key, 4)] = $value;
|
||||
} else {
|
||||
$ret[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return array('format' => $ret);
|
||||
}
|
||||
|
||||
private function parseStreams($data)
|
||||
{
|
||||
$ret = array();
|
||||
$n = -1;
|
||||
|
||||
foreach (explode(PHP_EOL, $data) as $line) {
|
||||
|
||||
if ($line == '[STREAM]') {
|
||||
$n ++;
|
||||
$ret[$n] = array();
|
||||
continue;
|
||||
}
|
||||
if ($line == '[/STREAM]') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chunks = explode('=', $line);
|
||||
$key = array_shift($chunks);
|
||||
|
||||
if ('' === trim($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim(implode('=', $chunks));
|
||||
|
||||
if ('N/A' === $value) {
|
||||
continue;
|
||||
}
|
||||
if ('profile' === $key && 'unknown' === $value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($key, array('index', 'width', 'height', 'channels', 'bits_per_sample', 'has_b_frames', 'level', 'start_pts', 'duration_ts'))) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
if (0 === strpos($key, 'TAG:')) {
|
||||
if (!isset($ret[$n]['tags'])) {
|
||||
$ret[$n]['tags'] = array();
|
||||
}
|
||||
$ret[$n]['tags'][substr($key, 4)] = $value;
|
||||
} elseif (0 === strpos($key, 'DISPOSITION:')) {
|
||||
if (!isset($ret[$n]['disposition'])) {
|
||||
$ret[$n]['disposition'] = array();
|
||||
}
|
||||
$ret[$n]['disposition'][substr($key, 12)] = $value;
|
||||
} else {
|
||||
$ret[$n][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return array('streams' => $ret);
|
||||
}
|
||||
}
|
||||
27
src/FFMpeg/FFProbe/OutputParserInterface.php
Normal file
27
src/FFMpeg/FFProbe/OutputParserInterface.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?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\FFProbe;
|
||||
|
||||
interface OutputParserInterface
|
||||
{
|
||||
/**
|
||||
* Parses ffprobe raw output
|
||||
*
|
||||
* @param string $type One of FFProbe::TYPE_* constant
|
||||
* @param string $data The data
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws InvalidArgumentException In case the type is not supported
|
||||
*/
|
||||
public function parse($type, $data);
|
||||
}
|
||||
29
src/FFMpeg/Filters/Audio/AudioFilterInterface.php
Normal file
29
src/FFMpeg/Filters/Audio/AudioFilterInterface.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Audio;
|
||||
|
||||
use FFMpeg\Filters\FilterInterface;
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
use FFMpeg\Media\Audio;
|
||||
|
||||
interface AudioFilterInterface extends FilterInterface
|
||||
{
|
||||
/**
|
||||
* Applies the filter on the the Audio media given an format.
|
||||
*
|
||||
* @param Audio $audio
|
||||
* @param AudioInterface $format
|
||||
*
|
||||
* @return array An array of arguments
|
||||
*/
|
||||
public function apply(Audio $audio, AudioInterface $format);
|
||||
}
|
||||
30
src/FFMpeg/Filters/Audio/AudioFilters.php
Normal file
30
src/FFMpeg/Filters/Audio/AudioFilters.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\Audio;
|
||||
|
||||
use FFMpeg\Media\Audio;
|
||||
use FFMpeg\Filters\Audio\AudioResamplableFilter;
|
||||
|
||||
class AudioFilters
|
||||
{
|
||||
private $audio;
|
||||
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
$this->audio = $audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resamples the audio file.
|
||||
*
|
||||
* @param Integer $rate
|
||||
*
|
||||
* @return AudioFilters
|
||||
*/
|
||||
public function resample($rate)
|
||||
{
|
||||
$this->audio->addFilter(new AudioResamplableFilter($rate));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
43
src/FFMpeg/Filters/Audio/AudioResamplableFilter.php
Normal file
43
src/FFMpeg/Filters/Audio/AudioResamplableFilter.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Audio;
|
||||
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
use FFMpeg\Media\Audio;
|
||||
|
||||
class AudioResamplableFilter implements AudioFilterInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $rate;
|
||||
|
||||
public function __construct($rate)
|
||||
{
|
||||
$this->rate = $rate;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public function getRate()
|
||||
{
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Audio $audio, AudioInterface $format)
|
||||
{
|
||||
return array('-ac', 2, '-ar', $this->rate);
|
||||
}
|
||||
}
|
||||
16
src/FFMpeg/Filters/FilterInterface.php
Normal file
16
src/FFMpeg/Filters/FilterInterface.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters;
|
||||
|
||||
interface FilterInterface
|
||||
{
|
||||
}
|
||||
45
src/FFMpeg/Filters/FiltersCollection.php
Normal file
45
src/FFMpeg/Filters/FiltersCollection.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters;
|
||||
|
||||
class FiltersCollection implements \Countable, \IteratorAggregate
|
||||
{
|
||||
private $filters = array();
|
||||
|
||||
/**
|
||||
* @param FilterInterface $filter
|
||||
*
|
||||
* @return FiltersCollection
|
||||
*/
|
||||
public function add(FilterInterface $filter)
|
||||
{
|
||||
$this->filters[] = $filter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new \ArrayIterator($this->filters);
|
||||
}
|
||||
}
|
||||
21
src/FFMpeg/Filters/Frame/FrameFilterInterface.php
Normal file
21
src/FFMpeg/Filters/Frame/FrameFilterInterface.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Frame;
|
||||
|
||||
use FFMpeg\Filters\FilterInterface;
|
||||
use FFMpeg\Media\Frame;
|
||||
use FFMpeg\Format\FrameInterface;
|
||||
|
||||
interface FrameFilterInterface extends FilterInterface
|
||||
{
|
||||
public function apply(Frame $frame, FrameInterface $format);
|
||||
}
|
||||
24
src/FFMpeg/Filters/Frame/FrameFilters.php
Normal file
24
src/FFMpeg/Filters/Frame/FrameFilters.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Frame;
|
||||
|
||||
use FFMpeg\Media\Frame;
|
||||
|
||||
class FrameFilters
|
||||
{
|
||||
private $frame;
|
||||
|
||||
public function __construct(Frame $frame)
|
||||
{
|
||||
$this->frame = $frame;
|
||||
}
|
||||
}
|
||||
126
src/FFMpeg/Filters/Video/ResizeFilter.php
Normal file
126
src/FFMpeg/Filters/Video/ResizeFilter.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Media\Video;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
|
||||
class ResizeFilter implements VideoFilterInterface
|
||||
{
|
||||
const RESIZEMODE_FIT = 'fit';
|
||||
const RESIZEMODE_INSET = 'inset';
|
||||
const RESIZEMODE_SCALE_WIDTH = 'width';
|
||||
const RESIZEMODE_SCALE_HEIGHT = 'height';
|
||||
|
||||
/** @var Dimension */
|
||||
private $dimension;
|
||||
/** @var string */
|
||||
private $mode;
|
||||
/** @var Boolean */
|
||||
private $forceStandards;
|
||||
|
||||
public function __construct(Dimension $dimension, $mode = self::RESIZEMODE_FIT, $forceStandards = true)
|
||||
{
|
||||
$this->dimension = $dimension;
|
||||
$this->mode = $mode;
|
||||
$this->forceStandards = $forceStandards;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Dimension
|
||||
*/
|
||||
public function getDimension()
|
||||
{
|
||||
return $this->dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMode()
|
||||
{
|
||||
return $this->mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean
|
||||
*/
|
||||
public function areStandardsForced()
|
||||
{
|
||||
return $this->forceStandards;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
$originalWidth = $originalHeight = null;
|
||||
|
||||
foreach ($video->getStreams() as $stream) {
|
||||
if ($stream->isVideo()) {
|
||||
if ($stream->has('width')) {
|
||||
$originalWidth = $stream->get('width');
|
||||
}
|
||||
if ($stream->has('height')) {
|
||||
$originalHeight = $stream->get('height');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$commands = array();
|
||||
|
||||
if ($originalHeight !== null && $originalWidth !== null) {
|
||||
$dimensions = $this->getComputedDimensions(new Dimension($originalWidth, $originalHeight), $format->getModulus());
|
||||
|
||||
$commands[] = '-s';
|
||||
$commands[] = $dimensions->getWidth() . 'x' . $dimensions->getHeight();
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
private function getComputedDimensions(Dimension $dimension, $modulus)
|
||||
{
|
||||
$originalRatio = $dimension->getRatio($this->forceStandards);
|
||||
|
||||
switch ($this->mode) {
|
||||
case self::RESIZEMODE_SCALE_WIDTH:
|
||||
$height = $this->dimension->getHeight();
|
||||
$width = $originalRatio->calculateWidth($height, $modulus);
|
||||
break;
|
||||
case self::RESIZEMODE_SCALE_HEIGHT:
|
||||
$width = $this->dimension->getWidth();
|
||||
$height = $originalRatio->calculateHeight($width, $modulus);
|
||||
break;
|
||||
case self::RESIZEMODE_INSET:
|
||||
$targetRatio = $this->dimension->getRatio($this->forceStandards);
|
||||
|
||||
if ($targetRatio->getValue() > $originalRatio->getValue()) {
|
||||
$height = $this->dimension->getHeight();
|
||||
$width = $originalRatio->calculateWidth($height, $modulus);
|
||||
} else {
|
||||
$width = $this->dimension->getWidth();
|
||||
$height = $originalRatio->calculateHeight($width, $modulus);
|
||||
}
|
||||
break;
|
||||
case self::RESIZEMODE_FIT:
|
||||
default:
|
||||
$width = $this->dimension->getWidth();
|
||||
$height = $this->dimension->getHeight();
|
||||
break;
|
||||
}
|
||||
|
||||
return new Dimension($width, $height);
|
||||
}
|
||||
}
|
||||
49
src/FFMpeg/Filters/Video/SynchronizeFilter.php
Normal file
49
src/FFMpeg/Filters/Video/SynchronizeFilter.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\Video;
|
||||
|
||||
class SynchronizeFilter implements VideoFilterInterface
|
||||
{
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
$streams = $video->getStreams();
|
||||
|
||||
if (null === $videoStream = $streams->videos()->first()) {
|
||||
return array();
|
||||
}
|
||||
if (!$videoStream->has('start_time')) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'-itsoffset',
|
||||
$videoStream->get('start_time'),
|
||||
'-i',
|
||||
$video->getPathfile(),
|
||||
);
|
||||
|
||||
foreach ($streams as $stream) {
|
||||
if ($videoStream === $stream) {
|
||||
$params[] = '-map';
|
||||
$params[] = '1:' . $stream->get('index');
|
||||
} else {
|
||||
$params[] = '-map';
|
||||
$params[] = '0:' . $stream->get('index');
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
29
src/FFMpeg/Filters/Video/VideoFilterInterface.php
Normal file
29
src/FFMpeg/Filters/Video/VideoFilterInterface.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Filters\FilterInterface;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\Video;
|
||||
|
||||
interface VideoFilterInterface extends FilterInterface
|
||||
{
|
||||
/**
|
||||
* Applies the filter on the the Video media given an format.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param VideoInterface $format
|
||||
*
|
||||
* @return array An array of arguments
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format);
|
||||
}
|
||||
69
src/FFMpeg/Filters/Video/VideoFilters.php
Normal file
69
src/FFMpeg/Filters/Video/VideoFilters.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Media\Video;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Coordinate\FrameRate;
|
||||
|
||||
class VideoFilters
|
||||
{
|
||||
private $video;
|
||||
|
||||
public function __construct(Video $video)
|
||||
{
|
||||
$this->video = $video;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes a video to a given dimension
|
||||
*
|
||||
* @param Dimension $dimension
|
||||
* @param string $mode
|
||||
* @param Boolean $forceStandards
|
||||
*
|
||||
* @return VideoFilters
|
||||
*/
|
||||
public function resize(Dimension $dimension, $mode = ResizeFilter::RESIZEMODE_FIT, $forceStandards = true)
|
||||
{
|
||||
$this->video->addFilter(new ResizeFilter($dimension, $mode, $forceStandards));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resamples the video to the given framerate.
|
||||
*
|
||||
* @param FrameRate $framerate
|
||||
* @param type $gop
|
||||
*
|
||||
* @return VideoFilters
|
||||
*/
|
||||
public function resample(FrameRate $framerate, $gop)
|
||||
{
|
||||
$this->video->addFilter(new VideoResampleFilter($framerate, $gop));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes audio and video.
|
||||
*
|
||||
* @return VideoFilters
|
||||
*/
|
||||
public function synchronize()
|
||||
{
|
||||
$this->video->addFilter(new SynchronizeFilter());
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
72
src/FFMpeg/Filters/Video/VideoResampleFilter.php
Normal file
72
src/FFMpeg/Filters/Video/VideoResampleFilter.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Coordinate\FrameRate;
|
||||
use FFMpeg\Media\Video;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
|
||||
class VideoResampleFilter implements VideoFilterInterface
|
||||
{
|
||||
private $rate;
|
||||
private $gop;
|
||||
|
||||
public function __construct(FrameRate $rate, $gop)
|
||||
{
|
||||
$this->rate = $rate;
|
||||
$this->gop = $gop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame rate
|
||||
*
|
||||
* @return FrameRate
|
||||
*/
|
||||
public function getFrameRate()
|
||||
{
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the GOP size
|
||||
*
|
||||
* @see https://wikipedia.org/wiki/Group_of_pictures
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public function getGOP()
|
||||
{
|
||||
return $this->gop;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
$commands = array('-r', $this->rate->getValue());
|
||||
|
||||
/**
|
||||
* @see http://sites.google.com/site/linuxencoding/x264-ffmpeg-mapping
|
||||
*/
|
||||
if ($format->supportBFrames()) {
|
||||
$commands[] = '-b_strategy';
|
||||
$commands[] = '1';
|
||||
$commands[] = '-bf';
|
||||
$commands[] = '3';
|
||||
$commands[] = '-g';
|
||||
$commands[] = $this->gop;
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,23 +11,24 @@
|
|||
|
||||
namespace FFMpeg\Format\Audio;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
use FFMpeg\Media\MediaTypeInterface;
|
||||
use FFMpeg\Format\ProgressableInterface;
|
||||
use FFMpeg\Format\ProgressListener\AudioProgressListener;
|
||||
use FFMpeg\FFProbe;
|
||||
|
||||
/**
|
||||
* The abstract default Audio format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
abstract class DefaultAudio implements Resamplable, Interactive
|
||||
abstract class DefaultAudio extends EventEmitter implements AudioInterface, ProgressableInterface
|
||||
{
|
||||
/** @var string */
|
||||
protected $audioCodec;
|
||||
protected $audioSampleRate = 44100;
|
||||
protected $kiloBitrate = 128;
|
||||
|
||||
/** @var integer */
|
||||
protected $audioKiloBitrate = 128;
|
||||
|
||||
/**
|
||||
* Returns extra parameters for the encoding
|
||||
*
|
||||
* @return string
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getExtraParams()
|
||||
{
|
||||
|
|
@ -43,11 +44,12 @@ abstract class DefaultAudio implements Resamplable, Interactive
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the audio codec, Should be in the available ones, otherwise an
|
||||
* Sets the audio codec, Should be in the available ones, otherwise an
|
||||
* exception is thrown
|
||||
*
|
||||
* @param string $audioCodec
|
||||
* @throws \InvalidArgumentException
|
||||
* @param string $audioCodec
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setAudioCodec($audioCodec)
|
||||
{
|
||||
|
|
@ -66,24 +68,24 @@ abstract class DefaultAudio implements Resamplable, Interactive
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAudioSampleRate()
|
||||
public function getAudioKiloBitrate()
|
||||
{
|
||||
return $this->audioSampleRate;
|
||||
return $this->audioKiloBitrate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio sample rate
|
||||
* Sets the kiloBitrate value
|
||||
*
|
||||
* @param integer $audioSampleRate
|
||||
* @throws \InvalidArgumentException
|
||||
* @param integer $kiloBitrate
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setAudioSampleRate($audioSampleRate)
|
||||
public function setAudioKiloBitrate($kiloBitrate)
|
||||
{
|
||||
if ($audioSampleRate < 1) {
|
||||
throw new InvalidArgumentException('Wrong audio sample rate value');
|
||||
if ($kiloBitrate < 1) {
|
||||
throw new InvalidArgumentException('Wrong kiloBitrate value');
|
||||
}
|
||||
|
||||
$this->audioSampleRate = (int) $audioSampleRate;
|
||||
$this->audioKiloBitrate = (int) $kiloBitrate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -91,25 +93,14 @@ abstract class DefaultAudio implements Resamplable, Interactive
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getKiloBitrate()
|
||||
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total)
|
||||
{
|
||||
return $this->kiloBitrate;
|
||||
}
|
||||
$format = $this;
|
||||
$listener = new AudioProgressListener($ffprobe, $media->getPathfile(), $pass, $total);
|
||||
$listener->on('progress', function () use ($media, $format) {
|
||||
$format->emit('progress', array_merge(array($media, $format), func_get_args()));
|
||||
});
|
||||
|
||||
/**
|
||||
* Set the kiloBitrate value
|
||||
*
|
||||
* @param int integer $kiloBitrate
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setKiloBitrate($kiloBitrate)
|
||||
{
|
||||
if ($kiloBitrate < 1) {
|
||||
throw new InvalidArgumentException('Wrong kiloBitrate value');
|
||||
}
|
||||
|
||||
$this->kiloBitrate = (int) $kiloBitrate;
|
||||
|
||||
return $this;
|
||||
return array($listener);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ namespace FFMpeg\Format\Audio;
|
|||
|
||||
/**
|
||||
* The Flac audio format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class Flac extends DefaultAudio
|
||||
{
|
||||
protected $audioCodec = 'flac';
|
||||
public function __construct()
|
||||
{
|
||||
$this->audioCodec = 'flac';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?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\Format\Audio;
|
||||
|
||||
/**
|
||||
* The interactive audio interface. This provide a method to list available
|
||||
* codecs. This is usefull to build interactive development and switch between
|
||||
* different codecs
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Interactive extends Transcodable
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the list of available audio codecs for this format
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableAudioCodecs();
|
||||
}
|
||||
|
|
@ -13,12 +13,13 @@ namespace FFMpeg\Format\Audio;
|
|||
|
||||
/**
|
||||
* The MP3 audio format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class Mp3 extends DefaultAudio
|
||||
{
|
||||
protected $audioCodec = 'libmp3lame';
|
||||
public function __construct()
|
||||
{
|
||||
$this->audioCodec = 'libmp3lame';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
<?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\Format\Audio;
|
||||
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
|
||||
/**
|
||||
* The resamplable audio interface
|
||||
*
|
||||
* This provide a method to define the AudiosampleRate
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Resamplable extends AudioInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the audio sample rate
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getAudioSampleRate();
|
||||
}
|
||||
|
|
@ -8,23 +8,16 @@
|
|||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Format;
|
||||
|
||||
/**
|
||||
* The base audio interface
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface AudioInterface
|
||||
interface AudioInterface extends FormatInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the kiloBitrate value
|
||||
* Get the audio kiloBitrate value
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getKiloBitrate();
|
||||
public function getAudioKiloBitrate();
|
||||
|
||||
/**
|
||||
* Return an array of extra parameters to add to ffmpeg commandline
|
||||
|
|
@ -33,4 +26,17 @@ interface AudioInterface
|
|||
*/
|
||||
public function getExtraParams();
|
||||
|
||||
/**
|
||||
* Returns the audio codec
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAudioCodec();
|
||||
|
||||
/**
|
||||
* Returns the list of available audio codecs for this format
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableAudioCodecs();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@
|
|||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace FFMpeg\Format;
|
||||
|
||||
namespace FFMpeg\Exception;
|
||||
|
||||
class BinaryNotFoundException extends \Exception implements ExceptionInterface
|
||||
interface FormatInterface
|
||||
{
|
||||
|
||||
}
|
||||
16
src/FFMpeg/Format/FrameInterface.php
Normal file
16
src/FFMpeg/Format/FrameInterface.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Format;
|
||||
|
||||
interface FrameInterface extends FormatInterface
|
||||
{
|
||||
}
|
||||
238
src/FFMpeg/Format/ProgressListener/AbstractProgressListener.php
Normal file
238
src/FFMpeg/Format/ProgressListener/AbstractProgressListener.php
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<?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\Format\ProgressListener;
|
||||
|
||||
use Alchemy\BinaryDriver\Listeners\ListenerInterface;
|
||||
use Evenement\EventEmitter;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* @author Robert Gruendler <r.gruendler@gmail.com>
|
||||
*/
|
||||
abstract class AbstractProgressListener extends EventEmitter implements ListenerInterface
|
||||
{
|
||||
/** @var integer */
|
||||
private $duration;
|
||||
|
||||
/** @var integer */
|
||||
private $totalSize;
|
||||
|
||||
/** @var integer */
|
||||
private $currentSize;
|
||||
|
||||
/** @var integer */
|
||||
private $currentTime;
|
||||
|
||||
/** @var double */
|
||||
private $lastOutput = null;
|
||||
|
||||
/** @var FFProbe */
|
||||
private $ffprobe;
|
||||
|
||||
/** @var string */
|
||||
private $pathfile;
|
||||
|
||||
/** @var Boolean */
|
||||
private $initialized = false;
|
||||
|
||||
/** @var integer */
|
||||
private $currentPass;
|
||||
|
||||
/** @var integer */
|
||||
private $totalPass;
|
||||
|
||||
/**
|
||||
* Transcoding rate in kb/s
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $rate;
|
||||
|
||||
/**
|
||||
* Percentage of transcoding progress (0 - 100)
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $percent = 0;
|
||||
|
||||
/**
|
||||
* Time remaining (seconds)
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $remaining = null;
|
||||
|
||||
/**
|
||||
* @param FFProbe $ffprobe
|
||||
* @param string $pathfile
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct(FFProbe $ffprobe, $pathfile, $currentPass, $totalPass)
|
||||
{
|
||||
$this->ffprobe = $ffprobe;
|
||||
$this->pathfile = $pathfile;
|
||||
$this->currentPass = $currentPass;
|
||||
$this->totalPass = $totalPass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function getFFProbe()
|
||||
{
|
||||
return $this->ffprobe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPathfile()
|
||||
{
|
||||
return $this->pathfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getCurrentPass()
|
||||
{
|
||||
return $this->currentPass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getTotalPass()
|
||||
{
|
||||
return $this->totalPass;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle($type, $data)
|
||||
{
|
||||
if (null !== $progress = $this->parseProgress($data)) {
|
||||
$this->emit('progress', array_values($progress));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function forwardedEvents()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the regex pattern to match a ffmpeg stderr status line
|
||||
*/
|
||||
abstract protected function getPattern();
|
||||
|
||||
/**
|
||||
* @param string $progress A ffmpeg stderr progress output
|
||||
*
|
||||
* @return array the progressinfo array or null if there's no progress available yet.
|
||||
*/
|
||||
private function parseProgress($progress)
|
||||
{
|
||||
if (!$this->initialized) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
$matches = array();
|
||||
|
||||
if (preg_match($this->getPattern(), $progress, $matches) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentDuration = $this->convertDuration($matches[2]);
|
||||
$currentTime = microtime(true);
|
||||
$currentSize = trim(str_replace('kb', '', strtolower(($matches[1]))));
|
||||
$percent = max(0, min(1, $currentDuration / $this->duration));
|
||||
|
||||
if ($this->lastOutput !== null) {
|
||||
$delta = $currentTime - $this->lastOutput;
|
||||
$deltaSize = $currentSize - $this->currentSize;
|
||||
$rate = $deltaSize * $delta;
|
||||
if ($rate > 0) {
|
||||
$totalDuration = $this->totalSize / $rate;
|
||||
$this->remaining = floor($totalDuration - ($totalDuration * $percent));
|
||||
$this->rate = floor($rate);
|
||||
} else {
|
||||
$this->remaining = 0;
|
||||
$this->rate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$percent = $percent / $this->totalPass + ($this->currentPass - 1) / $this->totalPass;
|
||||
|
||||
$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
|
||||
*/
|
||||
private 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
|
||||
*/
|
||||
private function getProgressInfo()
|
||||
{
|
||||
if ($this->remaining === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'percent' => $this->percent,
|
||||
'remaining' => $this->remaining,
|
||||
'rate' => $this->rate
|
||||
);
|
||||
}
|
||||
|
||||
private function initialize()
|
||||
{
|
||||
$format = $this->ffprobe->format($this->pathfile);
|
||||
|
||||
if (false === $format->has('size') || false === $format->has('duration')) {
|
||||
throw new RuntimeException(sprintf('Unable to probe format for %s', $this->pathfile));
|
||||
}
|
||||
|
||||
$this->totalSize = $format->get('size') / 1024;
|
||||
$this->duration = $format->get('duration');
|
||||
|
||||
$this->initialized = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Helper;
|
||||
namespace FFMpeg\Format\ProgressListener;
|
||||
|
||||
/**
|
||||
* Parses ffmpeg stderr progress information. An example:
|
||||
|
|
@ -20,7 +20,7 @@ namespace FFMpeg\Helper;
|
|||
*
|
||||
* @author Robert Gruendler <r.gruendler@gmail.com>
|
||||
*/
|
||||
class AudioProgressHelper extends ProgressHelper
|
||||
class AudioProgressListener extends AbstractProgressListener
|
||||
{
|
||||
public function getPattern()
|
||||
{
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Helper;
|
||||
namespace FFMpeg\Format\ProgressListener;
|
||||
|
||||
/**
|
||||
* Parses ffmpeg stderr progress information for video files. An example:
|
||||
|
|
@ -20,7 +20,7 @@ namespace FFMpeg\Helper;
|
|||
*
|
||||
* @author Robert Gruendler <r.gruendler@gmail.com>
|
||||
*/
|
||||
class VideoProgressHelper extends ProgressHelper
|
||||
class VideoProgressListener extends AbstractProgressListener
|
||||
{
|
||||
public function getPattern()
|
||||
{
|
||||
31
src/FFMpeg/Format/ProgressableInterface.php
Normal file
31
src/FFMpeg/Format/ProgressableInterface.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Format;
|
||||
|
||||
use Evenement\EventEmitterInterface;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Media\MediaTypeInterface;
|
||||
|
||||
interface ProgressableInterface extends EventEmitterInterface
|
||||
{
|
||||
/**
|
||||
* Creates the progress listener
|
||||
*
|
||||
* @param MediaTypeInterface $media
|
||||
* @param FFProbe $ffprobe
|
||||
* @param Integer $pass The current pas snumber
|
||||
* @param Integer $total The total pass number
|
||||
*
|
||||
* @return array An array of listeners
|
||||
*/
|
||||
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total);
|
||||
}
|
||||
|
|
@ -11,171 +11,48 @@
|
|||
|
||||
namespace FFMpeg\Format\Video;
|
||||
|
||||
use FFMpeg\Format\Audio\DefaultAudio;
|
||||
use FFMpeg\Format\Dimension;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Format\Audio\DefaultAudio;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\MediaTypeInterface;
|
||||
use FFMpeg\Format\ProgressListener\VideoProgressListener;
|
||||
|
||||
/**
|
||||
* The abstract default Video format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
abstract class DefaultVideo extends DefaultAudio implements Interactive, Resamplable, Resizable
|
||||
abstract class DefaultVideo extends DefaultAudio implements VideoInterface
|
||||
{
|
||||
const RESIZEMODE_FIT = 'fit';
|
||||
const RESIZEMODE_INSET = 'inset';
|
||||
const RESIZEMODE_SCALE_WIDTH = 'width';
|
||||
const RESIZEMODE_SCALE_HEIGHT = 'height';
|
||||
|
||||
protected $width;
|
||||
protected $height;
|
||||
protected $frameRate = 25;
|
||||
protected $resizeMode = self::RESIZEMODE_FIT;
|
||||
/** @var string */
|
||||
protected $videoCodec;
|
||||
protected $GOPsize = 25;
|
||||
|
||||
/** @var Integer */
|
||||
protected $kiloBitrate = 1000;
|
||||
|
||||
/** @var Integer */
|
||||
protected $modulus = 16;
|
||||
|
||||
/**
|
||||
* Returns the width setting.
|
||||
* The return of this method should not depend on a media file size
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getWidth()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the height setting
|
||||
* The return of this method should not depend on a media file size
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getHeight()
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dimensions
|
||||
*
|
||||
* @param integer $width The heigth
|
||||
* @param integer $height The width
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setDimensions($width, $height)
|
||||
{
|
||||
if ($width < 1) {
|
||||
throw new InvalidArgumentException('Wrong width value');
|
||||
}
|
||||
if ($height < 1) {
|
||||
throw new InvalidArgumentException('Wrong height value');
|
||||
}
|
||||
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc)
|
||||
*/
|
||||
public function getComputedDimensions($originalWidth, $originalHeight)
|
||||
{
|
||||
$originalRatio = $originalWidth / $originalHeight;
|
||||
|
||||
switch ($this->getResizeMode()) {
|
||||
case self::RESIZEMODE_SCALE_WIDTH:
|
||||
$height = $this->height;
|
||||
$width = round($originalRatio * $this->height);
|
||||
break;
|
||||
case self::RESIZEMODE_SCALE_HEIGHT:
|
||||
$width = $this->width;
|
||||
$height = round($this->width / $originalRatio);
|
||||
break;
|
||||
case self::RESIZEMODE_INSET:
|
||||
$targetRatio = $this->width / $this->height;
|
||||
|
||||
if ($targetRatio > $originalRatio) {
|
||||
$height = $this->height;
|
||||
$width = round($originalRatio * $this->height);
|
||||
} else {
|
||||
$width = $this->width;
|
||||
$height = round($this->width / $originalRatio);
|
||||
}
|
||||
break;
|
||||
case self::RESIZEMODE_FIT:
|
||||
default:
|
||||
if (null !== $this->width && null !== $this->height) {
|
||||
$width = $this->width;
|
||||
$height = $this->height;
|
||||
} else {
|
||||
$width = $originalWidth;
|
||||
$height = $originalHeight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return new Dimension($width, $height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resize mode
|
||||
*
|
||||
* @param string $mode The mode, one of the self::RESIZEMODE_* constants
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setResizeMode($mode)
|
||||
{
|
||||
if ( ! in_array($mode, array(self::RESIZEMODE_FIT, self::RESIZEMODE_INSET, self::RESIZEMODE_SCALE_WIDTH, self::RESIZEMODE_SCALE_HEIGHT))) {
|
||||
throw new InvalidArgumentException(
|
||||
'Resize mode `%s` is not valid , avalaible values are %s',
|
||||
$mode,
|
||||
implode(', ', array(self::RESIZEMODE_FIT, self::RESIZEMODE_INSET, self::RESIZEMODE_SCALE_WIDTH, self::RESIZEMODE_SCALE_HEIGHT))
|
||||
);
|
||||
}
|
||||
|
||||
$this->resizeMode = $mode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current resize mode name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResizeMode()
|
||||
{
|
||||
return $this->resizeMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFrameRate()
|
||||
public function getKiloBitrate()
|
||||
{
|
||||
return $this->frameRate;
|
||||
return $this->kiloBitrate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the framerate
|
||||
* Sets the kiloBitrate value
|
||||
*
|
||||
* @param integer $frameRate
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @param integer $kiloBitrate
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setFrameRate($frameRate)
|
||||
public function setKiloBitrate($kiloBitrate)
|
||||
{
|
||||
if ($frameRate < 1) {
|
||||
throw new InvalidArgumentException('Wrong framerate value');
|
||||
if ($kiloBitrate < 1) {
|
||||
throw new InvalidArgumentException('Wrong kiloBitrate value');
|
||||
}
|
||||
|
||||
$this->frameRate = (int) $frameRate;
|
||||
$this->kiloBitrate = (int) $kiloBitrate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -189,11 +66,11 @@ abstract class DefaultVideo extends DefaultAudio implements Interactive, Resampl
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the video codec, Should be in the available ones, otherwise an
|
||||
* Sets the video codec, Should be in the available ones, otherwise an
|
||||
* exception is thrown
|
||||
*
|
||||
* @param string $videoCodec
|
||||
* @throws \InvalidArgumentException
|
||||
* @param string $videoCodec
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setVideoCodec($videoCodec)
|
||||
{
|
||||
|
|
@ -209,32 +86,6 @@ abstract class DefaultVideo extends DefaultAudio implements Interactive, Resampl
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getGOPsize()
|
||||
{
|
||||
return $this->GOPsize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the GOP size
|
||||
*
|
||||
* @param integer $GOPsize
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setGOPsize($GOPsize)
|
||||
{
|
||||
if ($GOPsize < 1) {
|
||||
throw new InvalidArgumentException('Wrong GOP size value');
|
||||
}
|
||||
|
||||
$this->GOPsize = (int) $GOPsize;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
|
@ -244,24 +95,27 @@ abstract class DefaultVideo extends DefaultAudio implements Interactive, Resampl
|
|||
}
|
||||
|
||||
/**
|
||||
* Used to determine what resolutions sizes are valid.
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setModulus($value)
|
||||
{
|
||||
if(!in_array($value, array(2, 4, 8, 16))){
|
||||
throw new InvalidArgumentException('Wrong modulus division value. Valid values are 2, 4, 8 or 16');
|
||||
}
|
||||
|
||||
$this->modulus = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @return integer
|
||||
*/
|
||||
public function getModulus()
|
||||
{
|
||||
return $this->modulus;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total)
|
||||
{
|
||||
$format = $this;
|
||||
$listeners = array(new VideoProgressListener($ffprobe, $media->getPathfile(), $pass, $total));
|
||||
|
||||
foreach ($listeners as $listener) {
|
||||
$listener->on('progress', function () use ($format, $media) {
|
||||
$format->emit('progress', array_merge(array($media, $format), func_get_args()));
|
||||
});
|
||||
}
|
||||
|
||||
return $listeners;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?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\Format\Video;
|
||||
|
||||
/**
|
||||
* The interactive video interface. This provide a method to list available
|
||||
* codecs. This is usefull to build interactive development and switch between
|
||||
* different codecs
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Interactive extends Transcodable
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the list of available video codecs for this format
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableVideoCodecs();
|
||||
}
|
||||
|
|
@ -13,13 +13,15 @@ namespace FFMpeg\Format\Video;
|
|||
|
||||
/**
|
||||
* The Ogg video format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class Ogg extends DefaultVideo
|
||||
{
|
||||
protected $audioCodec = 'libvorbis';
|
||||
protected $videoCodec = 'libtheora';
|
||||
public function __construct($audioCodec = 'libvorbis', $videoCodec = 'libtheora')
|
||||
{
|
||||
$this
|
||||
->setAudioCodec($audioCodec)
|
||||
->setVideoCodec($videoCodec);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<?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\Format\Video;
|
||||
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
|
||||
/**
|
||||
* The resamplable video interface
|
||||
*
|
||||
* This interface provides frame rate and GOP size settings for video encoding
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Resamplable extends VideoInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the frame rate
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getFrameRate();
|
||||
|
||||
/**
|
||||
* Returns true if the current format supports B-Frames
|
||||
*
|
||||
* @see https://wikipedia.org/wiki/Video_compression_picture_types
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function supportBFrames();
|
||||
|
||||
/**
|
||||
* Returns the GOP size
|
||||
*
|
||||
* @see https://wikipedia.org/wiki/Group_of_pictures
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getGOPSize();
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<?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\Format\Video;
|
||||
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Format\Dimension;
|
||||
|
||||
/**
|
||||
* The resizable video interface
|
||||
*
|
||||
* This interface provides methods for video resizing.
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Resizable extends VideoInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the computed dimensions for the resize, after operation.
|
||||
* This method return the actual dimensions that FFmpeg will use.
|
||||
*
|
||||
* @param integer $originalWidth
|
||||
* @param integer $originalHeight
|
||||
* @return Dimension A dimension
|
||||
*/
|
||||
public function getComputedDimensions($originalWidth, $originalHeight);
|
||||
|
||||
/**
|
||||
* Returns the modulus used by the Resizable video.
|
||||
*
|
||||
* This used to calculate the target dimensions while maintaining the best
|
||||
* aspect ratio.
|
||||
*
|
||||
* @see http://www.undeadborn.net/tools/rescalculator.php
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getModulus();
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?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\Format\Video;
|
||||
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
|
||||
/**
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Transcodable extends VideoInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the video codec
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getVideoCodec();
|
||||
}
|
||||
|
|
@ -13,13 +13,15 @@ namespace FFMpeg\Format\Video;
|
|||
|
||||
/**
|
||||
* The WMV video format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class WMV extends DefaultVideo
|
||||
{
|
||||
protected $audioCodec = 'wmav2';
|
||||
protected $videoCodec = 'wmv2';
|
||||
public function __construct($audioCodec = 'wmav2', $videoCodec = 'wmv2')
|
||||
{
|
||||
$this
|
||||
->setAudioCodec($audioCodec)
|
||||
->setVideoCodec($videoCodec);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ namespace FFMpeg\Format\Video;
|
|||
|
||||
/**
|
||||
* The WMV video format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class WMV3 extends DefaultVideo
|
||||
{
|
||||
protected $audioCodec = 'wmav3';
|
||||
protected $videoCodec = 'wmv3';
|
||||
public function __construct($audioCodec = 'wmav3', $videoCodec = 'wmv3')
|
||||
{
|
||||
$this
|
||||
->setAudioCodec($audioCodec)
|
||||
->setVideoCodec($videoCodec);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ namespace FFMpeg\Format\Video;
|
|||
|
||||
/**
|
||||
* The WebM video format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class WebM extends DefaultVideo
|
||||
{
|
||||
protected $audioCodec = 'libvorbis';
|
||||
protected $videoCodec = 'libvpx';
|
||||
public function __construct($audioCodec = 'libvorbis', $videoCodec = 'libvpx')
|
||||
{
|
||||
$this
|
||||
->setAudioCodec($audioCodec)
|
||||
->setVideoCodec($videoCodec);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ namespace FFMpeg\Format\Video;
|
|||
|
||||
/**
|
||||
* The X264 video format
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
class X264 extends DefaultVideo
|
||||
{
|
||||
protected $audioCodec = 'libmp3lame';
|
||||
protected $videoCodec = 'libx264';
|
||||
public function __construct($audioCodec = 'libfaac', $videoCodec = 'libx264')
|
||||
{
|
||||
$this
|
||||
->setAudioCodec($audioCodec)
|
||||
->setVideoCodec($videoCodec);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,54 @@
|
|||
|
||||
namespace FFMpeg\Format;
|
||||
|
||||
/**
|
||||
* The base video interface
|
||||
*
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface VideoInterface extends AudioInterface
|
||||
{
|
||||
/**
|
||||
* Get the kiloBitrate value
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getKiloBitrate();
|
||||
|
||||
/**
|
||||
* Returns the number of passes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPasses();
|
||||
|
||||
/**
|
||||
* Returns the modulus used by the Resizable video.
|
||||
*
|
||||
* This used to calculate the target dimensions while maintaining the best
|
||||
* aspect ratio.
|
||||
*
|
||||
* @see http://www.undeadborn.net/tools/rescalculator.php
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getModulus();
|
||||
|
||||
/**
|
||||
* Returns the video codec
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getVideoCodec();
|
||||
|
||||
/**
|
||||
* Returns true if the current format supports B-Frames
|
||||
*
|
||||
* @see https://wikipedia.org/wiki/Video_compression_picture_types
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function supportBFrames();
|
||||
|
||||
/**
|
||||
* Returns the list of available video codecs for this format
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableVideoCodecs();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
<?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 = microtime(true);
|
||||
$currentSize = trim(str_replace('kb', '', strtolower(($matches[1]))));
|
||||
$percent = max(0, min(1, $currentDuration / $this->duration));
|
||||
|
||||
if ($this->lastOutput !== null) {
|
||||
$delta = $currentTime - $this->lastOutput;
|
||||
$deltaSize = $currentSize - $this->currentSize;
|
||||
$rate = $deltaSize * $delta;
|
||||
if ($rate > 0) {
|
||||
$totalDuration = $this->totalSize / $rate;
|
||||
$this->remaining = floor($totalDuration - ($totalDuration * $percent));
|
||||
$this->rate = floor($rate);
|
||||
} else {
|
||||
$this->remaining = 0;
|
||||
$this->rate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the regex pattern to match a ffmpeg stderr status line
|
||||
*/
|
||||
abstract function getPattern();
|
||||
}
|
||||
126
src/FFMpeg/Media/AbstractMediaType.php
Normal file
126
src/FFMpeg/Media/AbstractMediaType.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of PHP-FFmpeg.
|
||||
*
|
||||
* (c) Alchemy <dev.team@alchemy.fr>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Media;
|
||||
|
||||
use FFMpeg\Driver\FFMpegDriver;
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Filters\FiltersCollection;
|
||||
use FFMpeg\Media\MediaTypeInterface;
|
||||
|
||||
abstract class AbstractMediaType implements MediaTypeInterface
|
||||
{
|
||||
/** @var string */
|
||||
protected $pathfile;
|
||||
/** @var FFMpegDriver */
|
||||
protected $driver;
|
||||
/** @var FFProbe */
|
||||
protected $ffprobe;
|
||||
/** @var FiltersCollection */
|
||||
protected $filters;
|
||||
|
||||
public function __construct($pathfile, FFMpegDriver $driver, FFProbe $ffprobe)
|
||||
{
|
||||
$this->ensureFileIsPresent($pathfile);
|
||||
|
||||
$this->pathfile = $pathfile;
|
||||
$this->driver = $driver;
|
||||
$this->ffprobe = $ffprobe;
|
||||
$this->filters = new FiltersCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FFMpegDriver
|
||||
*/
|
||||
public function getFFMpegDriver()
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FFMpegDriver $driver
|
||||
*
|
||||
* @return MediaTypeInterface
|
||||
*/
|
||||
public function setFFMpegDriver(FFMpegDriver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FFProbe
|
||||
*/
|
||||
public function getFFProbe()
|
||||
{
|
||||
return $this->ffprobe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FFProbe $ffprobe
|
||||
*
|
||||
* @return MediaTypeInterface
|
||||
*/
|
||||
public function setFFProbe(FFProbe $ffprobe)
|
||||
{
|
||||
$this->ffprobe = $ffprobe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPathfile()
|
||||
{
|
||||
return $this->pathfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FiltersCollection $filters
|
||||
*
|
||||
* @return MediaTypeInterface
|
||||
*/
|
||||
public function setFiltersCollection(FiltersCollection $filters)
|
||||
{
|
||||
$this->filters = $filters;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MediaTypeInterface
|
||||
*/
|
||||
public function getFiltersCollection()
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
protected function ensureFileIsPresent($filename)
|
||||
{
|
||||
if (!is_file($filename) || !is_readable($filename)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s is not present or not readable', $filename
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
protected function cleanupTemporaryFile($filename)
|
||||
{
|
||||
if (file_exists($filename) && is_writable($filename)) {
|
||||
unlink($filename);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
34
src/FFMpeg/Media/AbstractStreamableMedia.php
Normal file
34
src/FFMpeg/Media/AbstractStreamableMedia.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?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\Media;
|
||||
|
||||
use FFMpeg\FFProbe\DataMapping\Stream;
|
||||
use FFMpeg\FFProbe\DataMapping\StreamCollection;
|
||||
|
||||
abstract class AbstractStreamableMedia extends AbstractMediaType
|
||||
{
|
||||
/**
|
||||
* @return StreamCollection
|
||||
*/
|
||||
public function getStreams()
|
||||
{
|
||||
return $this->ffprobe->streams($this->pathfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stream
|
||||
*/
|
||||
public function getFormat()
|
||||
{
|
||||
return $this->ffprobe->format($this->pathfile);
|
||||
}
|
||||
}
|
||||
92
src/FFMpeg/Media/Audio.php
Normal file
92
src/FFMpeg/Media/Audio.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?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\Media;
|
||||
|
||||
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
||||
use FFMpeg\Filters\Audio\AudioFilters;
|
||||
use FFMpeg\Format\FormatInterface;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\Filters\Audio\AudioFilterInterface;
|
||||
use FFMpeg\Format\ProgressableInterface;
|
||||
|
||||
class Audio extends AbstractStreamableMedia
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return AudioFilters
|
||||
*/
|
||||
public function filters()
|
||||
{
|
||||
return new AudioFilters($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Audio
|
||||
*/
|
||||
public function addFilter(AudioFilterInterface $filter)
|
||||
{
|
||||
$this->filters->add($filter);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the audio in the desired format, applies registered filters
|
||||
*
|
||||
* @param FormatInterface $format
|
||||
* @param string $outputPathfile
|
||||
*
|
||||
* @return Audio
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function save(FormatInterface $format, $outputPathfile)
|
||||
{
|
||||
$listeners = null;
|
||||
|
||||
if ($format instanceof ProgressableInterface) {
|
||||
$listeners = $format->createProgressListener($this, $this->ffprobe, 1, 1);
|
||||
}
|
||||
|
||||
$commands = array_merge(array('-y', '-i', $this->pathfile), $format->getExtraParams());
|
||||
|
||||
foreach ($this->filters as $filter) {
|
||||
$commands = array_merge($commands, $filter->apply($this, $format));
|
||||
}
|
||||
|
||||
if ($this->driver->getConfiguration()->has('ffmpeg.threads')) {
|
||||
$commands[] = '-threads';
|
||||
$commands[] = $this->driver->getConfiguration()->get('ffmpeg.threads');
|
||||
}
|
||||
|
||||
if (null !== $format->getAudioCodec()) {
|
||||
$commands[] = '-acodec';
|
||||
$commands[] = $format->getAudioCodec();
|
||||
}
|
||||
|
||||
$commands[] = '-b:a';
|
||||
$commands[] = $format->getAudioKiloBitrate() . 'k';
|
||||
$commands[] = $outputPathfile;
|
||||
|
||||
try {
|
||||
$this->driver->command($commands, false, $listeners);
|
||||
} catch (ExecutionFailureException $e) {
|
||||
$this->cleanupTemporaryFile($outputPathfile);
|
||||
throw new RuntimeException('Encoding failed', $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
104
src/FFMpeg/Media/Frame.php
Normal file
104
src/FFMpeg/Media/Frame.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?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\Media;
|
||||
|
||||
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
||||
use FFMpeg\Filters\Frame\FrameFilterInterface;
|
||||
use FFMpeg\Filters\Frame\FrameFilters;
|
||||
use FFMpeg\Driver\FFMpegDriver;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\Coordinate\TimeCode;
|
||||
|
||||
class Frame extends AbstractMediaType
|
||||
{
|
||||
/** @var TimeCode */
|
||||
private $timecode;
|
||||
|
||||
public function __construct($pathfile, FFMpegDriver $driver, FFProbe $ffprobe, TimeCode $timecode)
|
||||
{
|
||||
parent::__construct($pathfile, $driver, $ffprobe);
|
||||
$this->timecode = $timecode;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return FrameFilters
|
||||
*/
|
||||
public function filters()
|
||||
{
|
||||
return new FrameFilters($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Frame
|
||||
*/
|
||||
public function addFilter(FrameFilterInterface $filter)
|
||||
{
|
||||
$this->filters->add($filter);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TimeCode
|
||||
*/
|
||||
public function getTimeCode()
|
||||
{
|
||||
return $this->timecode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the frame in the given filename.
|
||||
*
|
||||
* Uses the `unaccurate method by default.`
|
||||
*
|
||||
* @param string $pathfile
|
||||
* @param Boolean $accurate
|
||||
*
|
||||
* @return Frame
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function saveAs($pathfile, $accurate = false)
|
||||
{
|
||||
/**
|
||||
* @see http://ffmpeg.org/ffmpeg.html#Main-options
|
||||
*/
|
||||
if (!$accurate) {
|
||||
$commands = array(
|
||||
'-ss', (string) $this->timecode,
|
||||
'-i', $this->pathfile,
|
||||
'-vframes', '1',
|
||||
'-f', 'image2', $pathfile
|
||||
);
|
||||
} else {
|
||||
$commands = array(
|
||||
'-i', $this->pathfile,
|
||||
'-vframes', '1', '-ss', (string) $this->timecode,
|
||||
'-f', 'image2', $pathfile
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->driver->command($commands);
|
||||
} catch (ExecutionFailureException $e) {
|
||||
$this->cleanupTemporaryFile($pathfile);
|
||||
throw new RuntimeException('Unable to save frame', $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,20 +9,17 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace FFMpeg\Format\Audio;
|
||||
namespace FFMpeg\Media;
|
||||
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
|
||||
/**
|
||||
* @author Romain Neutron imprec@gmail.com
|
||||
*/
|
||||
interface Transcodable extends AudioInterface
|
||||
interface MediaTypeInterface
|
||||
{
|
||||
/**
|
||||
* Returns the available filters
|
||||
*/
|
||||
public function filters();
|
||||
|
||||
/**
|
||||
* Returns the audio codec
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAudioCodec();
|
||||
public function getPathfile();
|
||||
}
|
||||
166
src/FFMpeg/Media/Video.php
Normal file
166
src/FFMpeg/Media/Video.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?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\Media;
|
||||
|
||||
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
||||
use FFMpeg\Coordinate\TimeCode;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\Filters\Video\VideoFilters;
|
||||
use FFMpeg\Filters\Video\VideoFilterInterface;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Format\ProgressableInterface;
|
||||
use FFMpeg\Media\Frame;
|
||||
|
||||
class Video extends AbstractStreamableMedia
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return VideoFilters
|
||||
*/
|
||||
public function filters()
|
||||
{
|
||||
return new VideoFilters($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Video
|
||||
*/
|
||||
public function addFilter(VideoFilterInterface $filter)
|
||||
{
|
||||
$this->filters->add($filter);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the video in the desired format, applies registered filters
|
||||
*
|
||||
* @param FormatInterface $format
|
||||
* @param string $outputPathfile
|
||||
*
|
||||
* @return Video
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function save(VideoInterface $format, $outputPathfile)
|
||||
{
|
||||
$commands = array_merge(array('-y', '-i', $this->pathfile), $format->getExtraParams());
|
||||
|
||||
foreach ($this->filters as $filter) {
|
||||
$commands = array_merge($commands, $filter->apply($this, $format));
|
||||
}
|
||||
|
||||
if ($this->driver->getConfiguration()->has('ffmpeg.threads')) {
|
||||
$commands[] = '-threads';
|
||||
$commands[] = $this->driver->getConfiguration()->get('ffmpeg.threads');
|
||||
}
|
||||
|
||||
if (null !== $format->getVideoCodec()) {
|
||||
$commands[] = '-vcodec';
|
||||
$commands[] = $format->getVideoCodec();
|
||||
}
|
||||
if (null !== $format->getAudioCodec()) {
|
||||
$commands[] = '-acodec';
|
||||
$commands[] = $format->getAudioCodec();
|
||||
}
|
||||
|
||||
$commands[] = '-b:v';
|
||||
$commands[] = $format->getKiloBitrate() . 'k';
|
||||
$commands[] = '-refs';
|
||||
$commands[] = '6';
|
||||
$commands[] = '-coder';
|
||||
$commands[] = '1';
|
||||
$commands[] = '-sc_threshold';
|
||||
$commands[] = '40';
|
||||
$commands[] = '-flags';
|
||||
$commands[] = '+loop';
|
||||
$commands[] = '-me_range';
|
||||
$commands[] = '16';
|
||||
$commands[] = '-subq';
|
||||
$commands[] = '7';
|
||||
$commands[] = '-i_qfactor';
|
||||
$commands[] = '0.71';
|
||||
$commands[] = '-qcomp';
|
||||
$commands[] = '0.6';
|
||||
$commands[] = '-qdiff';
|
||||
$commands[] = '4';
|
||||
$commands[] = '-trellis';
|
||||
$commands[] = '1';
|
||||
$commands[] = '-b:a';
|
||||
$commands[] = $format->getAudioKiloBitrate() . 'k';
|
||||
|
||||
$passPrefix = uniqid('pass-');
|
||||
|
||||
$pass1 = $commands;
|
||||
$pass2 = $commands;
|
||||
|
||||
$pass1[] = '-pass';
|
||||
$pass1[] = '1';
|
||||
$pass1[] = '-passlogfile';
|
||||
$pass1[] = $passPrefix;
|
||||
$pass1[] = '-an';
|
||||
$pass1[] = $outputPathfile;
|
||||
|
||||
$pass2[] = '-pass';
|
||||
$pass2[] = '2';
|
||||
$pass2[] = '-passlogfile';
|
||||
$pass2[] = $passPrefix;
|
||||
$pass2[] = '-ac';
|
||||
$pass2[] = '2';
|
||||
$pass2[] = '-ar';
|
||||
$pass2[] = '44100';
|
||||
$pass2[] = $outputPathfile;
|
||||
|
||||
$failure = null;
|
||||
|
||||
foreach (array($pass1, $pass2) as $pass => $passCommands) {
|
||||
try {
|
||||
/** add listeners here */
|
||||
$listeners = null;
|
||||
|
||||
if ($format instanceof ProgressableInterface) {
|
||||
$listeners = $format->createProgressListener($this, $this->ffprobe, $pass + 1, 2);
|
||||
}
|
||||
|
||||
$this->driver->command($passCommands, false, $listeners);
|
||||
} catch (ExecutionFailureException $e) {
|
||||
$failure = $e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this
|
||||
->cleanupTemporaryFile(getcwd() . '/' . $passPrefix . '-0.log')
|
||||
->cleanupTemporaryFile(getcwd() . '/' . $passPrefix . '-0.log')
|
||||
->cleanupTemporaryFile(getcwd() . '/' . $passPrefix . '-0.log.mbtree');
|
||||
|
||||
if (null !== $failure) {
|
||||
throw new RuntimeException('Encoding failed', $failure->getCode(), $failure);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frame at timecode
|
||||
*
|
||||
* @param Timecode $at
|
||||
* @return Frame
|
||||
*/
|
||||
public function frame(Timecode $at)
|
||||
{
|
||||
return new Frame($this->pathfile, $this->driver, $this->ffprobe, $at);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue