Version 0.3

This commit is contained in:
Romain Neutron 2013-06-25 10:03:20 +02:00
commit ad3a5af623
130 changed files with 7283 additions and 2627 deletions

View file

@ -17,25 +17,23 @@
}
],
"require": {
"php" : ">=5.3.3",
"symfony/process" : "~2.1",
"monolog/monolog" : "~1.0"
"php" : ">=5.3.3",
"alchemy/binary-driver" : "~1.5",
"doctrine/cache" : "~1.0",
"evenement/evenement" : "~1.0",
"symfony/process" : "~2.0"
},
"suggest": {
"php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg"
"php-ffmpeg/extras" : "A compilation of common audio & video drivers for PHP-FFMpeg"
},
"require-dev": {
"sami/sami" : "~1.0",
"silex/silex" : "~1.0"
"sami/sami" : "dev-master@dev",
"silex/silex" : "~1.0",
"phpunit/phpunit" : "~3.7"
},
"autoload": {
"psr-0": {
"FFMpeg": "src"
}
},
"extra": {
"branch-alias": {
"dev-master": "0.3-dev"
}
}
}

799
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="true"
verbose="false"
bootstrap="tests/bootstrap.php"
>
<testsuites>
<testsuite name="FFMpeg Tests Suite">
<directory>tests/FFMpeg/Functional</directory>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>vendor</directory>
<directory>tests</directory>
</blacklist>
</filter>
</phpunit>

View file

@ -9,20 +9,11 @@
stopOnFailure="false"
syntaxCheck="true"
verbose="false"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
>
<logging>
<log type="coverage-html" target="tests/phpunit_report/report" charset="UTF-8"
yui="true" highlight="false"
lowUpperBound="35" highLowerBound="70"/>
</logging>
<php>
<ini name="display_errors" value="on"/>
</php>
<testsuites>
<testsuite name="FFMpeg Tests Suite">
<directory>tests</directory>
<directory>tests/FFMpeg/Tests</directory>
</testsuite>
</testsuites>
<filter>

View file

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

View 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;
}
}

View file

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

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -13,5 +13,4 @@ namespace FFMpeg\Exception;
interface ExceptionInterface
{
}

View file

@ -13,5 +13,4 @@ namespace FFMpeg\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -13,5 +13,4 @@ namespace FFMpeg\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View file

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

View file

@ -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']);
});
}

View file

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

View 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);
}
}

View file

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

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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);
}

View 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;
}
}

View file

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

View 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);
}
}

View 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);
}

View 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);
}

View 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;
}
}

View 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);
}
}

View 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
{
}

View 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);
}
}

View 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);
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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);
}

View 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;
}
}

View 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
{
}

View 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;
}
}

View file

@ -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()
{

View file

@ -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()
{

View 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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}
}

View 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);
}
}

View 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
View 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;
}
}

View file

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

View file

