commit 11345d63671fd0265b3325cff80ec62a91fc5e8a Author: Romain Neutron Date: Fri Apr 13 10:20:54 2012 +0200 Initila Import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a6c37e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/nbproject/ +/vendor/ +composer.phar +composer.lock + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..19c73d7 --- /dev/null +++ b/.travis.yml @@ -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 + diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..92e7467 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,3 @@ +=5.3.6", + "symfony/process": ">2.0", + "monolog/monolog": "dev-master" + }, + "autoload": { + "psr-0": { + "FFMpeg": "src" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..58546bb --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,36 @@ + + + + + + + + + + + + tests + + + + + vendor + tests + + + + + diff --git a/src/FFMpeg/AdapterInterface.php b/src/FFMpeg/AdapterInterface.php new file mode 100644 index 0000000..413b450 --- /dev/null +++ b/src/FFMpeg/AdapterInterface.php @@ -0,0 +1,10 @@ +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'); + } + +} \ No newline at end of file diff --git a/src/FFMpeg/FFMpeg.php b/src/FFMpeg/FFMpeg.php new file mode 100644 index 0000000..b354eab --- /dev/null +++ b/src/FFMpeg/FFMpeg.php @@ -0,0 +1,132 @@ +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'; + } + +} diff --git a/src/FFMpeg/FFProbe.php b/src/FFMpeg/FFProbe.php new file mode 100644 index 0000000..3eeed75 --- /dev/null +++ b/src/FFMpeg/FFProbe.php @@ -0,0 +1,52 @@ +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'; + } + +} diff --git a/src/FFMpeg/Format/DefaultFormat.php b/src/FFMpeg/Format/DefaultFormat.php new file mode 100644 index 0000000..f763161 --- /dev/null +++ b/src/FFMpeg/Format/DefaultFormat.php @@ -0,0 +1,174 @@ +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(); + +} \ No newline at end of file diff --git a/src/FFMpeg/Format/Format.php b/src/FFMpeg/Format/Format.php new file mode 100644 index 0000000..37a89b7 --- /dev/null +++ b/src/FFMpeg/Format/Format.php @@ -0,0 +1,26 @@ +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); + } + +} diff --git a/tests/src/FFMpeg/FFProbeTest.php b/tests/src/FFMpeg/FFProbeTest.php new file mode 100644 index 0000000..617f182 --- /dev/null +++ b/tests/src/FFMpeg/FFProbeTest.php @@ -0,0 +1,68 @@ +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.' + ); + } + +}