Initila Import

This commit is contained in:
Romain Neutron 2012-04-13 10:20:54 +02:00
commit 11345d6367
17 changed files with 773 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/nbproject/
/vendor/
composer.phar
composer.lock

12
.travis.yml Normal file
View file

@ -0,0 +1,12 @@
language: php
before_script:
- sudo apt-get update
- sudo apt-get install -y ffmpeg libavcodec-extra-53
- curl -s http://getcomposer.org/installer | php
- php composer.phar install
php:
- 5.3
- 5.4

3
bootstrap.php Normal file
View file

@ -0,0 +1,3 @@
<?php
require __DIR__ . '/vendor/.composer/autoload.php';

24
composer.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "php-ffmpeg/php-ffmpeg",
"type": "library",
"description": "FFMpeg",
"keywords": ["video"],
"license": "MIT",
"authors": [
{
"name": "Romain Neutron",
"email": "imprec@gmail.com",
"homepage": "http://www.lickmychip.com/"
}
],
"require": {
"php": ">=5.3.6",
"symfony/process": ">2.0",
"monolog/monolog": "dev-master"
},
"autoload": {
"psr-0": {
"FFMpeg": "src"
}
}
}

36
phpunit.xml.dist Normal file
View file

@ -0,0 +1,36 @@
<?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="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>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>vendor</directory>
<directory>tests</directory>
</blacklist>
</filter>
</phpunit>

View file

@ -0,0 +1,10 @@
<?php
namespace FFMpeg;
interface AdapterInterface
{
public static function load(\Monolog\Logger $logger);
}

71
src/FFMpeg/Binary.php Normal file
View file

@ -0,0 +1,71 @@
<?php
namespace FFMpeg;
abstract class Binary implements AdapterInterface
{
protected $binary;
/**
*
* @var \Monolog\Logger
*/
protected $logger;
public function __construct($binary, $logger = null)
{
$this->binary = $binary;
if ( ! $logger)
{
$logger = new \Monolog\Logger('default');
$logger->pushHandler(new \Monolog\Handler\NullHandler());
}
$this->logger = $logger;
}
public static function load(\Monolog\Logger $logger = null)
{
if ('' === $binary = self::autodetect(static::getBinaryName()))
{
throw new \Exception('Binary not found');
}
return new static($binary, $logger);
}
protected static function run($command, $bypass_errors = false)
{
$process = new \Symfony\Component\Process\Process($command);
$process->run();
if ( ! $process->isSuccessful() && ! $bypass_errors)
{
throw new Exception\RuntimeException('Failed to execute ' . $command);
}
$result = $process->getOutput();
unset($process);
return $result;
}
/**
* Autodetect the presence of a binary
*
* @param string $binaryName
* @return string
*/
protected static function autodetect($binaryName)
{
return trim(self::run(sprintf('which %s', escapeshellarg($binaryName)), true));
}
protected static function getBinaryName()
{
throw new Exception('Should be implemented');
}
}

132
src/FFMpeg/FFMpeg.php Normal file
View file

@ -0,0 +1,132 @@
<?php
namespace FFMpeg;
class FFMpeg extends Binary
{
protected $pathfile;
public function open($pathfile)
{
if ( ! file_exists($pathfile))
{
$this->logger->addError(sprintf('Request to open %s failed', $pathfile));
throw new \InvalidArgumentException(sprintf('File %s does not exists', $pathfile));
}
$this->logger->addInfo(sprintf('FFmpeg opens %s', $pathfile));
$this->pathfile = $pathfile;
}
public function extractImage($time, $output, $width, $height)
{
if ( ! $this->pathfile)
{
throw new \RuntimeException('No file open');
}
$cmd = $this->binary
. ' -i ' . escapeshellarg($this->pathfile)
. ' -vframes 1 -ss ' . $time
. ' -f image2 ' . escapeshellarg($output);
$this->logger->addInfo(sprintf('Executing command %s', $cmd));
$process = new \Symfony\Component\Process\Process($cmd);
$process->run();
if ( ! $process->isSuccessful())
{
$this->logger->addError(sprintf('Command failed :: %s', $process->getErrorOutput()));
if (file_exists($output) && is_writable($output))
{
unlink($output);
}
throw new \RuntimeException('Failed to extract image');
}
$this->logger->addInfo('Command run with success');
return true;
}
public function encode(Format\Format $format, $outputPathfile, $threads = 1)
{
if ( ! $this->pathfile)
{
throw new \RuntimeException('No file open');
}
$threads = max(min($threads, 64), 1);
$cmd_part1 = $this->binary
. ' -y -i '
. escapeshellarg($this->pathfile) . ' '
. $format->getExtraParams() . ' ';
$cmd_part2 = ' -s ' . $format->getWidth() . 'x' . $format->getHeight()
. ' -r ' . $format->getFrameRate()
. ' -vcodec ' . $format->getVideoCodec()
. ' -b ' . $format->getKiloBitrate() . 'k -g 25 -bf 3'
. ' -threads ' . $threads
. ' -refs 6 -b_strategy 1 -coder 1 -qmin 10 -qmax 51 '
. ' -sc_threshold 40 -flags +loop -cmp +chroma'
. ' -me_range 16 -subq 7 -i_qfactor 0.71 -qcomp 0.6 -qdiff 4 '
. ' -trellis 1 -qscale 1 '
. '-acodec ' . $format->getAudioCodec() . ' -ab 92k ';
$tmpFile = new \SplFileInfo(tempnam(sys_get_temp_dir(), 'temp') . '.' . pathinfo($outputPathfile, PATHINFO_EXTENSION));
$passes = array();
$passes[] = $cmd_part1 . ' -pass 1 ' . $cmd_part2
. ' -an ' . escapeshellarg($tmpFile->getPathname());
$passes[] = $cmd_part1 . ' -pass 2 ' . $cmd_part2
. ' -ac 2 -ar 44100 ' . escapeshellarg($outputPathfile);
foreach ($passes as $pass)
{
$process = new \Symfony\Component\Process\Process($pass);
try
{
$process->run();
}
catch (\Exception $e)
{
break;
}
}
$this->cleanupTemporaryFile($tmpFile->getPathname());
$this->cleanupTemporaryFile(getcwd() . '/ffmpeg2pass-0.log');
$this->cleanupTemporaryFile(getcwd() . '/ffmpeg2pass-0.log.mbtree');
if ($process instanceof \Symfony\Component\Process\Process && ! $process->isSuccessful())
{
throw new \RuntimeException(sprintf('Encoding failed : %s', $process->getErrorOutput()));
}
return true;
}
protected function cleanupTemporaryFile($pathfile)
{
if (file_exists($pathfile) && is_writable($pathfile))
{
unlink($pathfile);
}
}
protected static function getBinaryName()
{
return 'ffmpeg';
}
}