@ -0,0 +1,13 @@
<?php
namespace FFMpeg\Functional;
use FFMpeg\FFMpeg;
abstract class FunctionalTestCase extends \PHPUnit_Framework_TestCase
{
public function getFFMpeg()
{
return FFMpeg::create(array('timeout' => 300));
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace FFMpeg\Functional;
use FFMpeg\Format\Video\X264;
class VideoTranscodeTest extends FunctionalTestCase
{
public function testSimpleTranscodeX264()
{
$filename = __DIR__ . '/output/output-x264.mp4';
if (is_file($filename)) {
unlink(__DIR__ . '/output/output-x264.mp4');
}
$ffmpeg = $this->getFFMpeg();
$video = $ffmpeg->open(__DIR__ . '/../../files/Test.ogv');
$this->assertInstanceOf('FFMpeg\Media\Video', $video);
$lastPercentage = null;
$phpunit = $this;
$codec = new X264('libvo_aacenc');
$codec->on('progress', function ($video, $codec, $percentage) use ($phpunit, &$lastPercentage) {
if (null !== $lastPercentage) {
$phpunit->assertGreaterThanOrEqual($lastPercentage, $percentage);
}
$lastPercentage = $percentage;
$phpunit->assertGreaterThanOrEqual(0, $percentage);
$phpunit->assertLessThanOrEqual(100, $percentage);
});
$video->save($codec, $filename);
$this->assertFileExists($filename);
unlink($filename);
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace FFMpeg\Tests\Coordinate;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\Tests\TestCase;
use FFMpeg\Coordinate\AspectRatio;
class AspectRatioTest extends TestCase
{
/**
* @dataProvider provideDimensionsAndExpectedratio
*/
public function testFromDimensions($width, $height, $strategy, $expected, $calculatedWidth, $calculatedHeight, $modulus = 2)
{
$ratio = AspectRatio::create(new Dimension($width, $height), $strategy);
$this->assertEquals($expected, $ratio->getValue());
$this->assertEquals($calculatedHeight, $ratio->calculateHeight(240, $modulus));
$this->assertEquals($calculatedWidth, $ratio->calculateWidth(320, $modulus));
}
public function provideDimensionsAndExpectedratio()
{
return array(
//AR_5_4
array(720, 576, false, 5/4, 400, 192),
array(720, 577, false, 5/4, 400, 192),
array(720, 620, false, 720/620, 372, 206),
array(720, 576, true, 5/4, 400, 192),
//AR_ROTATED_4_5
array(576, 720, false, 4/5, 256, 300),
array(576, 720, true, 4/5, 256, 300),
//AR_4_3
array(320, 240, false, 4/3, 426, 180),
array(320, 240, true, 4/3, 426, 180),
//AR_ROTATED_3_4
array(240, 320, false, 3/4, 240, 320),
array(240, 320, true, 3/4, 240, 320),
//AR_16_9
array(1920, 1080, false, 16/9, 568, 136),
array(1920, 1080, true, 16/9, 568, 136),
array(1280, 720, false, 16/9, 568, 136),
array(1280, 720, true, 16/9, 568, 136),
array(3840, 2160, false, 16/9, 568, 136),
array(3840, 2160, true, 16/9, 568, 136),
// modulus 4
array(1920, 1080, false, 16/9, 568, 136, 4),
array(1920, 1080, true, 16/9, 568, 136, 4),
array(1280, 720, false, 16/9, 568, 136, 4),
array(1280, 720, true, 16/9, 568, 136, 4),
array(3840, 2160, false, 16/9, 568, 136, 4),
array(3840, 2160, true, 16/9, 568, 136, 4),
// modulus 16
array(1920, 1080, false, 16/9, 576, 128, 16),
array(1920, 1080, true, 16/9, 576, 128, 16),
array(1280, 720, false, 16/9, 576, 128, 16),
array(1280, 720, true, 16/9, 576, 128, 16),
array(3840, 2160, false, 16/9, 576, 128, 16),
array(3840, 2160, true, 16/9, 576, 128, 16),
//AR_ROTATED_9_16
array(1080, 1920, false, 9/16, 180, 426),
array(1080, 1920, true, 9/16, 180, 426),
array(720, 1280, false, 9/16, 180, 426),
array(720, 1280, true, 9/16, 180, 426),
array(2160, 3840, false, 9/16, 180, 426),
array(2160, 3840, true, 9/16, 180, 426),
//AR_3_2
array(360, 240, false, 3/2, 480, 160),
array(360, 240, true, 3/2, 480, 160),
//AR_ROTATED_2_3
array(240, 360, false, 2/3, 214, 360),
array(240, 360, true, 2/3, 214, 360),
//AR_5_3
//AR_ROTATED_3_5
//AR_1_1
//AR_1_DOT_85_1
//AR_ROTATED_1_DOT_85
//AR_2_DOT_39_1
//AR_ROTATED_2_DOT_39
);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace FFMpeg\Tests\Coordinate;
use FFMpeg\Tests\TestCase;
use FFMpeg\Coordinate\Dimension;
class DimensionTest extends TestCase
{
/**
* @dataProvider provideInvalidDimensions
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testInvalidDimensions($width, $height)
{
new Dimension($width, $height);
}
public function provideInvalidDimensions()
{
return array(
array(320, 0),
array(320, -10),
array(0, 240),
array(-10, 240),
array(0, 0),
array(0, -10),
array(-10, 0),
);
}
public function testGetters()
{
$dimension = new Dimension(320, 240);
$this->assertEquals(320, $dimension->getWidth());
$this->assertEquals(240, $dimension->getHeight());
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace FFMpeg\Tests\Coordinate;
use FFMpeg\Tests\TestCase;
use FFMpeg\Coordinate\FrameRate;
class FrameRateTest extends TestCase
{
public function testGetter()
{
$fr = new FrameRate(23.997);
$this->assertEquals(23.997, $fr->getValue());
}
/**
* @dataProvider provideInvalidFrameRates
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testInvalidFrameRate($value)
{
new FrameRate($value);
}
public function provideInvalidFrameRates()
{
return array(
array(0), array(-1.5), array(-2),
);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace FFMpeg\Tests\Coordinate;
use FFMpeg\Tests\TestCase;
use FFMpeg\Coordinate\Point;
class PointTest extends TestCase
{
public function testGetters()
{
$point = new Point(4, 25);
$this->assertEquals(4, $point->getX());
$this->assertEquals(25, $point->getY());
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace FFMpeg\Tests\Coordinate;
use FFMpeg\Tests\TestCase;
use FFMpeg\Coordinate\TimeCode;
class TimeCodeTest extends TestCase
{
/**
* @dataProvider provideTimecodes
*/
public function testFromString($timecode, $expected)
{
$tc = TimeCode::fromString($timecode);
$this->assertEquals((string) $tc, $expected);
}
public function provideTimeCodes()
{
return array(
array('1:02:04:05:20', '26:04:05.20'),
array('1:02:04:05.20', '26:04:05.20'),
array('02:04:05:20', '02:04:05.20'),
array('02:04:05.20', '02:04:05.20'),
array('00:00:05.20', '00:00:05.20'),
array('00:00:00.00', '00:00:00.00'),
);
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testFromInvalidString()
{
TimeCode::fromString('lalali lala');
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace FFMpeg\Tests\Driver;
use Alchemy\BinaryDriver\Configuration;
use FFMpeg\Driver\FFMpegDriver;
use FFMpeg\Tests\TestCase;
use Symfony\Component\Process\ExecutableFinder;
class FFMpegDriverTest extends TestCase
{
public function setUp()
{
$executableFinder = new ExecutableFinder();
$found = false;
foreach (array('avconv', 'ffmpeg') as $name) {
if (null !== $executableFinder->find($name)) {
$found = true;
break;
}
}
if (!$found) {
$this->markTestSkipped('Neither ffmpeg or avconv found');
}
}
public function testCreate()
{
$logger = $this->getLoggerMock();
$ffmpeg = FFMpegDriver::create($logger, array());
$this->assertInstanceOf('FFMpeg\Driver\FFMpegDriver', $ffmpeg);
$this->assertEquals($logger, $ffmpeg->getProcessRunner()->getLogger());
}
public function testCreateWithConfig()
{
$conf = new Configuration();
$ffmpeg = FFMpegDriver::create($this->getLoggerMock(), $conf);
$this->assertEquals($conf, $ffmpeg->getConfiguration());
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace FFMpeg\Tests\Driver;
use Alchemy\BinaryDriver\Configuration;
use FFMpeg\Driver\FFProbeDriver;
use FFMpeg\Tests\TestCase;
use Symfony\Component\Process\ExecutableFinder;
class FFProbeDriverTest extends TestCase
{
public function setUp()
{
$executableFinder = new ExecutableFinder();
$found = false;
foreach (array('avprobe', 'ffprobe') as $name) {
if (null !== $executableFinder->find($name)) {
$found = true;
break;
}
}
if (!$found) {
$this->markTestSkipped('Neither ffprobe or avprobe found');
}
}
public function testCreate()
{
$logger = $this->getLoggerMock();
$ffprobe = FFProbeDriver::create(array(), $logger);
$this->assertInstanceOf('FFMpeg\Driver\FFProbeDriver', $ffprobe);
$this->assertEquals($logger, $ffprobe->getProcessRunner()->getLogger());
}
public function testCreateWithConfig()
{
$conf = new Configuration();
$ffprobe = FFProbeDriver::create($conf, $this->getLoggerMock());
$this->assertEquals($conf, $ffprobe->getConfiguration());
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace FFMpeg\Tests;
use FFMpeg\FFMpegServiceProvider;
use Silex\Application;
class FFMpegServiceProviderTest extends \PHPUnit_Framework_TestCase
{
public function testWithConfig()
{
$app = new Application();
$app->register(new FFMpegServiceProvider(), array(
'ffmpeg.configuration' => array(
'ffmpeg.threads' => 12,
'ffmpeg.timeout' => 10666,
'ffprobe.timeout' => 4242,
)
));
$this->assertInstanceOf('FFMpeg\FFMpeg', $app['ffmpeg']);
$this->assertSame($app['ffmpeg'], $app['ffmpeg.ffmpeg']);
$this->assertInstanceOf('FFMpeg\FFProbe', $app['ffmpeg.ffprobe']);
$this->assertEquals(12, $app['ffmpeg']->getFFMpegDriver()->getConfiguration()->get('ffmpeg.threads'));
$this->assertEquals(10666, $app['ffmpeg']->getFFMpegDriver()->getProcessBuilderFactory()->getTimeout());
$this->assertEquals(4242, $app['ffmpeg.ffprobe']->getFFProbeDriver()->getProcessBuilderFactory()->getTimeout());
}
public function testWithoutConfig()
{
$app = new Application();
$app->register(new FFMpegServiceProvider());
$this->assertInstanceOf('FFMpeg\FFMpeg', $app['ffmpeg']);
$this->assertSame($app['ffmpeg'], $app['ffmpeg.ffmpeg']);
$this->assertInstanceOf('FFMpeg\FFProbe', $app['ffmpeg.ffprobe']);
$this->assertEquals(4, $app['ffmpeg']->getFFMpegDriver()->getConfiguration()->get('ffmpeg.threads'));
$this->assertEquals(300, $app['ffmpeg']->getFFMpegDriver()->getProcessBuilderFactory()->getTimeout());
$this->assertEquals(30, $app['ffmpeg.ffprobe']->getFFProbeDriver()->getProcessBuilderFactory()->getTimeout());
}
public function testWithFFMpegBinaryConfig()
{
$app = new Application();
$app->register(new FFMpegServiceProvider(), array(
'ffmpeg.configuration' => array(
'ffmpeg.binaries' => '/path/to/ffmpeg',
)
));
$this->setExpectedException('Alchemy\BinaryDriver\Exception\ExecutableNotFoundException', 'Executable not found, proposed : /path/to/ffmpeg');
$app['ffmpeg'];
}
public function testWithFFMprobeBinaryConfig()
{
$app = new Application();
$app->register(new FFMpegServiceProvider(), array(
'ffmpeg.configuration' => array(
'ffprobe.binaries' => '/path/to/ffprobe',
)
));
$this->setExpectedException('Alchemy\BinaryDriver\Exception\ExecutableNotFoundException', 'Executable not found, proposed : /path/to/ffprobe');
$app['ffmpeg.ffprobe'];
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace FFMpeg\Tests;
use FFMpeg\FFMpeg;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\DataMapping\StreamCollection;
use FFMpeg\FFProbe\DataMapping\Stream;
class FFMpegTest Extends TestCase
{
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testOpenInvalid()
{
$ffmpeg = new FFMpeg($this->getFFMpegDriverMock(), $this->getFFProbeMock());
$ffmpeg->open('/path/to/unknown/file');
}
public function testOpenAudio()
{
$streams = $this->getStreamCollectionMock();
$streams->expects($this->once())
->method('audios')
->will($this->returnValue(new StreamCollection(array(new Stream(array())))));
$streams->expects($this->once())
->method('videos')
->will($this->returnValue(array()));
$ffprobe = $this->getFFProbeMock();
$ffprobe->expects($this->once())
->method('streams')
->with(__FILE__)
->will($this->returnValue($streams));
$ffmpeg = new FFMpeg($this->getFFMpegDriverMock(), $ffprobe);
$this->assertInstanceOf('FFMpeg\Media\Audio', $ffmpeg->open(__FILE__));
}
public function testOpenVideo()
{
$streams = $this->getStreamCollectionMock();
$streams->expects($this->once())
->method('videos')
->will($this->returnValue(new StreamCollection(array(new Stream(array())))));
$streams->expects($this->never())
->method('audios');
$ffprobe = $this->getFFProbeMock();
$ffprobe->expects($this->once())
->method('streams')
->with(__FILE__)
->will($this->returnValue($streams));
$ffmpeg = new FFMpeg($this->getFFMpegDriverMock(), $ffprobe);
$this->assertInstanceOf('FFMpeg\Media\Video', $ffmpeg->open(__FILE__));
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testOpenUnknown()
{
$ffprobe = $this->getFFProbeMock();
$ffprobe->expects($this->once())
->method('streams')
->with(__FILE__)
->will($this->returnValue(new StreamCollection()));
$ffmpeg = new FFMpeg($this->getFFMpegDriverMock(), $ffprobe);
$ffmpeg->open(__FILE__);
}
public function testCreateWithoutLoggerOrProbe()
{
$this->assertInstanceOf('FFMpeg\FFMpeg', FFMpeg::create());
}
public function testCreateWithLoggerAndProbe()
{
$logger = $this->getLoggerMock();
$ffprobe = $this->getFFProbeMock();
$ffmpeg = FFMpeg::create(array('timeout' => 42), $logger, $ffprobe);
$this->assertInstanceOf('FFMpeg\FFMpeg', $ffmpeg);
$this->assertSame($logger, $ffmpeg->getFFMpegDriver()->getProcessRunner()->getLogger());
$this->assertSame($ffprobe, $ffmpeg->getFFProbe());
$this->assertSame(42, $ffmpeg->getFFMpegDriver()->getProcessBuilderFactory()->getTimeout());
}
public function testGetSetFFProbe()
{
$ffprobe = $this->getFFProbeMock();
$ffmpeg = new FFMpeg($this->getFFMpegDriverMock(), $ffprobe);
$this->assertSame($ffprobe, $ffmpeg->getFFProbe());
$anotherFFProbe = $this->getFFProbeMock();
$ffmpeg->setFFProbe($anotherFFProbe);
$this->assertSame($anotherFFProbe, $ffmpeg->getFFProbe());
}
public function testGetSetDriver()
{
$driver = $this->getFFMpegDriverMock();
$ffmpeg = new FFMpeg($driver, $this->getFFProbeMock());
$this->assertSame($driver, $ffmpeg->getFFMpegDriver());
$anotherDriver = $this->getFFMpegDriverMock();
$ffmpeg->setFFMpegDriver($anotherDriver);
$this->assertSame($anotherDriver, $ffmpeg->getFFMpegDriver());
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace FFMpeg\Tests\FFProbe\DataMapping;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\DataMapping\AbstractData;
class AbstractDataTest extends TestCase
{
public function testHas()
{
$imp = new Implementation(array('key1' => 'value1', 'key2' => 'value2'));
$this->assertTrue($imp->has('key1'));
$this->assertTrue($imp->has('key2'));
$this->assertFalse($imp->has('value1'));
$this->assertFalse($imp->has('key3'));
}
public function testGet()
{
$imp = new Implementation(array('key1' => 'value1', 'key2' => 'value2'));
$this->assertEquals('value1', $imp->get('key1'));
$this->assertEquals('value2', $imp->get('key2'));
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testGetInvalid()
{
$imp = new Implementation(array('key1' => 'value1', 'key2' => 'value2'));
$imp->get('key3');
}
public function testKeys()
{
$imp = new Implementation(array('key1' => 'value1', 'key2' => 'value2'));
$this->assertEquals(array('key1', 'key2'), $imp->keys());
}
public function testAll()
{
$values = array('key1' => 'value1', 'key2' => 'value2');
$imp = new Implementation($values);
$this->assertEquals($values, $imp->all());
}
}
class Implementation extends AbstractData
{
}

View file

@ -0,0 +1,89 @@
<?php
namespace FFMpeg\Tests\FFProbe\DataMapping;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\DataMapping\StreamCollection;
class StreamCollectionTest extends TestCase
{
public function testAdd()
{
$stream = $this->getStreamMock();
$collection = new StreamCollection();
$this->assertEquals(array(), $collection->all());
$collection->add($stream);
$this->assertEquals(array($stream), $collection->all());
$collection->add($stream);
$this->assertEquals(array($stream, $stream), $collection->all());
}
public function testVideos()
{
$audio = $this->getStreamMock();
$audio->expects($this->once())
->method('isVideo')
->will($this->returnValue(false));
$video = $this->getStreamMock();
$video->expects($this->once())
->method('isVideo')
->will($this->returnValue(true));
$collection = new StreamCollection(array($audio, $video));
$videos = $collection->videos();
$this->assertInstanceOf('FFMpeg\FFProbe\DataMapping\StreamCollection', $videos);
$this->assertCount(1, $videos);
$this->assertEquals(array($video), $videos->all());
}
public function testAudios()
{
$audio = $this->getStreamMock();
$audio->expects($this->once())
->method('isAudio')
->will($this->returnValue(true));
$video = $this->getStreamMock();
$video->expects($this->once())
->method('isAudio')
->will($this->returnValue(false));
$collection = new StreamCollection(array($audio, $video));
$audios = $collection->audios();
$this->assertInstanceOf('FFMpeg\FFProbe\DataMapping\StreamCollection', $audios);
$this->assertCount(1, $audios);
$this->assertEquals(array($audio), $audios->all());
}
public function testCount()
{
$stream = $this->getStreamMock();
$collection = new StreamCollection(array($stream));
$this->assertCount(1, $collection);
}
public function testGetIterator()
{
$audio = $this->getStreamMock();
$video = $this->getStreamMock();
$collection = new StreamCollection(array($audio, $video));
$this->assertInstanceOf('\Iterator', $collection->getIterator());
$this->assertCount(2, $collection->getIterator());
}
public function testFirst()
{
$stream1 = $this->getStreamMock();
$stream2 = $this->getStreamMock();
$coll = new StreamCollection(array($stream1, $stream2));
$this->assertSame($stream1, $coll->first());
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace FFMpeg\Tests\FFProbe\DataMapping;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\DataMapping\Stream;
class StreamTest extends TestCase
{
/**
* @dataProvider provideAudioCases
*/
public function testIsAudio($isAudio, $properties)
{
$stream = new Stream($properties);
$this->assertTrue($isAudio === $stream->isAudio());
}
public function provideAudioCases()
{
return array(
array(true, array('codec_type' => 'audio')),
array(false, array('codec_type' => 'video')),
);
}
/**
* @dataProvider provideVideoCases
*/
public function testIsVideo($isVideo, $properties)
{
$stream = new Stream($properties);
$this->assertTrue($isVideo === $stream->isVideo());
}
public function provideVideoCases()
{
return array(
array(true, array('codec_type' => 'video')),
array(false, array('codec_type' => 'audio')),
);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace FFMpeg\Tests\FFProbe;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\Mapper;
use FFMpeg\FFProbe;
use FFMpeg\FFProbe\DataMapping\Format;
use FFMpeg\FFProbe\DataMapping\Stream;
use FFMpeg\FFProbe\DataMapping\StreamCollection;
class MapperTest extends TestCase
{
/**
* @dataProvider provideMappings
*/
public function testMap($type, $data, $expected)
{
$mapper = new Mapper();
$this->assertEquals($expected, $mapper->map($type, $data));
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testMapInvalidArgument()
{
$mapper = new Mapper();
$mapper->map('cool type', 'data');
}
public function provideMappings()
{
$format = json_decode(file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/show_format.json'), true);
$streams = json_decode(file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/show_streams.json'), true);
return array(
array(FFProbe::TYPE_FORMAT, $format, new Format($format['format'])),
array(FFProbe::TYPE_STREAMS, $streams, new StreamCollection(array_map(function ($streamData) {
return new Stream($streamData);
}, $streams['streams']))),
);
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace FFMpeg\Tests\FFProbe;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\OptionsTester;
class OptionsTesterTest extends TestCase
{
/**
* @dataProvider provideOptions
*/
public function testHasOptionWithCacheEmpty($isPresent, $data, $optionName)
{
$cache = $this->getCacheMock();
$cache->expects($this->never())
->method('fetch');
$cache->expects($this->exactly(2))
->method('contains')
->will($this->returnValue(false));
$cache->expects($this->exactly(2))
->method('save');
$ffprobe = $this->getFFProbeDriverMock();
$ffprobe->expects($this->once())
->method('command')
->with(array('-help', '-loglevel', 'quiet'))
->will($this->returnValue($data));
$tester = new OptionsTester($ffprobe, $cache);
$this->assertTrue($isPresent === $tester->has($optionName));
}
public function provideOptions()
{
$data = file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/help.raw');
return array(
array(true, $data, '-print_format'),
array(false, $data, '-another_print_format'),
);
}
/**
* @dataProvider provideOptions
*/
public function testHasOptionWithHelpCacheLoaded($isPresent, $data, $optionName)
{
$cache = $this->getCacheMock();
$cache->expects($this->once())
->method('fetch')
->will($this->returnValue($data));
$cache->expects($this->at(0))
->method('contains')
->will($this->returnValue(false));
$cache->expects($this->at(1))
->method('contains')
->will($this->returnValue(true));
$cache->expects($this->once())
->method('save');
$ffprobe = $this->getFFProbeDriverMock();
$ffprobe->expects($this->never())
->method('command');
$tester = new OptionsTester($ffprobe, $cache);
$this->assertTrue($isPresent === $tester->has($optionName));
}
/**
* @dataProvider provideOptions
*/
public function testHasOptionWithCacheFullyLoaded($isPresent, $data, $optionName)
{
$cache = $this->getCacheMock();
$cache->expects($this->once())
->method('fetch')
->with('option-' . $optionName)
->will($this->returnValue($isPresent));
$cache->expects($this->once())
->method('contains')
->with('option-' . $optionName)
->will($this->returnValue(true));
$ffprobe = $this->getFFProbeDriverMock();
$ffprobe->expects($this->never())
->method('command');
$tester = new OptionsTester($ffprobe, $cache);
$this->assertTrue($isPresent === $tester->has($optionName));
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace FFMpeg\Tests\FFProbe;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\OutputParser;
use FFMpeg\FFProbe;
class OutputParserTest extends TestCase
{
/**
* @dataProvider provideTypeDataAndOutput
*/
public function testParse($type, $data, $expectedOutput)
{
$parser = new OutputParser();
$this->assertEquals($expectedOutput, $parser->parse($type, $data));
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testParseWithInvalidArgument()
{
$parser = new OutputParser();
$parser->parse('comme ca', 'data');
}
public function provideTypeDataAndOutput()
{
$expectedFormat = json_decode(file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/show_format.json'), true);
$expectedStreams = json_decode(file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/show_streams.json'), true);
$rawFormat = file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/show_format.raw');
$rawStreams = file_get_contents(__DIR__ . '/../../../fixtures/ffprobe/show_streams.raw');
return array(
array(FFProbe::TYPE_FORMAT, $rawFormat, $expectedFormat),
array(FFProbe::TYPE_STREAMS, $rawStreams, $expectedStreams),
);
}
}

View file

@ -0,0 +1,308 @@
<?php
namespace FFMpeg\Tests;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe;
use Symfony\Component\Process\ExecutableFinder;
use Alchemy\BinaryDriver\ConfigurationInterface;
use Alchemy\BinaryDriver\Configuration;
class FFProbeTest extends TestCase
{
public function testGetSetParser()
{
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$parser = $this->getFFProbeParserMock();
$ffprobe->setParser($parser);
$this->assertSame($parser, $ffprobe->getParser());
}
public function testGetSetFFProbeDriver()
{
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$driver = $this->getFFProbeDriverMock();
$ffprobe->setFFProbeDriver($driver);
$this->assertSame($driver, $ffprobe->getFFProbeDriver());
}
public function testGetSetFFProbeMapper()
{
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$mapper = $this->getFFProbeMapperMock();
$ffprobe->setMapper($mapper);
$this->assertSame($mapper, $ffprobe->getMapper());
}
public function testGetSetOptionsTester()
{
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$tester = $this->getFFProbeOptionsTesterMock();
$ffprobe->setOptionsTester($tester);
$this->assertSame($tester, $ffprobe->getOptionsTester());
}
public function testGetSetCache()
{
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$cache = $this->getCacheMock();
$ffprobe->setCache($cache);
$this->assertSame($cache, $ffprobe->getCache());
}
public function provideDataWhitoutCache()
{
$stream = $this->getStreamMock();
$format = $this->getFormatMock();
return array(
array($stream, 'streams', array('-show_streams', '-print_format'), FFProbe::TYPE_STREAMS, array(__FILE__, '-show_streams', '-print_format', 'json'), false),
array($format, 'format', array('-show_format', '-print_format'), FFProbe::TYPE_FORMAT, array(__FILE__, '-show_format', '-print_format', 'json'), false),
array($stream, 'streams', array('-show_streams'), FFProbe::TYPE_STREAMS, array(__FILE__, '-show_streams'), true),
array($format, 'format', array('-show_format'), FFProbe::TYPE_FORMAT, array(__FILE__, '-show_format'), true),
);
}
/**
* @dataProvider provideDataWhitoutCache
*/
public function testProbeWithoutCache($output, $method, $commands, $type, $caughtCommands, $isRaw)
{
$pathfile = __FILE__;
$data = array('key' => 'value');
$rawData = 'raw data';
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$mapper = $this->getFFProbeMapperMock();
$mapper->expects($this->once())
->method('map')
->with($type, $data)
->will($this->returnValue($output));
$parser = $this->getFFProbeParserMock();
if ($isRaw) {
$parser->expects($this->once())
->method('parse')
->with($type, $rawData)
->will($this->returnValue($data));
} else {
$parser->expects($this->never())
->method('parse');
}
$tester = $this->getFFProbeOptionsTesterMockWithOptions($commands);
$cache = $this->getCacheMock();
$cache->expects($this->once())
->method('contains')
->will($this->returnValue(false));
$cache->expects($this->never())
->method('fetch');
$cache->expects($this->once())
->method('save')
->with($this->anything(), $output);
$driver = $this->getFFProbeDriverMock();
$driver->expects($this->once())
->method('command')
->with($caughtCommands)
->will($this->returnValue($isRaw ? $rawData : json_encode($data)));
$ffprobe->setOptionsTester($tester)
->setCache($cache)
->setMapper($mapper)
->setFFProbeDriver($driver)
->setParser($parser);
$this->assertEquals($output, call_user_func(array($ffprobe, $method), $pathfile));
}
public function provideDataForInvalidJson()
{
$stream = $this->getStreamMock();
$format = $this->getFormatMock();
return array(
array($stream, 'streams', array('-show_streams', '-print_format'), FFProbe::TYPE_STREAMS, array(__FILE__, '-show_streams', '-print_format', 'json')),
array($format, 'format', array('-show_format', '-print_format'), FFProbe::TYPE_FORMAT, array(__FILE__, '-show_format', '-print_format', 'json')),
);
}
/**
* @dataProvider provideDataForInvalidJson
*/
public function testProbeWithWrongJson($output, $method, $commands, $type, $caughtCommands)
{
$pathfile = __FILE__;
$data = array('key' => 'value');
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$mapper = $this->getFFProbeMapperMock();
$mapper->expects($this->once())
->method('map')
->with($this->isType('string'), 'good data parsed')
->will($this->returnValue($output));
$parser = $this->getFFProbeParserMock();
$parser->expects($this->once())
->method('parse')
->with($this->isType('string', json_encode($data) . 'lala'))
->will($this->returnValue('good data parsed'));
$tester = $this->getFFProbeOptionsTesterMockWithOptions($commands);
$cache = $this->getCacheMock();
$cache->expects($this->exactly(2))
->method('contains')
->will($this->returnValue(false));
$cache->expects($this->never())
->method('fetch');
$driver = $this->getFFProbeDriverMock();
$driver->expects($this->exactly(2))
->method('command')
->will($this->returnValue(json_encode($data) . 'lala'));
$ffprobe->setOptionsTester($tester)
->setCache($cache)
->setMapper($mapper)
->setFFProbeDriver($driver)
->setParser($parser);
$this->assertEquals($output, call_user_func(array($ffprobe, $method), $pathfile));
}
public function provideProbingDataWithCache()
{
$stream = $this->getStreamMock();
$format = $this->getFormatMock();
return array(
array($stream, 'streams'),
array($format, 'format'),
);
}
/**
* @dataProvider provideProbingDataWithCache
*/
public function testProbeWithCache($output, $method)
{
$pathfile = __FILE__;
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$mapper = $this->getFFProbeMapperMock();
$mapper->expects($this->never())
->method('map');
$tester = $this->getFFProbeOptionsTesterMock();
$cache = $this->getCacheMock();
$cache->expects($this->once())
->method('contains')
->will($this->returnValue(true));
$cache->expects($this->once())
->method('fetch')
->will($this->returnValue($output));
$cache->expects($this->never())
->method('save');
$driver = $this->getFFProbeDriverMock();
$driver->expects($this->never())
->method('command');
$ffprobe->setOptionsTester($tester)
->setCache($cache)
->setMapper($mapper)
->setFFProbeDriver($driver);
$this->assertEquals($output, call_user_func(array($ffprobe, $method), $pathfile));
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
* @dataProvider provideProbeMethod
*/
public function testProbeWithInvalidFile($method)
{
$pathfile = '/path/to/nofile';
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
call_user_func(array($ffprobe, $method), $pathfile);
}
public function provideProbeMethod()
{
return array(
array('streams'),
array('format'),
);
}
/**
* @expectedException FFMpeg\Exception\RuntimeException
* @dataProvider provideProbeMethod
*/
public function testProbeWithoutShowStreamsAvailable($method)
{
$pathfile = __FILE__;
$ffprobe = new FFProbe($this->getFFProbeDriverMock(), $this->getCacheMock());
$ffprobe->setOptionsTester($this->getFFProbeOptionsTesterMock());
call_user_func(array($ffprobe, $method), $pathfile);
}
/**
* @dataProvider provideCreateOptions
*/
public function testCreate($logger, $conf, $cache)
{
$finder = new ExecutableFinder();
$found = false;
foreach (array('avprobe', 'ffprobe') as $name) {
if (null !== $finder->find($name)) {
$found = true;
}
}
if (!$found) {
$this->markTestSkipped("Unable to find avprobe or ffprobe on system");
}
$ffprobe = FFProbe::create();
$this->assertInstanceOf('FFMpeg\FFprobe', $ffprobe);
$ffprobe = FFProbe::create($conf, $logger, $cache);
$this->assertInstanceOf('FFMpeg\FFprobe', $ffprobe);
if (null !== $cache) {
$this->assertSame($cache, $ffprobe->getCache());
}
if (null !== $logger) {
$this->assertSame($logger, $ffprobe->getFFProbeDriver()->getProcessRunner()->getLogger());
}
if ($conf instanceof ConfigurationInterface) {
$this->assertSame($conf, $ffprobe->getFFProbeDriver()->getConfiguration());
}
}
public function provideCreateOptions()
{
return array(
array(null, array('key' => 'value'), null),
array($this->getLoggerMock(), array('key' => 'value'), null),
array(null, new Configuration(), null),
array(null, array('key' => 'value'), $this->getCacheMock()),
);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace FFMpeg\Tests\Filters\Audio;
use FFMpeg\Filters\Audio\AudioFilters;
use FFMpeg\Tests\TestCase;
class AudioFiltersTest extends TestCase
{
public function testResample()
{
$capturedFilter = null;
$audio = $this->getAudioMock();
$audio->expects($this->once())
->method('addFilter')
->with($this->isInstanceOf('FFMpeg\Filters\Audio\AudioResamplableFilter'))
->will($this->returnCallback(function ($filter) use (&$capturedFilter) {
$capturedFilter = $filter;
}));
$filters = new AudioFilters($audio);
$filters->resample(8000);
$this->assertEquals(8000, $capturedFilter->getRate());
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace FFMpeg\Tests\Filters\Audio;
use FFMpeg\Filters\Audio\AudioResamplableFilter;
use FFMpeg\Tests\TestCase;
class AudioResamplableFilterTest extends TestCase
{
public function testGetRate()
{
$filter = new AudioResamplableFilter(500);
$this->assertEquals(500, $filter->getRate());
}
public function testApply()
{
$audio = $this->getAudioMock();
$format = $this->getMock('FFMpeg\Format\AudioInterface');
$filter = new AudioResamplableFilter(500);
$this->assertEquals(array('-ac', 2, '-ar', 500), $filter->apply($audio, $format));
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace FFMpeg\Tests\Filters;
use FFMpeg\Filters\FiltersCollection;
class FiltersCollectionTest extends \PHPUnit_Framework_TestCase
{
public function testCount()
{
$coll = new FiltersCollection();
$this->assertCount(0, $coll);
$coll->add($this->getMock('FFMpeg\Filters\FilterInterface'));
$this->assertCount(1, $coll);
$coll->add($this->getMock('FFMpeg\Filters\FilterInterface'));
$this->assertCount(2, $coll);
}
public function testIterator()
{
$coll = new FiltersCollection();
$coll->add($this->getMock('FFMpeg\Filters\FilterInterface'));
$coll->add($this->getMock('FFMpeg\Filters\FilterInterface'));
$this->assertInstanceOf('\ArrayIterator', $coll->getIterator());
$this->assertCount(2, $coll->getIterator());
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace FFMpeg\Tests\Filters\Video;
use FFMpeg\Filters\Video\ResizeFilter;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\DataMapping\Stream;
use FFMpeg\FFProbe\DataMapping\StreamCollection;
use FFMpeg\Coordinate\Dimension;
class ResizeFilterTest extends TestCase
{
/**
* @dataProvider provideDimensions
*/
public function testApply(Dimension $dimension, $mode, $width, $height, $modulus, $expected, $forceStandards = true)
{
$video = $this->getVideoMock();
$pathfile = '/path/to/file'.mt_rand();
$format = $this->getMock('FFMpeg\Format\VideoInterface');
$format->expects($this->any())
->method('getModulus')
->will($this->returnValue($modulus));
$streams = new StreamCollection(array(
new Stream(array(
'codec_type' => 'video',
'width' => $width,
'height' => $height,
))
));
$video->expects($this->once())
->method('getStreams')
->will($this->returnValue($streams));
$filter = new ResizeFilter($dimension, $mode, $forceStandards);
$this->assertEquals($expected, $filter->apply($video, $format));
}
public function provideDimensions()
{
return array(
array(new Dimension(320, 240), ResizeFilter::RESIZEMODE_FIT, 640, 480, 2, array('-s', '320x240')),
array(new Dimension(320, 240), ResizeFilter::RESIZEMODE_INSET, 640, 480, 2, array('-s', '320x240')),
array(new Dimension(320, 240), ResizeFilter::RESIZEMODE_SCALE_HEIGHT, 640, 480, 2, array('-s', '320x240')),
array(new Dimension(320, 240), ResizeFilter::RESIZEMODE_SCALE_WIDTH, 640, 480, 2, array('-s', '320x240')),
array(new Dimension(640, 480), ResizeFilter::RESIZEMODE_FIT, 320, 240, 2, array('-s', '640x480')),
array(new Dimension(640, 480), ResizeFilter::RESIZEMODE_INSET, 320, 240, 2, array('-s', '640x480')),
array(new Dimension(640, 480), ResizeFilter::RESIZEMODE_SCALE_HEIGHT, 320, 240, 2, array('-s', '640x480')),
array(new Dimension(640, 480), ResizeFilter::RESIZEMODE_SCALE_WIDTH, 320, 240, 2, array('-s', '640x480')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_FIT, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_INSET, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_SCALE_HEIGHT, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_SCALE_WIDTH, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_FIT, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_INSET, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_SCALE_HEIGHT, 1280, 720, 2, array('-s', '640x360')),
array(new Dimension(640, 360), ResizeFilter::RESIZEMODE_SCALE_WIDTH, 1280, 720, 2, array('-s', '640x360')),
// test non standard dimension
array(new Dimension(700, 150), ResizeFilter::RESIZEMODE_INSET, 123, 456, 2, array('-s', '62x150'), true),
array(new Dimension(700, 150), ResizeFilter::RESIZEMODE_INSET, 123, 456, 2, array('-s', '40x150'), false),
array(new Dimension(320, 320), ResizeFilter::RESIZEMODE_FIT, 640, 480, 2, array('-s', '320x320')),
array(new Dimension(320, 320), ResizeFilter::RESIZEMODE_INSET, 640, 480, 2, array('-s', '320x240')),
array(new Dimension(320, 320), ResizeFilter::RESIZEMODE_SCALE_HEIGHT, 640, 480, 2, array('-s', '320x240')),
array(new Dimension(320, 320), ResizeFilter::RESIZEMODE_SCALE_WIDTH, 640, 480, 2, array('-s', '426x320')),
);
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace FFMpeg\Tests\Filters\Video;
use FFMpeg\Tests\TestCase;
use FFMpeg\FFProbe\DataMapping\StreamCollection;
use FFMpeg\FFProbe\DataMapping\Stream;
use FFMpeg\Filters\Video\SynchronizeFilter;
class SynchronizeFilterTest extends TestCase
{
/**
* @dataProvider provideStreams
*/
public function testApply($streams, $expected)
{
$video = $this->getVideoMock();
$format = $this->getMock('FFMpeg\Format\VideoInterface');
$video->expects($this->once())
->method('getStreams')
->will($this->returnValue($streams));
$video->expects($this->any())
->method('getPathfile')
->will($this->returnValue(__FILE__));
$filter = new SynchronizeFilter();
$this->assertEquals($expected, $filter->apply($video, $format));
}
public function provideStreams()
{
$audio = new StreamCollection(array(new Stream(array(
'index' => 0,
'codec_type' => 'audio',
))));
$synced = new StreamCollection(array(new Stream(array(
'index' => 0,
'codec_type' => 'video',
)), new Stream(array(
'index' => 1,
'codec_type' => 'audio',
))));
$video = new StreamCollection(array(new Stream(array(
'index' => 0,
'codec_type' => 'video',
'start_time' => '0.123456',
)), new Stream(array(
'index' => 1,
'codec_type' => 'audio',
))));
return array(
array($audio, array()),
array($synced, array()),
array($video, array('-itsoffset', '0.123456', '-i', __FILE__, '-map', '1:0', '-map', '0:1')),
);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace FFMpeg\Tests\Filters\Video;
use FFMpeg\Tests\TestCase;
use FFMpeg\Filters\Video\VideoFilters;
use FFMpeg\Filters\Video\ResizeFilter;
class VideoFiltersTest extends TestCase
{
/**
* @dataProvider provideResizeOptions
*/
public function testResize($mode, $forceStandards)
{
$capturedFilter = null;
$video = $this->getVideoMock();
$filters = new VideoFilters($video);
$dimension = $this->getDimensionMock();
$video->expects($this->once())
->method('addFilter')
->with($this->isInstanceOf('FFMpeg\Filters\Video\ResizeFilter'))
->will($this->returnCallback(function ($filter) use (&$capturedFilter) {
$capturedFilter = $filter;
}));
$filters->resize($dimension, $mode, $forceStandards);
$this->assertSame($mode, $capturedFilter->getMode());
$this->assertSame($forceStandards, $capturedFilter->areStandardsForced());
$this->assertSame($dimension, $capturedFilter->getDimension());
}
public function provideResizeOptions()
{
return array(
array(ResizeFilter::RESIZEMODE_FIT, true),
array(ResizeFilter::RESIZEMODE_SCALE_WIDTH, true),
array(ResizeFilter::RESIZEMODE_SCALE_HEIGHT, false),
array(ResizeFilter::RESIZEMODE_INSET, false),
);
}
public function testResample()
{
$capturedFilter = null;
$video = $this->getVideoMock();
$filters = new VideoFilters($video);
$framerate = $this->getFramerateMock();
$gop = 42;
$video->expects($this->once())
->method('addFilter')
->with($this->isInstanceOf('FFMpeg\Filters\Video\VideoResampleFilter'))
->will($this->returnCallback(function ($filter) use (&$capturedFilter) {
$capturedFilter = $filter;
}));
$filters->resample($framerate, $gop);
$this->assertSame($framerate, $capturedFilter->getFramerate());
$this->assertSame($gop, $capturedFilter->getGOP());
}
public function testSynchronize()
{
$video = $this->getVideoMock();
$filters = new VideoFilters($video);
$video->expects($this->once())
->method('addFilter')
->with($this->isInstanceOf('FFMpeg\Filters\Video\SynchronizeFilter'));
$filters->synchronize();
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace FFMpeg\Tests\Filters\Video;
use FFMpeg\Filters\Video\VideoResampleFilter;
use FFMpeg\Tests\TestCase;
use FFMpeg\Coordinate\FrameRate;
class VideoResampleFilterTest extends TestCase
{
public function testApplyWithAFormatThatSupportsBFrames()
{
$framerate = new FrameRate(54);
$gop = 42;
$video = $this->getVideoMock();
$format = $this->getMock('FFMpeg\Format\VideoInterface');
$format->expects($this->any())
->method('supportBFrames')
->will($this->returnValue(true));
$expected = array('-r', 54, '-b_strategy', '1', '-bf', '3', '-g', 42);
$filter = new VideoResampleFilter($framerate, $gop);
$this->assertEquals($expected, $filter->apply($video, $format));
}
public function testApplyWithAFormatThatDoesNotSupportsBFrames()
{
$framerate = new FrameRate(54);
$gop = 42;
$video = $this->getVideoMock();
$format = $this->getMock('FFMpeg\Format\VideoInterface');
$format->expects($this->any())
->method('supportBFrames')
->will($this->returnValue(false));
$expected = array('-r', 54);
$filter = new VideoResampleFilter($framerate, $gop);
$this->assertEquals($expected, $filter->apply($video, $format));
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace FFMpeg\Tests\Format\Audio;
use FFMpeg\Tests\TestCase;
use FFMpeg\Format\Audio\DefaultAudio;
abstract class AudioTestCase extends TestCase
{
public function testExtraParams()
{
foreach ($this->getFormat()->getExtraParams() as $param) {
$this->assertScalar($param);
}
}
public function testGetAudioCodec()
{
$this->assertScalar($this->getFormat()->getAudioCodec());
$this->assertContains($this->getFormat()->getAudioCodec(), $this->getFormat()->getAvailableAudioCodecs());
}
public function testSetAudioCodec()
{
$format = $this->getFormat();
foreach ($format->getAvailableAudioCodecs() as $codec) {
$format->setAudioCodec($codec);
$this->assertEquals($codec, $format->getAudioCodec());
}
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testSetInvalidAudioCodec()
{
$this->getFormat()->setAudioCodec('invalid-random-audio-codec');
}
public function testGetAvailableAudioCodecs()
{
$this->assertGreaterThan(0, count($this->getFormat()->getAvailableAudioCodecs()));
}
public function testGetAudioKiloBitrate()
{
$this->assertInternalType('integer', $this->getFormat()->getAudioKiloBitrate());
}
public function testSetAudioKiloBitrate()
{
$format = $this->getFormat();
$format->setAudioKiloBitrate(256);
$this->assertEquals(256, $format->getAudioKiloBitrate());
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testSetInvalidKiloBitrate()
{
$this->getFormat()->setAudioKiloBitrate(0);
}
/**
* @expectedException FFMpeg\Exception\InvalidArgumentException
*/
public function testSetNegativeKiloBitrate()
{
$this->getFormat()->setAudioKiloBitrate(-10);
}
public function testCreateProgressListener()
{
$media = $this->getMock('FFMpeg\Media\MediaTypeInterface');
$media->expects($this->any())
->method('getPathfile')
->will($this->returnValue(__FILE__));
$format = $this->getFormat();
$ffprobe = $this->getFFProbeMock();
foreach ($format->createProgressListener($media, $ffprobe, 1, 3) as $listener) {
$this->assertInstanceOf('FFMpeg\Format\ProgressListener\AudioProgressListener', $listener);
$this->assertSame($ffprobe, $listener->getFFProbe());
$this->assertSame(__FILE__, $listener->getPathfile());
$this->assertSame(1, $listener->getCurrentPass());
$this->assertSame(3, $listener->getTotalPass());
}
}
/**
* @return DefaultAudio
*/
abstract public function getFormat();
}

View file

@ -0,0 +1,13 @@
<?php
namespace FFMpeg\Tests\Format\Audio;
use FFMpeg\Format\Audio\Flac;
class FlacTest extends AudioTestCase
{
public function getFormat()
{
return new Flac();
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace FFMpeg\Tests\Format\Audio;
use FFMpeg\Format\Audio\Mp3;
class Mp3Test extends AudioTestCase
{
public function getFormat()
{
return new Mp3();
}
}

Some files were not shown because too many files have changed in this diff Show more