52
src/FFMpeg/FFProbe.php Normal file
View file

@ -0,0 +1,52 @@
<?php
namespace FFMpeg;
class FFProbe extends Binary
{
public function probeFormat($pathfile)
{
if ( ! is_file($pathfile))
{
throw new \RuntimeException($pathfile);
}
$cmd = $this->binary . ' ' . $pathfile . ' -show_format';
return $this->executeProbe($cmd);
}
public function probeStreams($pathfile)
{
if ( ! is_file($pathfile))
{
throw new \RuntimeException($pathfile);
}
$cmd = $this->binary . ' ' . $pathfile . ' -show_streams';
return $this->executeProbe($cmd);
}
protected function executeProbe($command)
{
$process = new \Symfony\Component\Process\Process($command);
$process->run();
if ( ! $process->isSuccessful())
{
throw new \RuntimeException('Failed to probe');
}
return $process->getOutput();
}
protected static function getBinaryName()
{
return 'ffprobe';
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace FFMpeg\Format;
abstract class DefaultFormat implements Format
{
protected $width;
protected $height;
protected $frameRate = 25;
protected $audioCodec;
protected $audioSampleRate = 44100;
protected $videoCodec;
protected $kiloBitrate = 1000;
protected $GOPsize = 25;
public function __construct($width, $height)
{
$this->setDimensions($width, $height);
}
public function getExtraParams()
{
}
public function getWidth()
{
return $this->width;
}
public function getHeight()
{
return $this->height;
}
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 = $this->getMultiple($width, 16);
$this->height = $this->getMultiple($height, 16);
return;
}
public function getFrameRate()
{
return $this->frameRate;
}
public function setFrameRate($frameRate)
{
if ($frameRate < 1)
{
throw new \InvalidArgumentException('Wrong framerate value');
}
$this->frameRate = (int) $frameRate;
}
public function getAudioCodec()
{
return $this->audioCodec;
}
public function setAudioCodec($audioCodec)
{
if ( ! in_array($audioCodec, $this->getAvailableAudioCodecs()))
{
throw new \InvalidArgumentException('Wrong audiocodec value');
}
$this->audioCodec = $audioCodec;
}
public function getAudioSampleRate()
{
return $this->audioSampleRate;
}
public function setAudioSampleRate($audioSampleRate)
{
if ($audioSampleRate < 1)
{
throw new \InvalidArgumentException('Wrong audio sample rate value');
}
$this->audioSampleRate = (int) $audioSampleRate;
}
public function getVideoCodec()
{
return $this->videoCodec;
}
public function setVideoCodec($videoCodec)
{
if ( ! in_array($videoCodec, $this->getAvailableVideoCodecs()))
{
throw new \InvalidArgumentException('Wrong videocodec value');
}
$this->videoCodec = $videoCodec;
}
public function getKiloBitrate()
{
return $this->kiloBitrate;
}
public function setKiloBitrate($kiloBitrate)
{
if ($kiloBitrate < 1)
{
throw new \InvalidArgumentException('Wrong kiloBitrate value');
}
$this->kiloBitrate = (int) $kiloBitrate;
}
public function getGOPsize()
{
return $this->GOPsize;
}
public function setGOPsize($GOPsize)
{
$this->GOPsize = (int) $GOPsize;
}
protected function getMultiple($value, $multiple)
{
$modulo = $value % $multiple;
$ret = (int) $multiple;
$halfDistance = $multiple / 2;
if ($modulo <= $halfDistance)
$bound = 'bottom';
else
$bound = 'top';
switch ($bound)
{
default:
case 'top':
$ret = $value + $multiple - $modulo;
break;
case 'bottom':
$ret = $value - $modulo;
break;
}
if ($ret < $multiple)
{
$ret = (int) $multiple;
}
return (int) $ret;
}
abstract protected function getAvailableAudioCodecs();
abstract protected function getAvailableVideoCodecs();
}

View file

@ -0,0 +1,26 @@
<?php
namespace FFMpeg\Format;
interface Format
{
public function getExtraParams();
public function getWidth();
public function getHeight();
public function getFrameRate();
public function getAudioCodec();
public function getAudioSampleRate();
public function getVideoCodec();
public function getKiloBitrate();
public function getGOPSize();
}

21
src/FFMpeg/Format/Ogg.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace FFMpeg\Format;
class Ogg extends DefaultFormat
{
protected $audioCodec = 'libvorbis';
protected $videoCodec = 'libtheora';
protected function getAvailableAudioCodecs()
{
return array('libvorbis');
}
protected function getAvailableVideoCodecs()
{
return array('libtheora');
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace FFMpeg\Format;
class WebM extends DefaultFormat
{
protected $audioCodec = 'libvorbis';
protected $videoCodec = 'libvpx';
public function getExtraParams()
{
return '-f webm';
}
protected function getAvailableAudioCodecs()
{
return array('libvorbis');
}
protected function getAvailableVideoCodecs()
{
return array('libvpx');
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace FFMpeg\Format;
class X264 extends DefaultFormat
{
protected $audioCodec = 'libmp3lame';
protected $videoCodec = 'libx264';
protected function getAvailableAudioCodecs()
{
return array('libvo_aacenc', 'libfaac', 'libmp3lame');
}
protected function getAvailableVideoCodecs()
{
return array('libx264');
}
}

BIN
tests/files/Test.ogv Normal file

Binary file not shown.

View file

@ -0,0 +1,92 @@
<?php
namespace FFMpeg;
class FFMpegTest extends \PHPUnit_Framework_TestCase
{
/**
* @var FFMpeg
*/
protected $object;
/**
* @covers FFMpeg\FFMpeg::open
* @todo Implement testOpen().
*/
public function testOpen()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
/**
* @covers FFMpeg\FFMpeg::extractImage
* @todo Implement testExtractImage().
*/
public function testExtractImage()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
/**
* @covers FFMpeg\FFMpeg::encode
*/
public function testEncodeWebm()
{
$ffprobe = FFProbe::load();
$dest = __DIR__ . '/../../files/encode_test.webm';
$ffmpeg = FFMpeg::load(new \Monolog\Logger('test'));
$ffmpeg->open(__DIR__ . '/../../files/Test.ogv');
$ffmpeg->encode(new Format\WebM(32, 32), $dest);
$ffprobe->probeFormat($dest);
unlink($dest);
}
/**
* @covers FFMpeg\FFMpeg::encode
*/
public function testEncodeOgg()
{
$ffprobe = FFProbe::load();
$dest = __DIR__ . '/../../files/encode_test.ogv';
$ffmpeg = FFMpeg::load(new \Monolog\Logger('test'));
$ffmpeg->open(__DIR__ . '/../../files/Test.ogv');
$ffmpeg->encode(new Format\Ogg(32, 32), $dest);
$ffprobe->probeFormat($dest);
unlink($dest);
}
/**
* @covers FFMpeg\FFMpeg::encode
*/
public function testEncodeX264()
{
$ffprobe = FFProbe::load();
$dest = __DIR__ . '/../../files/encode_test.mp4';
$ffmpeg = FFMpeg::load(new \Monolog\Logger('test'));
$ffmpeg->open(__DIR__ . '/../../files/Test.ogv');
$ffmpeg->encode(new Format\X264(32, 32), $dest);
$ffprobe->probeFormat($dest);
unlink($dest);
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace FFMpeg;
class FFProbeTest extends \PHPUnit_Framework_TestCase
{
/**
* @covers FFMpeg\FFProbe::probeFormat
* @todo Implement testProbeFormat().
*/
public function testProbeFormat()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
/**
* @covers FFMpeg\FFProbe::probeStreams
* @todo Implement testProbeStreams().
*/
public function testProbeStreams()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
/**
* @covers FFMpeg\FFProbe::probeFrames
* @todo Implement testProbeFrames().
*/
public function testProbeFrames()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
/**
* @covers FFMpeg\FFProbe::probePackets
* @todo Implement testProbePackets().
*/
public function testProbePackets()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
/**
* @covers FFMpeg\FFProbe::probeErrors
* @todo Implement testProbeErrors().
*/
public function testProbeErrors()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
}