diff --git a/composer.json b/composer.json index 3e1a5bb..906606a 100644 --- a/composer.json +++ b/composer.json @@ -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" - } } } diff --git a/composer.lock b/composer.lock index 4429cb6..97993c9 100644 --- a/composer.lock +++ b/composer.lock @@ -3,8 +3,166 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "531fb2f99e17d12b1e8edc8957c170d7", + "hash": "336691561433798ee6f95d41970f1eae", "packages": [ + { + "name": "alchemy/binary-driver", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/alchemy-fr/BinaryDriver.git", + "reference": "1.5.0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/alchemy-fr/BinaryDriver/zipball/1.5.0", + "reference": "1.5.0", + "shasum": "" + }, + "require": { + "evenement/evenement": ">=1.0,<2.0", + "monolog/monolog": ">=1.3,<2.0", + "php": ">=5.3.3", + "psr/log": ">=1.0,<2.0", + "symfony/process": ">=2.0,<3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=3.7,<4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Alchemy": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Romain Neutron", + "email": "imprec@gmail.com", + "homepage": "http://www.lickmychip.com/" + }, + { + "name": "Nicolas Le Goff", + "email": "legoff.n@gmail.com" + }, + { + "name": "Phraseanet Team", + "email": "info@alchemy.fr", + "homepage": "http://www.phraseanet.com/" + } + ], + "description": "A set of tools to build binary drivers", + "keywords": [ + "binary", + "driver" + ], + "time": "2013-06-21 15:51:20" + }, + { + "name": "doctrine/cache", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "v1.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/doctrine/cache/archive/v1.0.zip", + "reference": "v1.0", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "Doctrine\\Common\\Cache\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com", + "homepage": "http://www.jwage.com/" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com", + "homepage": "http://www.instaclick.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "http://jmsyst.com", + "role": "Developer of wrapped JMSSerializerBundle" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2013-01-10 22:43:46" + }, + { + "name": "evenement/evenement", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement", + "reference": "v1.0.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/igorw/evenement/zipball/v1.0.0", + "reference": "v1.0.0", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch", + "homepage": "http://wiedler.ch/igor/" + } + ], + "description": "Événement is a very simple event dispatching library for PHP 5.3", + "keywords": [ + "event-dispatcher" + ], + "time": "2012-05-30 08:01:08" + }, { "name": "monolog/monolog", "version": "1.5.0", @@ -107,17 +265,17 @@ }, { "name": "symfony/process", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/Process", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/Process/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -126,7 +284,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -150,10 +308,64 @@ ], "description": "Symfony Process Component", "homepage": "http://symfony.com", - "time": "2013-03-23 07:49:54" + "time": "2013-05-06 20:03:44" } ], "packages-dev": [ + { + "name": "dflydev/markdown", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-markdown.git", + "reference": "v1.0.2" + }, + "dist": { + "type": "zip", + "url": "https://github.com/dflydev/dflydev-markdown/zipball/v1.0.2", + "reference": "v1.0.2", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "autoload": { + "psr-0": { + "dflydev\\markdown": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "New BSD License" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Michel Fortin", + "homepage": "http://michelf.com" + }, + { + "name": "John Gruber", + "homepage": "http://daringfireball.net" + } + ], + "description": "PHP Markdown & Extra", + "homepage": "http://github.com/dflydev/dflydev-markdown", + "keywords": [ + "markdown" + ], + "time": "2012-01-15 19:36:37" + }, { "name": "nikic/php-parser", "version": "v0.9.3", @@ -193,6 +405,363 @@ ], "time": "2012-11-22 18:54:05" }, + { + "name": "phpunit/php-code-coverage", + "version": "1.2.11", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "1.2.11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.11", + "reference": "1.2.11", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": ">=1.3.0@stable", + "phpunit/php-text-template": ">=1.1.1@stable", + "phpunit/php-token-stream": ">=1.1.3@stable" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.0.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2013-05-23 18:23:24" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.3.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "1.3.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-file-iterator/zipball/1.3.3", + "reference": "1.3.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "File/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2012-10-11 04:44:38" + }, + { + "name": "phpunit/php-text-template", + "version": "1.1.4", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-text-template.git", + "reference": "1.1.4" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4", + "reference": "1.1.4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "Text/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2012-10-31 11:15:28" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.4", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-timer.git", + "reference": "1.0.4" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-timer/zipball/1.0.4", + "reference": "1.0.4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "timer" + ], + "time": "2012-10-11 04:45:58" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.1.5", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-token-stream.git", + "reference": "1.1.5" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-token-stream/zipball/1.1.5", + "reference": "1.1.5", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "tokenizer" + ], + "time": "2012-10-11 04:47:14" + }, + { + "name": "phpunit/phpunit", + "version": "3.7.21", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "3.7.21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.21", + "reference": "3.7.21", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpunit/php-code-coverage": ">=1.2.1,<1.3.0", + "phpunit/php-file-iterator": ">=1.3.1", + "phpunit/php-text-template": ">=1.1.1", + "phpunit/php-timer": ">=1.0.2,<1.1.0", + "phpunit/phpunit-mock-objects": ">=1.2.0,<1.3.0", + "symfony/yaml": ">=2.0,<3.0" + }, + "require-dev": { + "pear-pear/pear": "1.9.4" + }, + "suggest": { + "ext-json": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "phpunit/php-invoker": ">=1.1.0,<1.2.0" + }, + "bin": [ + "composer/bin/phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "", + "../../symfony/yaml/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2013-05-23 18:54:29" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "1.2.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "1.2.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", + "reference": "1.2.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-text-template": ">=1.1.1@stable" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2013-01-13 10:24:48" + }, { "name": "pimple/pimple", "version": "v1.0.2", @@ -241,19 +810,20 @@ }, { "name": "sami/sami", - "version": "v1.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/fabpot/Sami.git", - "reference": "v1.0" + "reference": "9503a81e0b505be1cd9f7eec93e0da8b2615c211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fabpot/Sami/zipball/v1.0", - "reference": "v1.0", + "url": "https://api.github.com/repos/fabpot/Sami/zipball/9503a81e0b505be1cd9f7eec93e0da8b2615c211", + "reference": "9503a81e0b505be1cd9f7eec93e0da8b2615c211", "shasum": "" }, "require": { + "dflydev/markdown": "1.0.*", "nikic/php-parser": "0.9.*", "php": ">=5.3.0", "pimple/pimple": "1.0.*", @@ -293,7 +863,7 @@ "keywords": [ "phpdoc" ], - "time": "2013-04-05 13:01:32" + "time": "2013-06-02 12:00:43" }, { "name": "silex/silex", @@ -379,26 +949,32 @@ }, { "name": "symfony/console", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/Console", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/Console/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "symfony/event-dispatcher": ">=2.1,<3.0" + }, + "suggest": { + "symfony/event-dispatcher": "" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -422,21 +998,77 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-03-19 20:48:08" + "time": "2013-06-11 07:15:14" + }, + { + "name": "symfony/debug", + "version": "v2.3.1", + "target-dir": "Symfony/Component/Debug", + "source": { + "type": "git", + "url": "https://github.com/symfony/Debug.git", + "reference": "v2.3.1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Debug/zipball/v2.3.1", + "reference": "v2.3.1", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/http-foundation": ">=2.1,<3.0", + "symfony/http-kernel": ">=2.1,<3.0" + }, + "suggest": { + "symfony/class-loader": "", + "symfony/http-foundation": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Debug\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "http://symfony.com", + "time": "2013-06-02 11:58:44" }, { "name": "symfony/event-dispatcher", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -446,13 +1078,13 @@ "symfony/dependency-injection": ">=2.0,<3.0" }, "suggest": { - "symfony/dependency-injection": "2.2.*", - "symfony/http-kernel": "2.2.*" + "symfony/dependency-injection": "", + "symfony/http-kernel": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -476,21 +1108,21 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2013-02-11 11:26:43" + "time": "2013-05-13 14:36:40" }, { "name": "symfony/filesystem", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/Filesystem", "source": { "type": "git", "url": "https://github.com/symfony/Filesystem.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Filesystem/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/Filesystem/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -499,7 +1131,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -523,21 +1155,21 @@ ], "description": "Symfony Filesystem Component", "homepage": "http://symfony.com", - "time": "2013-01-17 15:25:59" + "time": "2013-06-04 15:02:05" }, { "name": "symfony/finder", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/Finder", "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/Finder/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -546,7 +1178,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -570,21 +1202,21 @@ ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2013-04-01 07:51:50" + "time": "2013-06-02 12:05:51" }, { "name": "symfony/http-foundation", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/HttpFoundation", "source": { "type": "git", "url": "https://github.com/symfony/HttpFoundation.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/HttpFoundation/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/HttpFoundation/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -593,7 +1225,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -620,28 +1252,29 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "http://symfony.com", - "time": "2013-04-06 10:15:43" + "time": "2013-05-10 06:00:03" }, { "name": "symfony/http-kernel", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/HttpKernel", "source": { "type": "git", "url": "https://github.com/symfony/HttpKernel.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/HttpKernel/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/HttpKernel/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { "php": ">=5.3.3", "psr/log": ">=1.0,<2.0", + "symfony/debug": ">=2.3,<3.0", "symfony/event-dispatcher": ">=2.1,<3.0", - "symfony/http-foundation": ">=2.2,<2.3-dev" + "symfony/http-foundation": ">=2.2,<3.0" }, "require-dev": { "symfony/browser-kit": "2.2.*", @@ -651,21 +1284,21 @@ "symfony/dependency-injection": ">=2.0,<3.0", "symfony/finder": ">=2.0,<3.0", "symfony/process": ">=2.0,<3.0", - "symfony/routing": ">=2.2,<2.3-dev", - "symfony/stopwatch": ">=2.2,<2.3-dev" + "symfony/routing": ">=2.2,<3.0", + "symfony/stopwatch": ">=2.2,<3.0" }, "suggest": { - "symfony/browser-kit": "2.2.*", - "symfony/class-loader": "2.2.*", - "symfony/config": "2.2.*", - "symfony/console": "2.2.*", - "symfony/dependency-injection": "2.2.*", - "symfony/finder": "2.2.*" + "symfony/browser-kit": "", + "symfony/class-loader": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "", + "symfony/finder": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -689,21 +1322,21 @@ ], "description": "Symfony HttpKernel Component", "homepage": "http://symfony.com", - "time": "2013-04-06 10:16:33" + "time": "2013-06-11 11:46:38" }, { "name": "symfony/routing", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/Routing", "source": { "type": "git", "url": "https://github.com/symfony/Routing.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Routing/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/Routing/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -712,18 +1345,18 @@ "require-dev": { "doctrine/common": ">=2.2,<3.0", "psr/log": ">=1.0,<2.0", - "symfony/config": ">=2.2,<2.3-dev", + "symfony/config": ">=2.2,<3.0", "symfony/yaml": ">=2.0,<3.0" }, "suggest": { - "doctrine/common": "~2.2", - "symfony/config": "2.2.*", - "symfony/yaml": "2.2.*" + "doctrine/common": "", + "symfony/config": "", + "symfony/yaml": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -747,21 +1380,21 @@ ], "description": "Symfony Routing Component", "homepage": "http://symfony.com", - "time": "2013-03-23 12:03:22" + "time": "2013-05-20 08:57:26" }, { "name": "symfony/yaml", - "version": "v2.2.1", + "version": "v2.3.1", "target-dir": "Symfony/Component/Yaml", "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "v2.2.1" + "reference": "v2.3.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/v2.2.1", - "reference": "v2.2.1", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/v2.3.1", + "reference": "v2.3.1", "shasum": "" }, "require": { @@ -770,7 +1403,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -794,20 +1427,20 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-03-23 07:49:54" + "time": "2013-05-10 18:12:13" }, { "name": "twig/twig", - "version": "v1.13.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/fabpot/Twig.git", - "reference": "v1.13.0" + "reference": "v1.13.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fabpot/Twig/zipball/v1.13.0", - "reference": "v1.13.0", + "url": "https://api.github.com/repos/fabpot/Twig/zipball/v1.13.1", + "reference": "v1.13.1", "shasum": "" }, "require": { @@ -843,16 +1476,16 @@ "keywords": [ "templating" ], - "time": "2013-05-10 15:12:43" + "time": "2013-06-06 06:06:01" } ], "aliases": [ ], "minimum-stability": "stable", - "stability-flags": [ - - ], + "stability-flags": { + "sami/sami": 20 + }, "platform": { "php": ">=5.3.3" }, diff --git a/phpunit-functional.xml.dist b/phpunit-functional.xml.dist new file mode 100644 index 0000000..3f0f738 --- /dev/null +++ b/phpunit-functional.xml.dist @@ -0,0 +1,27 @@ + + + + + tests/FFMpeg/Functional + + + + + vendor + tests + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4e55c40..f36c568 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,20 +9,11 @@ stopOnFailure="false" syntaxCheck="true" verbose="false" - bootstrap="vendor/autoload.php" + bootstrap="tests/bootstrap.php" > - - - - - - - - tests + tests/FFMpeg/Tests diff --git a/src/FFMpeg/Binary.php b/src/FFMpeg/Binary.php deleted file mode 100644 index 0ede05f..0000000 --- a/src/FFMpeg/Binary.php +++ /dev/null @@ -1,128 +0,0 @@ - - * - * 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'); - } -} diff --git a/src/FFMpeg/Coordinate/AspectRatio.php b/src/FFMpeg/Coordinate/AspectRatio.php new file mode 100644 index 0000000..a3c298a --- /dev/null +++ b/src/FFMpeg/Coordinate/AspectRatio.php @@ -0,0 +1,248 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Format/Dimension.php b/src/FFMpeg/Coordinate/Dimension.php similarity index 76% rename from src/FFMpeg/Format/Dimension.php rename to src/FFMpeg/Coordinate/Dimension.php index 605d6ef..15e448d 100644 --- a/src/FFMpeg/Format/Dimension.php +++ b/src/FFMpeg/Coordinate/Dimension.php @@ -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); + } } diff --git a/src/FFMpeg/Coordinate/FrameRate.php b/src/FFMpeg/Coordinate/FrameRate.php new file mode 100644 index 0000000..ed0c7b2 --- /dev/null +++ b/src/FFMpeg/Coordinate/FrameRate.php @@ -0,0 +1,36 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Coordinate/Point.php b/src/FFMpeg/Coordinate/Point.php new file mode 100644 index 0000000..2ee5c62 --- /dev/null +++ b/src/FFMpeg/Coordinate/Point.php @@ -0,0 +1,40 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Coordinate/TimeCode.php b/src/FFMpeg/Coordinate/TimeCode.php new file mode 100644 index 0000000..14864cb --- /dev/null +++ b/src/FFMpeg/Coordinate/TimeCode.php @@ -0,0 +1,66 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Driver/FFMpegDriver.php b/src/FFMpeg/Driver/FFMpegDriver.php new file mode 100644 index 0000000..12b64c8 --- /dev/null +++ b/src/FFMpeg/Driver/FFMpegDriver.php @@ -0,0 +1,40 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Driver/FFProbeDriver.php b/src/FFMpeg/Driver/FFProbeDriver.php new file mode 100644 index 0000000..2a8fb32 --- /dev/null +++ b/src/FFMpeg/Driver/FFProbeDriver.php @@ -0,0 +1,47 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Exception/ExceptionInterface.php b/src/FFMpeg/Exception/ExceptionInterface.php index a98ed0e..df1bfb0 100644 --- a/src/FFMpeg/Exception/ExceptionInterface.php +++ b/src/FFMpeg/Exception/ExceptionInterface.php @@ -13,5 +13,4 @@ namespace FFMpeg\Exception; interface ExceptionInterface { - } diff --git a/src/FFMpeg/Exception/InvalidArgumentException.php b/src/FFMpeg/Exception/InvalidArgumentException.php index df17f0c..c2ef8bd 100644 --- a/src/FFMpeg/Exception/InvalidArgumentException.php +++ b/src/FFMpeg/Exception/InvalidArgumentException.php @@ -13,5 +13,4 @@ namespace FFMpeg\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { - } diff --git a/src/FFMpeg/Exception/RuntimeException.php b/src/FFMpeg/Exception/RuntimeException.php index 7a49b12..14261da 100644 --- a/src/FFMpeg/Exception/RuntimeException.php +++ b/src/FFMpeg/Exception/RuntimeException.php @@ -13,5 +13,4 @@ namespace FFMpeg\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface { - } diff --git a/src/FFMpeg/FFMpeg.php b/src/FFMpeg/FFMpeg.php index cc50d7e..78acd4e 100644 --- a/src/FFMpeg/FFMpeg.php +++ b/src/FFMpeg/FFMpeg.php @@ -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); } } diff --git a/src/FFMpeg/FFMpegServiceProvider.php b/src/FFMpeg/FFMpegServiceProvider.php index 6060d50..2d1419d 100644 --- a/src/FFMpeg/FFMpegServiceProvider.php +++ b/src/FFMpeg/FFMpegServiceProvider.php @@ -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']); }); } diff --git a/src/FFMpeg/FFProbe.php b/src/FFMpeg/FFProbe.php index 4d428d2..1a9bbfd 100644 --- a/src/FFMpeg/FFProbe.php +++ b/src/FFMpeg/FFProbe.php @@ -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; } } diff --git a/src/FFMpeg/FFProbe/DataMapping/AbstractData.php b/src/FFMpeg/FFProbe/DataMapping/AbstractData.php new file mode 100644 index 0000000..5088b89 --- /dev/null +++ b/src/FFMpeg/FFProbe/DataMapping/AbstractData.php @@ -0,0 +1,80 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Exception/LogicException.php b/src/FFMpeg/FFProbe/DataMapping/Format.php similarity index 68% rename from src/FFMpeg/Exception/LogicException.php rename to src/FFMpeg/FFProbe/DataMapping/Format.php index 9ba8143..4d7caa0 100644 --- a/src/FFMpeg/Exception/LogicException.php +++ b/src/FFMpeg/FFProbe/DataMapping/Format.php @@ -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 { - } diff --git a/src/FFMpeg/FFProbe/DataMapping/Stream.php b/src/FFMpeg/FFProbe/DataMapping/Stream.php new file mode 100644 index 0000000..d3a84c9 --- /dev/null +++ b/src/FFMpeg/FFProbe/DataMapping/Stream.php @@ -0,0 +1,35 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/FFProbe/DataMapping/StreamCollection.php b/src/FFMpeg/FFProbe/DataMapping/StreamCollection.php new file mode 100644 index 0000000..bb730ee --- /dev/null +++ b/src/FFMpeg/FFProbe/DataMapping/StreamCollection.php @@ -0,0 +1,99 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/FFProbe/Mapper.php b/src/FFMpeg/FFProbe/Mapper.php new file mode 100644 index 0000000..c0bf70f --- /dev/null +++ b/src/FFMpeg/FFProbe/Mapper.php @@ -0,0 +1,54 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/FFProbe/MapperInterface.php b/src/FFMpeg/FFProbe/MapperInterface.php new file mode 100644 index 0000000..a505784 --- /dev/null +++ b/src/FFMpeg/FFProbe/MapperInterface.php @@ -0,0 +1,27 @@ + + * + * 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); +} diff --git a/src/FFMpeg/FFProbe/OptionsTester.php b/src/FFMpeg/FFProbe/OptionsTester.php new file mode 100644 index 0000000..f6feb7a --- /dev/null +++ b/src/FFMpeg/FFProbe/OptionsTester.php @@ -0,0 +1,64 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/AdapterInterface.php b/src/FFMpeg/FFProbe/OptionsTesterInterface.php similarity index 51% rename from src/FFMpeg/AdapterInterface.php rename to src/FFMpeg/FFProbe/OptionsTesterInterface.php index 84fc007..b4d40e3 100644 --- a/src/FFMpeg/AdapterInterface.php +++ b/src/FFMpeg/FFProbe/OptionsTesterInterface.php @@ -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); } diff --git a/src/FFMpeg/FFProbe/OutputParser.php b/src/FFMpeg/FFProbe/OutputParser.php new file mode 100644 index 0000000..acaa1a0 --- /dev/null +++ b/src/FFMpeg/FFProbe/OutputParser.php @@ -0,0 +1,125 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/FFProbe/OutputParserInterface.php b/src/FFMpeg/FFProbe/OutputParserInterface.php new file mode 100644 index 0000000..a179478 --- /dev/null +++ b/src/FFMpeg/FFProbe/OutputParserInterface.php @@ -0,0 +1,27 @@ + + * + * 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); +} diff --git a/src/FFMpeg/Filters/Audio/AudioFilterInterface.php b/src/FFMpeg/Filters/Audio/AudioFilterInterface.php new file mode 100644 index 0000000..b111cfe --- /dev/null +++ b/src/FFMpeg/Filters/Audio/AudioFilterInterface.php @@ -0,0 +1,29 @@ + + * + * 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); +} diff --git a/src/FFMpeg/Filters/Audio/AudioFilters.php b/src/FFMpeg/Filters/Audio/AudioFilters.php new file mode 100644 index 0000000..39453bb --- /dev/null +++ b/src/FFMpeg/Filters/Audio/AudioFilters.php @@ -0,0 +1,30 @@ +audio = $audio; + } + + /** + * Resamples the audio file. + * + * @param Integer $rate + * + * @return AudioFilters + */ + public function resample($rate) + { + $this->audio->addFilter(new AudioResamplableFilter($rate)); + + return $this; + } +} diff --git a/src/FFMpeg/Filters/Audio/AudioResamplableFilter.php b/src/FFMpeg/Filters/Audio/AudioResamplableFilter.php new file mode 100644 index 0000000..2a25d57 --- /dev/null +++ b/src/FFMpeg/Filters/Audio/AudioResamplableFilter.php @@ -0,0 +1,43 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Filters/FilterInterface.php b/src/FFMpeg/Filters/FilterInterface.php new file mode 100644 index 0000000..4939fb5 --- /dev/null +++ b/src/FFMpeg/Filters/FilterInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Filters; + +interface FilterInterface +{ +} diff --git a/src/FFMpeg/Filters/FiltersCollection.php b/src/FFMpeg/Filters/FiltersCollection.php new file mode 100644 index 0000000..43a856b --- /dev/null +++ b/src/FFMpeg/Filters/FiltersCollection.php @@ -0,0 +1,45 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Filters/Frame/FrameFilterInterface.php b/src/FFMpeg/Filters/Frame/FrameFilterInterface.php new file mode 100644 index 0000000..db4fdac --- /dev/null +++ b/src/FFMpeg/Filters/Frame/FrameFilterInterface.php @@ -0,0 +1,21 @@ + + * + * 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); +} diff --git a/src/FFMpeg/Filters/Frame/FrameFilters.php b/src/FFMpeg/Filters/Frame/FrameFilters.php new file mode 100644 index 0000000..85bfde9 --- /dev/null +++ b/src/FFMpeg/Filters/Frame/FrameFilters.php @@ -0,0 +1,24 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Filters/Video/ResizeFilter.php b/src/FFMpeg/Filters/Video/ResizeFilter.php new file mode 100644 index 0000000..c183d57 --- /dev/null +++ b/src/FFMpeg/Filters/Video/ResizeFilter.php @@ -0,0 +1,126 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Filters/Video/SynchronizeFilter.php b/src/FFMpeg/Filters/Video/SynchronizeFilter.php new file mode 100644 index 0000000..f189d8f --- /dev/null +++ b/src/FFMpeg/Filters/Video/SynchronizeFilter.php @@ -0,0 +1,49 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Filters/Video/VideoFilterInterface.php b/src/FFMpeg/Filters/Video/VideoFilterInterface.php new file mode 100644 index 0000000..f4cbbcb --- /dev/null +++ b/src/FFMpeg/Filters/Video/VideoFilterInterface.php @@ -0,0 +1,29 @@ + + * + * 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); +} diff --git a/src/FFMpeg/Filters/Video/VideoFilters.php b/src/FFMpeg/Filters/Video/VideoFilters.php new file mode 100644 index 0000000..393580b --- /dev/null +++ b/src/FFMpeg/Filters/Video/VideoFilters.php @@ -0,0 +1,69 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Filters/Video/VideoResampleFilter.php b/src/FFMpeg/Filters/Video/VideoResampleFilter.php new file mode 100644 index 0000000..e35b87d --- /dev/null +++ b/src/FFMpeg/Filters/Video/VideoResampleFilter.php @@ -0,0 +1,72 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Format/Audio/DefaultAudio.php b/src/FFMpeg/Format/Audio/DefaultAudio.php index 7eaa5dd..2a3c653 100644 --- a/src/FFMpeg/Format/Audio/DefaultAudio.php +++ b/src/FFMpeg/Format/Audio/DefaultAudio.php @@ -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); } } diff --git a/src/FFMpeg/Format/Audio/Flac.php b/src/FFMpeg/Format/Audio/Flac.php index 579ffc6..28a7197 100644 --- a/src/FFMpeg/Format/Audio/Flac.php +++ b/src/FFMpeg/Format/Audio/Flac.php @@ -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} diff --git a/src/FFMpeg/Format/Audio/Interactive.php b/src/FFMpeg/Format/Audio/Interactive.php deleted file mode 100644 index eb0254a..0000000 --- a/src/FFMpeg/Format/Audio/Interactive.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * 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(); -} diff --git a/src/FFMpeg/Format/Audio/Mp3.php b/src/FFMpeg/Format/Audio/Mp3.php index c7d45f3..4cd86f5 100644 --- a/src/FFMpeg/Format/Audio/Mp3.php +++ b/src/FFMpeg/Format/Audio/Mp3.php @@ -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} diff --git a/src/FFMpeg/Format/Audio/Resamplable.php b/src/FFMpeg/Format/Audio/Resamplable.php deleted file mode 100644 index 2f8aa81..0000000 --- a/src/FFMpeg/Format/Audio/Resamplable.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * 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(); -} diff --git a/src/FFMpeg/Format/AudioInterface.php b/src/FFMpeg/Format/AudioInterface.php index 22eac12..47ed5d6 100644 --- a/src/FFMpeg/Format/AudioInterface.php +++ b/src/FFMpeg/Format/AudioInterface.php @@ -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(); } diff --git a/src/FFMpeg/Exception/BinaryNotFoundException.php b/src/FFMpeg/Format/FormatInterface.php similarity index 66% rename from src/FFMpeg/Exception/BinaryNotFoundException.php rename to src/FFMpeg/Format/FormatInterface.php index aa16b8c..4495f7a 100644 --- a/src/FFMpeg/Exception/BinaryNotFoundException.php +++ b/src/FFMpeg/Format/FormatInterface.php @@ -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 { - } diff --git a/src/FFMpeg/Format/FrameInterface.php b/src/FFMpeg/Format/FrameInterface.php new file mode 100644 index 0000000..77a27ab --- /dev/null +++ b/src/FFMpeg/Format/FrameInterface.php @@ -0,0 +1,16 @@ + + * + * 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 +{ +} diff --git a/src/FFMpeg/Format/ProgressListener/AbstractProgressListener.php b/src/FFMpeg/Format/ProgressListener/AbstractProgressListener.php new file mode 100644 index 0000000..6066535 --- /dev/null +++ b/src/FFMpeg/Format/ProgressListener/AbstractProgressListener.php @@ -0,0 +1,238 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/FFMpeg/Helper/AudioProgressHelper.php b/src/FFMpeg/Format/ProgressListener/AudioProgressListener.php similarity index 83% rename from src/FFMpeg/Helper/AudioProgressHelper.php rename to src/FFMpeg/Format/ProgressListener/AudioProgressListener.php index 5044469..7fbb0bb 100644 --- a/src/FFMpeg/Helper/AudioProgressHelper.php +++ b/src/FFMpeg/Format/ProgressListener/AudioProgressListener.php @@ -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 */ -class AudioProgressHelper extends ProgressHelper +class AudioProgressListener extends AbstractProgressListener { public function getPattern() { diff --git a/src/FFMpeg/Helper/VideoProgressHelper.php b/src/FFMpeg/Format/ProgressListener/VideoProgressListener.php similarity index 84% rename from src/FFMpeg/Helper/VideoProgressHelper.php rename to src/FFMpeg/Format/ProgressListener/VideoProgressListener.php index 7322abf..6bfa4a1 100644 --- a/src/FFMpeg/Helper/VideoProgressHelper.php +++ b/src/FFMpeg/Format/ProgressListener/VideoProgressListener.php @@ -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 */ -class VideoProgressHelper extends ProgressHelper +class VideoProgressListener extends AbstractProgressListener { public function getPattern() { diff --git a/src/FFMpeg/Format/ProgressableInterface.php b/src/FFMpeg/Format/ProgressableInterface.php new file mode 100644 index 0000000..7318d65 --- /dev/null +++ b/src/FFMpeg/Format/ProgressableInterface.php @@ -0,0 +1,31 @@ + + * + * 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); +} diff --git a/src/FFMpeg/Format/Video/DefaultVideo.php b/src/FFMpeg/Format/Video/DefaultVideo.php index 47b0210..7427175 100644 --- a/src/FFMpeg/Format/Video/DefaultVideo.php +++ b/src/FFMpeg/Format/Video/DefaultVideo.php @@ -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; + } } diff --git a/src/FFMpeg/Format/Video/Interactive.php b/src/FFMpeg/Format/Video/Interactive.php deleted file mode 100644 index 4641d69..0000000 --- a/src/FFMpeg/Format/Video/Interactive.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * 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(); -} diff --git a/src/FFMpeg/Format/Video/Ogg.php b/src/FFMpeg/Format/Video/Ogg.php index a1aca8c..b67fd68 100644 --- a/src/FFMpeg/Format/Video/Ogg.php +++ b/src/FFMpeg/Format/Video/Ogg.php @@ -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} diff --git a/src/FFMpeg/Format/Video/Resamplable.php b/src/FFMpeg/Format/Video/Resamplable.php deleted file mode 100644 index dd00ebc..0000000 --- a/src/FFMpeg/Format/Video/Resamplable.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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(); -} diff --git a/src/FFMpeg/Format/Video/Resizable.php b/src/FFMpeg/Format/Video/Resizable.php deleted file mode 100644 index 335c85f..0000000 --- a/src/FFMpeg/Format/Video/Resizable.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * 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(); -} diff --git a/src/FFMpeg/Format/Video/Transcodable.php b/src/FFMpeg/Format/Video/Transcodable.php deleted file mode 100644 index 2c6a92a..0000000 --- a/src/FFMpeg/Format/Video/Transcodable.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * 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(); -} diff --git a/src/FFMpeg/Format/Video/WMV.php b/src/FFMpeg/Format/Video/WMV.php index ae57ecd..56c170e 100644 --- a/src/FFMpeg/Format/Video/WMV.php +++ b/src/FFMpeg/Format/Video/WMV.php @@ -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} diff --git a/src/FFMpeg/Format/Video/WMV3.php b/src/FFMpeg/Format/Video/WMV3.php index a534b52..575936e 100644 --- a/src/FFMpeg/Format/Video/WMV3.php +++ b/src/FFMpeg/Format/Video/WMV3.php @@ -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} diff --git a/src/FFMpeg/Format/Video/WebM.php b/src/FFMpeg/Format/Video/WebM.php index fbb3abc..e055c6a 100644 --- a/src/FFMpeg/Format/Video/WebM.php +++ b/src/FFMpeg/Format/Video/WebM.php @@ -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} diff --git a/src/FFMpeg/Format/Video/X264.php b/src/FFMpeg/Format/Video/X264.php index f2d4018..40010fd 100644 --- a/src/FFMpeg/Format/Video/X264.php +++ b/src/FFMpeg/Format/Video/X264.php @@ -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} diff --git a/src/FFMpeg/Format/VideoInterface.php b/src/FFMpeg/Format/VideoInterface.php index 6e16a5a..d1bbcf6 100644 --- a/src/FFMpeg/Format/VideoInterface.php +++ b/src/FFMpeg/Format/VideoInterface.php @@ -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(); } diff --git a/src/FFMpeg/Helper/HelperInterface.php b/src/FFMpeg/Helper/HelperInterface.php deleted file mode 100644 index e7778c0..0000000 --- a/src/FFMpeg/Helper/HelperInterface.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * 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 - */ -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); -} diff --git a/src/FFMpeg/Helper/ProgressHelper.php b/src/FFMpeg/Helper/ProgressHelper.php deleted file mode 100644 index bf4fa1f..0000000 --- a/src/FFMpeg/Helper/ProgressHelper.php +++ /dev/null @@ -1,218 +0,0 @@ - - * - * 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 - */ -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(); -} diff --git a/src/FFMpeg/Media/AbstractMediaType.php b/src/FFMpeg/Media/AbstractMediaType.php new file mode 100644 index 0000000..4cb3345 --- /dev/null +++ b/src/FFMpeg/Media/AbstractMediaType.php @@ -0,0 +1,126 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Media/AbstractStreamableMedia.php b/src/FFMpeg/Media/AbstractStreamableMedia.php new file mode 100644 index 0000000..5458015 --- /dev/null +++ b/src/FFMpeg/Media/AbstractStreamableMedia.php @@ -0,0 +1,34 @@ + + * + * 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); + } +} diff --git a/src/FFMpeg/Media/Audio.php b/src/FFMpeg/Media/Audio.php new file mode 100644 index 0000000..2ba0304 --- /dev/null +++ b/src/FFMpeg/Media/Audio.php @@ -0,0 +1,92 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Media/Frame.php b/src/FFMpeg/Media/Frame.php new file mode 100644 index 0000000..9ffb38d --- /dev/null +++ b/src/FFMpeg/Media/Frame.php @@ -0,0 +1,104 @@ + + * + * 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; + } +} diff --git a/src/FFMpeg/Format/Audio/Transcodable.php b/src/FFMpeg/Media/MediaTypeInterface.php similarity index 52% rename from src/FFMpeg/Format/Audio/Transcodable.php rename to src/FFMpeg/Media/MediaTypeInterface.php index 3b85ecc..eca7f52 100644 --- a/src/FFMpeg/Format/Audio/Transcodable.php +++ b/src/FFMpeg/Media/MediaTypeInterface.php @@ -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(); } diff --git a/src/FFMpeg/Media/Video.php b/src/FFMpeg/Media/Video.php new file mode 100644 index 0000000..b368497 --- /dev/null +++ b/src/FFMpeg/Media/Video.php @@ -0,0 +1,166 @@ + + * + * 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); + } +} diff --git a/tests/FFMpeg/Functional/FunctionalTestCase.php b/tests/FFMpeg/Functional/FunctionalTestCase.php new file mode 100644 index 0000000..062bf68 --- /dev/null +++ b/tests/FFMpeg/Functional/FunctionalTestCase.php @@ -0,0 +1,13 @@ + 300)); + } +} diff --git a/tests/FFMpeg/Functional/VideoTranscodeTest.php b/tests/FFMpeg/Functional/VideoTranscodeTest.php new file mode 100644 index 0000000..2b9ff24 --- /dev/null +++ b/tests/FFMpeg/Functional/VideoTranscodeTest.php @@ -0,0 +1,38 @@ +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); + } +} diff --git a/tests/FFMpeg/Functional/output/.placeholder b/tests/FFMpeg/Functional/output/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/FFMpeg/Tests/Coordinate/AspectRatioTest.php b/tests/FFMpeg/Tests/Coordinate/AspectRatioTest.php new file mode 100644 index 0000000..49e703c --- /dev/null +++ b/tests/FFMpeg/Tests/Coordinate/AspectRatioTest.php @@ -0,0 +1,83 @@ +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 + ); + } +} diff --git a/tests/FFMpeg/Tests/Coordinate/DimensionTest.php b/tests/FFMpeg/Tests/Coordinate/DimensionTest.php new file mode 100644 index 0000000..2b3e14b --- /dev/null +++ b/tests/FFMpeg/Tests/Coordinate/DimensionTest.php @@ -0,0 +1,38 @@ +assertEquals(320, $dimension->getWidth()); + $this->assertEquals(240, $dimension->getHeight()); + } +} diff --git a/tests/FFMpeg/Tests/Coordinate/FrameRateTest.php b/tests/FFMpeg/Tests/Coordinate/FrameRateTest.php new file mode 100644 index 0000000..51bfbff --- /dev/null +++ b/tests/FFMpeg/Tests/Coordinate/FrameRateTest.php @@ -0,0 +1,31 @@ +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), + ); + } +} diff --git a/tests/FFMpeg/Tests/Coordinate/PointTest.php b/tests/FFMpeg/Tests/Coordinate/PointTest.php new file mode 100644 index 0000000..33e3d37 --- /dev/null +++ b/tests/FFMpeg/Tests/Coordinate/PointTest.php @@ -0,0 +1,16 @@ +assertEquals(4, $point->getX()); + $this->assertEquals(25, $point->getY()); + } +} diff --git a/tests/FFMpeg/Tests/Coordinate/TimeCodeTest.php b/tests/FFMpeg/Tests/Coordinate/TimeCodeTest.php new file mode 100644 index 0000000..241ee47 --- /dev/null +++ b/tests/FFMpeg/Tests/Coordinate/TimeCodeTest.php @@ -0,0 +1,38 @@ +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'); + } +} diff --git a/tests/FFMpeg/Tests/Driver/FFMpegDriverTest.php b/tests/FFMpeg/Tests/Driver/FFMpegDriverTest.php new file mode 100644 index 0000000..3463c81 --- /dev/null +++ b/tests/FFMpeg/Tests/Driver/FFMpegDriverTest.php @@ -0,0 +1,43 @@ +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()); + } +} diff --git a/tests/FFMpeg/Tests/Driver/FFProbeDriverTest.php b/tests/FFMpeg/Tests/Driver/FFProbeDriverTest.php new file mode 100644 index 0000000..3ec3de5 --- /dev/null +++ b/tests/FFMpeg/Tests/Driver/FFProbeDriverTest.php @@ -0,0 +1,43 @@ +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()); + } +} diff --git a/tests/FFMpeg/Tests/FFMpegServiceProviderTest.php b/tests/FFMpeg/Tests/FFMpegServiceProviderTest.php new file mode 100644 index 0000000..371133e --- /dev/null +++ b/tests/FFMpeg/Tests/FFMpegServiceProviderTest.php @@ -0,0 +1,69 @@ +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']; + } +} diff --git a/tests/FFMpeg/Tests/FFMpegTest.php b/tests/FFMpeg/Tests/FFMpegTest.php new file mode 100644 index 0000000..6695df0 --- /dev/null +++ b/tests/FFMpeg/Tests/FFMpegTest.php @@ -0,0 +1,112 @@ +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()); + } +} diff --git a/tests/FFMpeg/Tests/FFProbe/DataMapping/AbstractDataTest.php b/tests/FFMpeg/Tests/FFProbe/DataMapping/AbstractDataTest.php new file mode 100644 index 0000000..d2a4d63 --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbe/DataMapping/AbstractDataTest.php @@ -0,0 +1,56 @@ + '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 +{ +} diff --git a/tests/FFMpeg/Tests/FFProbe/DataMapping/StreamCollectionTest.php b/tests/FFMpeg/Tests/FFProbe/DataMapping/StreamCollectionTest.php new file mode 100644 index 0000000..e010f1a --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbe/DataMapping/StreamCollectionTest.php @@ -0,0 +1,89 @@ +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()); + } +} diff --git a/tests/FFMpeg/Tests/FFProbe/DataMapping/StreamTest.php b/tests/FFMpeg/Tests/FFProbe/DataMapping/StreamTest.php new file mode 100644 index 0000000..e1ea7b6 --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbe/DataMapping/StreamTest.php @@ -0,0 +1,43 @@ +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')), + ); + } +} diff --git a/tests/FFMpeg/Tests/FFProbe/MapperTest.php b/tests/FFMpeg/Tests/FFProbe/MapperTest.php new file mode 100644 index 0000000..519e1b4 --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbe/MapperTest.php @@ -0,0 +1,44 @@ +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']))), + ); + } +} diff --git a/tests/FFMpeg/Tests/FFProbe/OptionsTesterTest.php b/tests/FFMpeg/Tests/FFProbe/OptionsTesterTest.php new file mode 100644 index 0000000..b743a2c --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbe/OptionsTesterTest.php @@ -0,0 +1,101 @@ +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)); + } +} diff --git a/tests/FFMpeg/Tests/FFProbe/OutputParserTest.php b/tests/FFMpeg/Tests/FFProbe/OutputParserTest.php new file mode 100644 index 0000000..96e90af --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbe/OutputParserTest.php @@ -0,0 +1,42 @@ +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), + ); + } +} diff --git a/tests/FFMpeg/Tests/FFProbeTest.php b/tests/FFMpeg/Tests/FFProbeTest.php new file mode 100644 index 0000000..bf0ca3c --- /dev/null +++ b/tests/FFMpeg/Tests/FFProbeTest.php @@ -0,0 +1,308 @@ +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()), + ); + } +} diff --git a/tests/FFMpeg/Tests/Filters/Audio/AudioFiltersTest.php b/tests/FFMpeg/Tests/Filters/Audio/AudioFiltersTest.php new file mode 100644 index 0000000..930cca9 --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/Audio/AudioFiltersTest.php @@ -0,0 +1,26 @@ +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()); + } +} diff --git a/tests/FFMpeg/Tests/Filters/Audio/AudioResamplableFilterTest.php b/tests/FFMpeg/Tests/Filters/Audio/AudioResamplableFilterTest.php new file mode 100644 index 0000000..fbed42c --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/Audio/AudioResamplableFilterTest.php @@ -0,0 +1,24 @@ +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)); + } +} diff --git a/tests/FFMpeg/Tests/Filters/FiltersCollectionTest.php b/tests/FFMpeg/Tests/Filters/FiltersCollectionTest.php new file mode 100644 index 0000000..6ac820e --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/FiltersCollectionTest.php @@ -0,0 +1,30 @@ +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()); + } +} diff --git a/tests/FFMpeg/Tests/Filters/Video/ResizeFilterTest.php b/tests/FFMpeg/Tests/Filters/Video/ResizeFilterTest.php new file mode 100644 index 0000000..09b58a6 --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/Video/ResizeFilterTest.php @@ -0,0 +1,75 @@ +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')), + ); + } +} diff --git a/tests/FFMpeg/Tests/Filters/Video/SynchronizeFilterTest.php b/tests/FFMpeg/Tests/Filters/Video/SynchronizeFilterTest.php new file mode 100644 index 0000000..8a50f05 --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/Video/SynchronizeFilterTest.php @@ -0,0 +1,58 @@ +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')), + ); + } +} diff --git a/tests/FFMpeg/Tests/Filters/Video/VideoFiltersTest.php b/tests/FFMpeg/Tests/Filters/Video/VideoFiltersTest.php new file mode 100644 index 0000000..3524d6f --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/Video/VideoFiltersTest.php @@ -0,0 +1,79 @@ +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(); + } +} diff --git a/tests/FFMpeg/Tests/Filters/Video/VideoResampleFilterTest.php b/tests/FFMpeg/Tests/Filters/Video/VideoResampleFilterTest.php new file mode 100644 index 0000000..dc6634c --- /dev/null +++ b/tests/FFMpeg/Tests/Filters/Video/VideoResampleFilterTest.php @@ -0,0 +1,44 @@ +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)); + } +} diff --git a/tests/FFMpeg/Tests/Format/Audio/AudioTestCase.php b/tests/FFMpeg/Tests/Format/Audio/AudioTestCase.php new file mode 100644 index 0000000..ff75478 --- /dev/null +++ b/tests/FFMpeg/Tests/Format/Audio/AudioTestCase.php @@ -0,0 +1,96 @@ +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(); +} diff --git a/tests/FFMpeg/Tests/Format/Audio/FlacTest.php b/tests/FFMpeg/Tests/Format/Audio/FlacTest.php new file mode 100644 index 0000000..6b45226 --- /dev/null +++ b/tests/FFMpeg/Tests/Format/Audio/FlacTest.php @@ -0,0 +1,13 @@ +getFFProbeMock(); + $ffprobe->expects($this->once()) + ->method('format') + ->with(__FILE__) + ->will($this->returnValue(new Format(array( + 'size' => $size, + 'duration' => $duration, + )))); + + $listener = new AudioProgressListener($ffprobe, __FILE__, $currentPass, $totalPass); + $phpunit = $this; + $n = 0; + $listener->on('progress', function ($percent, $remaining, $rate) use (&$n, $phpunit, $expectedPercent, $expectedRemaining, $expectedRate, $expectedPercent2, $expectedRemaining2, $expectedRate2) { + if (0 === $n) { + $phpunit->assertEquals($expectedPercent, $percent); + $phpunit->assertEquals($expectedRemaining, $remaining); + $phpunit->assertEquals($expectedRate, $rate); + } elseif (1 === $n) { + $phpunit->assertEquals($expectedPercent2, $percent); + $phpunit->assertEquals($expectedRemaining2, $remaining); + $phpunit->assertLessThan($expectedRate2 + 3, $rate); + $phpunit->assertGreaterThan($expectedRate2 - 3, $rate); + } + $n++; + }); + // first one does not trigger progress event + $listener->handle('any-type'.mt_rand(), $data); + sleep(1); + $listener->handle('any-type'.mt_rand(), $data); + sleep(1); + $listener->handle('any-type'.mt_rand(), $data2); + $this->assertEquals(2, $n); + } + + public function provideData() + { + return array( + array( + 2894412, + 180.900750, + 'size= 712kB time=00:00:45.50 bitrate= 128.1kbits/s', + 25, + 0, + 0, + 'size= 1274kB time=00:01:29.32 bitrate= 142.8kbits/s', + 49, + 2, + 563, + 1, + 1 + ), + array( + 2894412, + 180.900750, + 'size= 712kB time=00:00:45.50 bitrate= 128.1kbits/s', + 12, + 0, + 0, + 'size= 1274kB time=00:01:29.32 bitrate= 142.8kbits/s', + 24, + 2, + 563, + 1, + 2 + ) + ); + } +} diff --git a/tests/FFMpeg/Tests/Format/ProgressListener/VideoProgressListenerTest.php b/tests/FFMpeg/Tests/Format/ProgressListener/VideoProgressListenerTest.php new file mode 100644 index 0000000..1e6663d --- /dev/null +++ b/tests/FFMpeg/Tests/Format/ProgressListener/VideoProgressListenerTest.php @@ -0,0 +1,87 @@ +getFFProbeMock(); + $ffprobe->expects($this->once()) + ->method('format') + ->with(__FILE__) + ->will($this->returnValue(new Format(array( + 'size' => $size, + 'duration' => $duration, + )))); + + $listener = new VideoProgressListener($ffprobe, __FILE__, $currentPass, $totalPass); + $phpunit = $this; + $n = 0; + $listener->on('progress', function ($percent, $remaining, $rate) use (&$n, $phpunit, $expectedPercent, $expectedRemaining, $expectedRate, $expectedPercent2, $expectedRemaining2, $expectedRate2) { + if (0 === $n) { + $phpunit->assertEquals($expectedPercent, $percent); + $phpunit->assertEquals($expectedRemaining, $remaining); + $phpunit->assertEquals($expectedRate, $rate); + } elseif (1 === $n) { + $phpunit->assertEquals($expectedPercent2, $percent); + $phpunit->assertEquals($expectedRemaining2, $remaining); + $phpunit->assertLessThan($expectedRate2 + 10, $rate); + $phpunit->assertGreaterThan($expectedRate2 - 10, $rate); + } + $n++; + }); + // first one does not trigger progress event + $listener->handle('any-type'.mt_rand(), $data); + sleep(1); + $listener->handle('any-type'.mt_rand(), $data); + sleep(1); + $listener->handle('any-type'.mt_rand(), $data2); + $this->assertEquals(2, $n); + } + + public function provideData() + { + return array( + array( + 147073958, + 281.147533, + 'frame= 206 fps=202 q=10.0 size= 571kB time=00:00:07.12 bitrate= 656.8kbits/s dup=9 drop=0', + 2, + 0, + 0, + 'frame= 854 fps=113 q=20.0 size= 4430kB time=00:00:33.04 bitrate=1098.5kbits/s dup=36 drop=0', + 11, + 32, + 3868, + 1, + 1 + ), + array( + 147073958, + 281.147533, + 'frame= 206 fps=202 q=10.0 size= 571kB time=00:00:07.12 bitrate= 656.8kbits/s dup=9 drop=0', + 1, + 0, + 0, + 'frame= 854 fps=113 q=20.0 size= 4430kB time=00:00:33.04 bitrate=1098.5kbits/s dup=36 drop=0', + 5, + 32, + 3868, + 1, + 2 + ) + ); + } +} diff --git a/tests/FFMpeg/Tests/Format/Video/OggTest.php b/tests/FFMpeg/Tests/Format/Video/OggTest.php new file mode 100644 index 0000000..16bb2ae --- /dev/null +++ b/tests/FFMpeg/Tests/Format/Video/OggTest.php @@ -0,0 +1,13 @@ +assertScalar($this->getFormat()->getVideoCodec()); + $this->assertContains($this->getFormat()->getVideoCodec(), $this->getFormat()->getAvailableVideoCodecs()); + } + + public function testSupportBFrames() + { + $this->assertInternalType('boolean', $this->getFormat()->supportBFrames()); + } + + public function testSetVideoCodec() + { + $format = $this->getFormat(); + + foreach ($format->getAvailableVideoCodecs() as $codec) { + $format->setVideoCodec($codec); + $this->assertEquals($codec, $format->getVideoCodec()); + } + } + + public function testGetKiloBitrate() + { + $this->assertInternalType('integer', $this->getFormat()->getKiloBitrate()); + } + + public function testSetKiloBitrate() + { + $format = $this->getFormat(); + $format->setKiloBitrate(2560); + $this->assertEquals(2560, $format->getKiloBitrate()); + } + + /** + * @expectedException FFMpeg\Exception\InvalidArgumentException + */ + public function testSetInvalidVideoCodec() + { + $this->getFormat()->setVideoCodec('invalid-random-video-codec'); + } + + public function testGetAvailableVideoCodecs() + { + $this->assertGreaterThan(0, count($this->getFormat()->getAvailableVideoCodecs())); + } + + 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\VideoProgressListener', $listener); + $this->assertSame($ffprobe, $listener->getFFProbe()); + $this->assertSame(__FILE__, $listener->getPathfile()); + $this->assertSame(1, $listener->getCurrentPass()); + $this->assertSame(3, $listener->getTotalPass()); + } + } + + public function testGetPasses() + { + $this->assertInternalType('integer', $this->getFormat()->getPasses()); + $this->assertGreaterThan(0, $this->getFormat()->getPasses()); + } + + public function testGetModulus() + { + $this->assertInternalType('integer', $this->getFormat()->getModulus()); + $this->assertGreaterThan(0, $this->getFormat()->getModulus()); + $this->assertEquals(0, $this->getFormat()->getModulus() % 2); + } +} diff --git a/tests/FFMpeg/Tests/Format/Video/WMV3Test.php b/tests/FFMpeg/Tests/Format/Video/WMV3Test.php new file mode 100644 index 0000000..c035c60 --- /dev/null +++ b/tests/FFMpeg/Tests/Format/Video/WMV3Test.php @@ -0,0 +1,13 @@ +getClassName(); + $ffprobe = $this->getFFProbeMock(); + $format = $this->getFormatMock(); + + $ffprobe->expects($this->once()) + ->method('format') + ->with(__FILE__) + ->will($this->returnValue($format)); + + $media = new $classname(__FILE__, $this->getFFMpegDriverMock(), $ffprobe); + $this->assertSame($format, $media->getFormat()); + } + + public function testGetFormat() + { + $classname = $this->getClassName(); + $ffprobe = $this->getFFProbeMock(); + $streams = $this->getStreamCollectionMock(); + + $ffprobe->expects($this->once()) + ->method('streams') + ->with(__FILE__) + ->will($this->returnValue($streams)); + + $media = new $classname(__FILE__, $this->getFFMpegDriverMock(), $ffprobe); + $this->assertSame($streams, $media->getStreams()); + } + + abstract protected function getClassName(); +} diff --git a/tests/FFMpeg/Tests/Media/AudioTest.php b/tests/FFMpeg/Tests/Media/AudioTest.php new file mode 100644 index 0000000..79f5653 --- /dev/null +++ b/tests/FFMpeg/Tests/Media/AudioTest.php @@ -0,0 +1,268 @@ +getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + new Audio('/no/file', $driver, $ffprobe); + } + + public function testFiltersReturnsAudioFilters() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $audio = new Audio(__FILE__, $driver, $ffprobe); + $this->assertInstanceOf('FFMpeg\Filters\Audio\AudioFilters', $audio->filters()); + } + + public function testAddFiltersAddsAFilter() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $filters = $this->getMockBuilder('FFMpeg\Filters\FiltersCollection') + ->disableOriginalConstructor() + ->getMock(); + + $audio = new Audio(__FILE__, $driver, $ffprobe); + $audio->setFiltersCollection($filters); + + $filter = $this->getMock('FFMpeg\Filters\Audio\AudioFilterInterface'); + + $filters->expects($this->once()) + ->method('add') + ->with($filter); + + $audio->addFilter($filter); + } + + public function testSaveWithFailure() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $outputPathfile = '/target/file'; + + $format = $this->getMock('FFMpeg\Format\AudioInterface'); + $format->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + + $configuration = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + + $driver->expects($this->any()) + ->method('getConfiguration') + ->will($this->returnValue($configuration)); + + $failure = new ExecutionFailureException('failed to encode'); + $driver->expects($this->once()) + ->method('command') + ->will($this->throwException($failure)); + + $audio = new Audio(__FILE__, $driver, $ffprobe); + $this->setExpectedException('FFMpeg\Exception\RuntimeException'); + $audio->save($format, $outputPathfile); + } + + public function testSaveAppliesFilters() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $outputPathfile = '/target/file'; + $format = $this->getMock('FFMpeg\Format\AudioInterface'); + $format->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + + $configuration = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + + $driver->expects($this->any()) + ->method('getConfiguration') + ->will($this->returnValue($configuration)); + + $audio = new Audio(__FILE__, $driver, $ffprobe); + + $filter = $this->getMock('FFMpeg\Filters\Audio\AudioFilterInterface'); + $filter->expects($this->once()) + ->method('apply') + ->with($audio, $format) + ->will($this->returnValue(array('extra-filter-command'))); + + $capturedCommands = array(); + + $driver->expects($this->once()) + ->method('command') + ->with($this->isType('array'), false, $this->anything()) + ->will($this->returnCallback(function ($commands, $errors, $listeners) use (&$capturedCommands) { + $capturedCommands[] = $commands; + })); + + $audio->addFilter($filter); + $audio->save($format, $outputPathfile); + + foreach ($capturedCommands as $commands) { + $this->assertEquals('-y', $commands[0]); + $this->assertEquals('-i', $commands[1]); + $this->assertEquals(__FILE__, $commands[2]); + $this->assertEquals('extra-filter-command', $commands[3]); + } + } + + /** + * @dataProvider provideSaveData + */ + public function testSaveShouldSave($threads, $expectedCommands, $expectedListeners, $format) + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $configuration = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + + $driver->expects($this->any()) + ->method('getConfiguration') + ->will($this->returnValue($configuration)); + + $configuration->expects($this->once()) + ->method('has') + ->with($this->equalTo('ffmpeg.threads')) + ->will($this->returnValue($threads)); + + if ($threads) { + $configuration->expects($this->once()) + ->method('get') + ->with($this->equalTo('ffmpeg.threads')) + ->will($this->returnValue(24)); + } else { + $configuration->expects($this->never()) + ->method('get'); + } + + $capturedCommand = $capturedListeners = null; + + $driver->expects($this->once()) + ->method('command') + ->with($this->isType('array'), false, $this->anything()) + ->will($this->returnCallback(function ($commands, $errors, $listeners) use (&$capturedCommand, &$capturedListeners) { + $capturedCommand = $commands; + $capturedListeners = $listeners; + })); + + $outputPathfile = '/target/file'; + + $audio = new Audio(__FILE__, $driver, $ffprobe); + $audio->save($format, $outputPathfile); + + $this->assertEquals($expectedCommands, $capturedCommand); + $this->assertEquals($expectedListeners, $capturedListeners); + } + + public function provideSaveData() + { + $format = $this->getMock('FFMpeg\Format\AudioInterface'); + $format->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + $format->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(663)); + + $audioFormat = $this->getMock('FFMpeg\Format\AudioInterface'); + $audioFormat->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + $audioFormat->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(664)); + $audioFormat->expects($this->any()) + ->method('getAudioCodec') + ->will($this->returnValue('patati-patata-audio')); + + $formatExtra = $this->getMock('FFMpeg\Format\AudioInterface'); + $formatExtra->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array('extra', 'param'))); + $formatExtra->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(665)); + + $listeners = array($this->getMock('Alchemy\BinaryDriver\Listeners\ListenerInterface')); + + $progressableFormat = $this->getMockBuilder('FFMpeg\Tests\Media\AudioProg') + ->disableOriginalConstructor()->getMock(); + $progressableFormat->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + $progressableFormat->expects($this->any()) + ->method('createProgressListener') + ->will($this->returnValue($listeners)); + $progressableFormat->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(666)); + + return array( + array(false, array( + '-y', '-i', __FILE__, + '-b:a', '663k', + '/target/file', + ), null, $format), + array(false, array( + '-y', '-i', __FILE__, + '-acodec', 'patati-patata-audio', + '-b:a', '664k', + '/target/file', + ), null, $audioFormat), + array(false, array( + '-y', '-i', __FILE__, + 'extra', 'param', + '-b:a', '665k', + '/target/file', + ), null, $formatExtra), + array(true, array( + '-y', '-i', __FILE__, + '-threads', 24, + '-b:a', '663k', + '/target/file', + ), null, $format), + array(true, array( + '-y', '-i', __FILE__, + 'extra', 'param', + '-threads', 24, + '-b:a', '665k', + '/target/file', + ), null, $formatExtra), + array(false, array( + '-y', '-i', __FILE__, + '-b:a', '666k', + '/target/file', + ), $listeners, $progressableFormat), + array(true, array( + '-y', '-i', __FILE__, + '-threads', 24, + '-b:a', '666k', + '/target/file', + ), $listeners, $progressableFormat), + ); + } + + public function getClassName() + { + return 'FFMpeg\Media\Audio'; + } +} + +abstract class AudioProg implements ProgressableInterface, AudioInterface +{ +} diff --git a/tests/FFMpeg/Tests/Media/FrameTest.php b/tests/FFMpeg/Tests/Media/FrameTest.php new file mode 100644 index 0000000..7d0b8e4 --- /dev/null +++ b/tests/FFMpeg/Tests/Media/FrameTest.php @@ -0,0 +1,98 @@ +getFFMpegDriverMock(), $this->getFFProbeMock(), $this->getTimeCodeMock()); + } + + public function testGetTimeCode() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + + $frame = new Frame(__FILE__, $driver, $ffprobe, $timecode); + $this->assertSame($timecode, $frame->getTimeCode()); + } + + public function testFiltersReturnFilters() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + + $frame = new Frame(__FILE__, $driver, $ffprobe, $timecode); + $this->assertInstanceOf('FFMpeg\Filters\Frame\FrameFilters', $frame->filters()); + } + + public function testAddFiltersAddsAFilter() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + + $filters = $this->getMockBuilder('FFMpeg\Filters\FiltersCollection') + ->disableOriginalConstructor() + ->getMock(); + + $filter = $this->getMock('FFMpeg\Filters\Frame\FrameFilterInterface'); + + $filters->expects($this->once()) + ->method('add') + ->with($filter); + + $frame = new Frame(__FILE__, $driver, $ffprobe, $timecode); + $frame->setFiltersCollection($filters); + $frame->addFilter($filter); + } + + /** + * @dataProvider provideSaveAsOptions + */ + public function testSaveAs($accurate, $commands) + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + $timecode->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('timecode')); + + $pathfile = '/target/destination'; + + array_push($commands, $pathfile); + + $driver->expects($this->once()) + ->method('command') + ->with($commands); + + $frame = new Frame(__FILE__, $driver, $ffprobe, $timecode); + $this->assertSame($frame, $frame->saveAs($pathfile, $accurate)); + } + + public function provideSaveAsOptions() + { + return array( + array(false, array( + '-ss', 'timecode', + '-i', __FILE__, + '-vframes', '1', + '-f', 'image2') + ), + array(true, array( + '-i', __FILE__, + '-vframes', '1', '-ss', 'timecode', + '-f', 'image2' + )), + ); + } +} diff --git a/tests/FFMpeg/Tests/Media/VideoTest.php b/tests/FFMpeg/Tests/Media/VideoTest.php new file mode 100644 index 0000000..14f290d --- /dev/null +++ b/tests/FFMpeg/Tests/Media/VideoTest.php @@ -0,0 +1,394 @@ +getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + new Video('/no/file', $driver, $ffprobe); + } + + public function testFiltersReturnsVideoFilters() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $video = new Video(__FILE__, $driver, $ffprobe); + $this->assertInstanceOf('FFMpeg\Filters\Video\VideoFilters', $video->filters()); + } + + public function testAddFiltersAddsAFilter() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $filters = $this->getMockBuilder('FFMpeg\Filters\FiltersCollection') + ->disableOriginalConstructor() + ->getMock(); + + $video = new Video(__FILE__, $driver, $ffprobe); + $video->setFiltersCollection($filters); + + $filter = $this->getMock('FFMpeg\Filters\Video\VideoFilterInterface'); + + $filters->expects($this->once()) + ->method('add') + ->with($filter); + + $video->addFilter($filter); + } + + public function testFrameShouldReturnAFrame() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $at = $this->getTimeCodeMock(); + + $video = new Video(__FILE__, $driver, $ffprobe); + $frame = $video->frame($at); + + $this->assertInstanceOf('FFMpeg\Media\Frame', $frame); + $this->assertSame($at, $frame->getTimeCode()); + $this->assertSame(__FILE__, $frame->getPathfile()); + } + + public function testSaveWithFailure() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $outputPathfile = '/target/file'; + $format = $this->getMock('FFMpeg\Format\VideoInterface'); + $format->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + + $configuration = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + + $driver->expects($this->any()) + ->method('getConfiguration') + ->will($this->returnValue($configuration)); + + $failure = new ExecutionFailureException('failed to encode'); + $driver->expects($this->once()) + ->method('command') + ->will($this->throwException($failure)); + + $video = new Video(__FILE__, $driver, $ffprobe); + $this->setExpectedException('FFMpeg\Exception\RuntimeException'); + $video->save($format, $outputPathfile); + } + + public function testSaveAppliesFilters() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $outputPathfile = '/target/file'; + $format = $this->getMock('FFMpeg\Format\VideoInterface'); + $format->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + + $configuration = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + + $driver->expects($this->any()) + ->method('getConfiguration') + ->will($this->returnValue($configuration)); + + $video = new Video(__FILE__, $driver, $ffprobe); + + $filter = $this->getMock('FFMpeg\Filters\Video\VideoFilterInterface'); + $filter->expects($this->once()) + ->method('apply') + ->with($video, $format) + ->will($this->returnValue(array('extra-filter-command'))); + + $capturedCommands = array(); + + $driver->expects($this->exactly(2)) + ->method('command') + ->with($this->isType('array'), false, $this->anything()) + ->will($this->returnCallback(function ($commands, $errors, $listeners) use (&$capturedCommands) { + $capturedCommands[] = $commands; + })); + + $video->addFilter($filter); + $video->save($format, $outputPathfile); + + foreach ($capturedCommands as $commands) { + $this->assertEquals('-y', $commands[0]); + $this->assertEquals('-i', $commands[1]); + $this->assertEquals(__FILE__, $commands[2]); + $this->assertEquals('extra-filter-command', $commands[3]); + } + } + + /** + * @dataProvider provideSaveData + */ + public function testSaveShouldSave($threads, $expectedCommands, $expectedListeners, $format) + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + + $configuration = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + + $driver->expects($this->any()) + ->method('getConfiguration') + ->will($this->returnValue($configuration)); + + $configuration->expects($this->once()) + ->method('has') + ->with($this->equalTo('ffmpeg.threads')) + ->will($this->returnValue($threads)); + + if ($threads) { + $configuration->expects($this->once()) + ->method('get') + ->with($this->equalTo('ffmpeg.threads')) + ->will($this->returnValue(24)); + } else { + $configuration->expects($this->never()) + ->method('get'); + } + + $capturedCommands = array(); + $capturedListeners = null; + + $driver->expects($this->exactly(2)) + ->method('command') + ->with($this->isType('array'), false, $this->anything()) + ->will($this->returnCallback(function ($commands, $errors, $listeners) use (&$capturedCommands, &$capturedListeners) { + $capturedCommands[] = $commands; + $capturedListeners = $listeners; + })); + + $outputPathfile = '/target/file'; + + $video = new Video(__FILE__, $driver, $ffprobe); + $video->save($format, $outputPathfile); + + $prefix = null; + + foreach ($capturedCommands as $passKey => $pass) { + foreach ($pass as $command) { + if (0 === strpos($command, 'pass-')) { + $prefix = $command; + break; + } + } + + if (null === $prefix) { + $this->fail('Unable to find pass prefix command.'); + } + + $found = false; + foreach ($pass as $key => $command) { + if ($command === $prefix) { + $found = true; + unset($capturedCommands[$passKey][$key]); + $capturedCommands[$passKey] = array_values($capturedCommands[$passKey]); + break; + } + } + + if (!$found) { + $this->fail('Unable to find pass prefix command back.'); + } + } + + $this->assertEquals($expectedCommands, $capturedCommands); + $this->assertEquals($expectedListeners, $capturedListeners); + } + + public function provideSaveData() + { + $format = $this->getMock('FFMpeg\Format\VideoInterface'); + $format->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + $format->expects($this->any()) + ->method('getKiloBitrate') + ->will($this->returnValue(663)); + $format->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(92)); + + $audioVideoFormat = $this->getMock('FFMpeg\Format\VideoInterface'); + $audioVideoFormat->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + $audioVideoFormat->expects($this->any()) + ->method('getVideoCodec') + ->will($this->returnValue('gloubi-boulga-video')); + $audioVideoFormat->expects($this->any()) + ->method('getAudioCodec') + ->will($this->returnValue('patati-patata-audio')); + $audioVideoFormat->expects($this->any()) + ->method('getKiloBitrate') + ->will($this->returnValue(664)); + $audioVideoFormat->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(92)); + + $formatExtra = $this->getMock('FFMpeg\Format\VideoInterface'); + $formatExtra->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array('extra', 'param'))); + $formatExtra->expects($this->any()) + ->method('getKiloBitrate') + ->will($this->returnValue(665)); + $formatExtra->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(92)); + + $listeners = array($this->getMock('Alchemy\BinaryDriver\Listeners\ListenerInterface')); + + $progressableFormat = $this->getMockBuilder('FFMpeg\Tests\Media\Prog') + ->disableOriginalConstructor()->getMock(); + $progressableFormat->expects($this->any()) + ->method('getExtraParams') + ->will($this->returnValue(array())); + $progressableFormat->expects($this->any()) + ->method('createProgressListener') + ->will($this->returnValue($listeners)); + $progressableFormat->expects($this->any()) + ->method('getKiloBitrate') + ->will($this->returnValue(666)); + $progressableFormat->expects($this->any()) + ->method('getAudioKiloBitrate') + ->will($this->returnValue(92)); + + return array( + array(false, array(array( + '-y', '-i', __FILE__, '-b:v', '663k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + '-b:v', '663k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), null, $format), + array(false, array(array( + '-y', '-i', __FILE__, + '-vcodec', 'gloubi-boulga-video', + '-acodec', 'patati-patata-audio', '-b:v', '664k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + '-vcodec', 'gloubi-boulga-video', + '-acodec', 'patati-patata-audio', + '-b:v', '664k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), null, $audioVideoFormat), + array(false, array(array( + '-y', '-i', __FILE__, + 'extra', 'param','-b:v', '665k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + 'extra', 'param', '-b:v', '665k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), null, $formatExtra), + array(true, array(array( + '-y', '-i', __FILE__, + '-threads', 24, '-b:v', '663k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + '-threads', 24, + '-b:v', '663k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), null, $format), + array(true, array(array( + '-y', '-i', __FILE__, + 'extra', 'param', '-threads', 24, '-b:v', '665k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + 'extra', 'param', '-threads', 24, '-b:v', '665k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), null, $formatExtra), + array(false, array(array( + '-y', '-i', __FILE__, '-b:v', '666k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + '-b:v', '666k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), $listeners, $progressableFormat), + array(true, array(array( + '-y', '-i', __FILE__, + '-threads', 24, '-b:v', '666k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '1', '-passlogfile', + '-an', '/target/file', + ), array( + '-y', '-i', __FILE__, + '-threads', 24, + '-b:v', '666k', + '-refs', '6', '-coder', '1', '-sc_threshold', '40', '-flags', '+loop', + '-me_range', '16', '-subq', '7', '-i_qfactor', '0.71', '-qcomp', '0.6', + '-qdiff', '4', '-trellis', '1', '-b:a', '92k', '-pass', '2', '-passlogfile', + '-ac', '2', '-ar', '44100', '/target/file', + )), $listeners, $progressableFormat), + ); + } + + public function getClassName() + { + return 'FFMpeg\Media\Video'; + } +} + +abstract class Prog implements ProgressableInterface, VideoInterface +{ +} diff --git a/tests/FFMpeg/Tests/TestCase.php b/tests/FFMpeg/Tests/TestCase.php new file mode 100644 index 0000000..d347acd --- /dev/null +++ b/tests/FFMpeg/Tests/TestCase.php @@ -0,0 +1,131 @@ +assertTrue(is_scalar($value)); + } + + public function getLoggerMock() + { + return $this->getMock('Psr\Log\LoggerInterface'); + } + + public function getCacheMock() + { + return $this->getMock('Doctrine\Common\Cache\Cache'); + } + + public function getTimeCodeMock() + { + return $this->getMockBuilder('FFMpeg\Coordinate\TimeCode') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getDimensionMock() + { + return $this->getMockBuilder('FFMpeg\Coordinate\Dimension') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getFramerateMock() + { + return $this->getMockBuilder('FFMpeg\Coordinate\Framerate') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getFFMpegDriverMock() + { + return $this->getMockBuilder('FFMpeg\Driver\FFMpegDriver') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getFFProbeDriverMock() + { + return $this->getMockBuilder('FFMpeg\Driver\FFProbeDriver') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getFFProbeMock() + { + return $this->getMockBuilder('FFMpeg\FFProbe') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getStreamMock() + { + return $this->getMockBuilder('FFMpeg\FFProbe\DataMapping\Stream') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getFFProbeParserMock() + { + return $this->getMock('FFMpeg\FFProbe\OutputParserInterface'); + } + + public function getFFProbeOptionsTesterMock() + { + return $this->getMock('FFMpeg\FFProbe\OptionsTesterInterface'); + } + + public function getFFProbeMapperMock() + { + return $this->getMock('FFMpeg\FFProbe\MapperInterface'); + } + + public function getFFProbeOptionsTesterMockWithOptions(array $options) + { + $tester = $this->getFFProbeOptionsTesterMock(); + + $tester->expects($this->any()) + ->method('has') + ->will($this->returnCallback(function ($option) use ($options) { + return in_array($option, $options); + })); + + return $tester; + } + + public function getConfigurationMock() + { + return $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); + } + + public function getFormatMock() + { + return $this->getMockBuilder('FFMpeg\FFProbe\DataMapping\Format') + ->disableOriginalConstructor() + ->getMock(); + } + + public function getStreamCollectionMock() + { + return $this->getMockBuilder('FFMpeg\FFProbe\DataMapping\StreamCollection') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function getAudioMock() + { + return $this->getMockBuilder('FFMpeg\Media\Audio') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function getVideoMock() + { + return $this->getMockBuilder('FFMpeg\Media\Video') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a6f006d --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +add('FFMpeg\Tests', __DIR__); +$loader->add('FFMpeg\Functional', __DIR__); diff --git a/tests/fixtures/ffprobe/help.raw b/tests/fixtures/ffprobe/help.raw new file mode 100644 index 0000000..fbcc8ba --- /dev/null +++ b/tests/fixtures/ffprobe/help.raw @@ -0,0 +1,512 @@ +Simple multimedia streams analyzer +usage: ffprobe [OPTIONS] [INPUT_FILE] + +Main options: +-L show license +-h topic show help +-? topic show help +-help topic show help +--help topic show help +-version show version +-formats show available formats +-codecs show available codecs +-decoders show available decoders +-encoders show available encoders +-bsfs show available bit stream filters +-protocols show available protocols +-filters show available filters +-pix_fmts show available pixel formats +-layouts show standard channel layouts +-sample_fmts show available audio sample formats +-loglevel loglevel set libav* logging level +-v loglevel set libav* logging level +-report generate a report +-max_alloc bytes set maximum size of a single allocated block +-cpuflags flags force specific cpu flags +-f format force format +-unit show unit of the displayed values +-prefix use SI prefixes for the displayed values +-byte_binary_prefix use binary prefixes for byte units +-sexagesimal use sexagesimal format HOURS:MM:SS.MICROSECONDS for time units +-pretty prettify the format of displayed values, make it more human readable +-print_format format set the output printing format (available formats are: default, compact, csv, flat, ini, json, xml) +-of format alias for -print_format +-select_streams stream_specifier select the specified streams +-sections print sections structure and section information, and exit +-show_data show packets data +-show_error show probing error +-show_format show format/container info +-show_frames show frames info +-show_format_entry entry show a particular entry from the format/container info +-show_entries entry_list show a set of specified entries +-show_packets show packets info +-show_streams show streams info +-count_frames count the number of frames per stream +-count_packets count the number of packets per stream +-show_program_version show ffprobe version +-show_library_versions show library versions +-show_versions show program and library versions +-show_private_data show private data +-private same as show_private_data +-bitexact force bitexact output +-default generic catch all option +-i input_file read specified file + + +AVFormatContext AVOptions: +-avioflags ED.... + direct ED.... reduce buffering +-probesize .D.... set probing size (from 32 to INT_MAX) +-fflags ED.... + ignidx .D.... ignore index + genpts .D.... generate pts + nofillin .D.... do not fill in missing values that can be exactly calculated + noparse .D.... disable AVParsers, this needs nofillin too + igndts .D.... ignore dts + discardcorrupt .D.... discard corrupted frames + sortdts .D.... try to interleave outputted packets by dts + keepside .D.... dont merge side data + nobuffer .D.... reduce the latency introduced by optional buffering +-seek2any .D.... forces seeking to enable seek to any mode (from 0 to 1) +-analyzeduration .D.... specify how many microseconds are analyzed to probe the input (from 0 to INT_MAX) +-cryptokey .D.... decryption key +-indexmem .D.... max memory used for timestamp index (per stream) (from 0 to INT_MAX) +-rtbufsize .D.... max memory used for buffering real-time frames (from 0 to INT_MAX) +-fdebug ED.... print specific debug info + ts ED.... +-max_delay ED.... maximum muxing or demuxing delay in microseconds (from -1 to INT_MAX) +-fpsprobesize .D.... number of frames used to probe fps (from -1 to 2.14748e+09) +-f_err_detect .D.... set error detection flags (deprecated; use err_detect, save via avconv) + crccheck .D.... verify embedded CRCs + bitstream .D.... detect bitstream specification deviations + buffer .D.... detect improper bitstream length + explode .D.... abort decoding on minor error detection + careful .D.... consider things that violate the spec and have not been seen in the wild as errors + compliant .D.... consider all spec non compliancies as errors + aggressive .D.... consider things that a sane encoder shouldnt do as an error +-err_detect .D.... set error detection flags + crccheck .D.... verify embedded CRCs + bitstream .D.... detect bitstream specification deviations + buffer .D.... detect improper bitstream length + explode .D.... abort decoding on minor error detection + careful .D.... consider things that violate the spec and have not been seen in the wild as errors + compliant .D.... consider all spec non compliancies as errors + aggressive .D.... consider things that a sane encoder shouldnt do as an error +-use_wallclock_as_timestamps .D.... use wallclock as timestamps (from 0 to 2.14748e+09) +-skip_initial_bytes .D.... skip initial bytes (from 0 to 2.14748e+09) +-correct_ts_overflow .D.... correct single timestamp overflows (from 0 to 1) + +AVIOContext AVOptions: + +URLContext AVOptions: + +crypto AVOptions: +-key .D.... AES decryption key +-iv .D.... AES decryption initialization vector + +ffrtmphttp AVOptions: +-ffrtmphttp_tls .D.... Use a HTTPS tunneling connection (RTMPTS). (from 0 to 1) + +file AVOptions: + +http AVOptions: +-seekable .D.... control seekability of connection (from -1 to 1) +-headers ED.... set custom HTTP headers, can override built in default headers +-content_type ED.... force a content type +-user-agent .D.... override User-Agent header +-multiple_requests ED.... use persistent connections (from 0 to 1) +-post_data ED.... set custom HTTP post data +-timeout ED.... set timeout of socket I/O operations (from -1 to INT_MAX) + +rtmp AVOptions: +-rtmp_app ED.... Name of application to connect to on the RTMP server +-rtmp_buffer ED.... Set buffer time in milliseconds. The default is 3000. (from 0 to INT_MAX) +-rtmp_conn ED.... Append arbitrary AMF data to the Connect message +-rtmp_flashver ED.... Version of the Flash plugin used to run the SWF player. +-rtmp_live .D.... Specify that the media is a live stream. (from INT_MIN to INT_MAX) + any .D.... both + live .D.... live stream + recorded .D.... recorded stream +-rtmp_pageurl .D.... URL of the web page in which the media was embedded. By default no value will be sent. +-rtmp_playpath ED.... Stream identifier to play or to publish +-rtmp_subscribe .D.... Name of live stream to subscribe to. Defaults to rtmp_playpath. +-rtmp_swfhash .D.... SHA256 hash of the decompressed SWF file (32 bytes). +-rtmp_swfsize .D.... Size of the decompressed SWF file, required for SWFVerification. (from 0 to INT_MAX) +-rtmp_swfurl ED.... URL of the SWF player. By default no value will be sent +-rtmp_swfverify .D.... URL to player swf file, compute hash/size automatically. +-rtmp_tcurl ED.... URL of the target stream. Defaults to proto://host[:port]/app. +-rtmp_listen .D.... Listen for incoming rtmp connections (from INT_MIN to INT_MAX) +-timeout .D.... Maximum timeout (in seconds) to wait for incoming connections. -1 is infinite. Implies -rtmp_listen 1 (from INT_MIN to INT_MAX) + +rtmpt AVOptions: +-rtmp_app ED.... Name of application to connect to on the RTMP server +-rtmp_buffer ED.... Set buffer time in milliseconds. The default is 3000. (from 0 to INT_MAX) +-rtmp_conn ED.... Append arbitrary AMF data to the Connect message +-rtmp_flashver ED.... Version of the Flash plugin used to run the SWF player. +-rtmp_live .D.... Specify that the media is a live stream. (from INT_MIN to INT_MAX) + any .D.... both + live .D.... live stream + recorded .D.... recorded stream +-rtmp_pageurl .D.... URL of the web page in which the media was embedded. By default no value will be sent. +-rtmp_playpath ED.... Stream identifier to play or to publish +-rtmp_subscribe .D.... Name of live stream to subscribe to. Defaults to rtmp_playpath. +-rtmp_swfhash .D.... SHA256 hash of the decompressed SWF file (32 bytes). +-rtmp_swfsize .D.... Size of the decompressed SWF file, required for SWFVerification. (from 0 to INT_MAX) +-rtmp_swfurl ED.... URL of the SWF player. By default no value will be sent +-rtmp_swfverify .D.... URL to player swf file, compute hash/size automatically. +-rtmp_tcurl ED.... URL of the target stream. Defaults to proto://host[:port]/app. +-rtmp_listen .D.... Listen for incoming rtmp connections (from INT_MIN to INT_MAX) +-timeout .D.... Maximum timeout (in seconds) to wait for incoming connections. -1 is infinite. Implies -rtmp_listen 1 (from INT_MIN to INT_MAX) + +srtp AVOptions: + +tcp AVOptions: +-listen ED.... listen on port instead of connecting (from 0 to 1) +-timeout ED.... timeout of socket i/o operations (from 0 to INT_MAX) +-listen_timeout ED.... connection awaiting timeout (from -1 to INT_MAX) + +udp AVOptions: +-buffer_size ED.... Socket buffer size in bytes (from 0 to INT_MAX) +-localport ED.... Set local port to bind to (from 0 to INT_MAX) +-localaddr ED.... Choose local IP address +-pkt_size ED.... Set size of UDP packets (from 0 to INT_MAX) +-reuse ED.... Explicitly allow or disallow reusing UDP sockets (from 0 to 1) +-connect ED.... Should connect() be called on socket (from 0 to 1) +-fifo_size .D.... Set the UDP receiving circular buffer size, expressed as a number of packets with size of 188 bytes (from 0 to INT_MAX) +-overrun_nonfatal .D.... Survive in case of UDP receiving circular buffer overrun (from 0 to 1) +-timeout .D.... In read mode: if no data arrived in more than this time interval, raise error (from 0 to INT_MAX) + +Artworx Data Format demuxer AVOptions: +-linespeed .D.... set simulated line speed (bytes per second) (from 1 to INT_MAX) +-video_size .D.... set video size, such as 640x480 or hd720. +-framerate .D.... set framerate (frames per second) + +aqtdec AVOptions: +-subfps .D...S set the movie frame rate (from 0 to INT_MAX) + +asf demuxer AVOptions: +-no_resync_search .D.... Don't try to resynchronize by looking for a certain optional start code (from 0 to 1) + +avi AVOptions: +-use_odml .D.... use odml index (from -1 to 1) + +Binary text demuxer AVOptions: +-linespeed .D.... set simulated line speed (bytes per second) (from 1 to INT_MAX) +-video_size .D.... set video size, such as 640x480 or hd720. +-framerate .D.... set framerate (frames per second) + +cavsvideo demuxer AVOptions: +-framerate .D.... + +CDXL demuxer AVOptions: +-sample_rate .D.... (from 1 to INT_MAX) +-framerate .D.... + +concat demuxer AVOptions: +-safe .D.... enable safe mode (from -1 to 1) + +dirac demuxer AVOptions: +-framerate .D.... + +dnxhd demuxer AVOptions: +-framerate .D.... + +flvdec AVOptions: +-flv_metadata .D.V.. Allocate streams according the onMetaData array (from 0 to 1) + +g729 demuxer AVOptions: +-bit_rate .D.... (from 0 to INT_MAX) + +GIF demuxer AVOptions: +-min_delay .D.... minimum valid delay between frames (in hundredths of second) (from 0 to 6000) +-default_delay .D.... default delay between frames (in hundredths of second) (from 0 to 6000) + +gsm demuxer AVOptions: +-sample_rate .D.... (from 1 to 6.50753e+07) + +h261 demuxer AVOptions: +-framerate .D.... + +h263 demuxer AVOptions: +-framerate .D.... + +h264 demuxer AVOptions: +-framerate .D.... + +iCE Draw File demuxer AVOptions: +-linespeed .D.... set simulated line speed (bytes per second) (from 1 to INT_MAX) +-video_size .D.... set video size, such as 640x480 or hd720. +-framerate .D.... set framerate (frames per second) + +image2 demuxer AVOptions: +-framerate .D.... set the video framerate +-loop .D.... force loop over input file sequence (from 0 to 1) +-pattern_type .D.... set pattern type (from 0 to INT_MAX) + glob_sequence .D.... select glob/sequence pattern type + glob .D.... select glob pattern type + sequence .D.... select sequence pattern type +-pixel_format .D.... set video pixel format +-start_number .D.... set first number in the sequence (from 0 to INT_MAX) +-start_number_range .D.... set range for looking at the first sequence number (from 1 to INT_MAX) +-video_size .D.... set video size +-frame_size .D.... force frame size in bytes (from 0 to INT_MAX) + +image2pipe demuxer AVOptions: +-framerate .D.... set the video framerate +-loop .D.... force loop over input file sequence (from 0 to 1) +-pattern_type .D.... set pattern type (from 0 to INT_MAX) + glob_sequence .D.... select glob/sequence pattern type + glob .D.... select glob pattern type + sequence .D.... select sequence pattern type +-pixel_format .D.... set video pixel format +-start_number .D.... set first number in the sequence (from 0 to INT_MAX) +-start_number_range .D.... set range for looking at the first sequence number (from 1 to INT_MAX) +-video_size .D.... set video size +-frame_size .D.... force frame size in bytes (from 0 to INT_MAX) + +ingenient demuxer AVOptions: +-framerate .D.... + +m4v demuxer AVOptions: +-framerate .D.... + +mjpeg demuxer AVOptions: +-framerate .D.... + +mov,mp4,m4a,3gp,3g2,mj2 AVOptions: +-use_absolute_path .D.V.. allow using absolute path when opening alias, this is a possible security issue (from 0 to 1) +-ignore_editlist .D.V.. (from 0 to 1) + +mpegtsraw demuxer AVOptions: +-compute_pcr .D.... Compute exact PCR for each transport stream packet. (from 0 to 1) + +mpegvideo demuxer AVOptions: +-framerate .D.... + +alaw demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +mulaw demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +f64be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +f64le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +f32be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +f32le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s32be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s32le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s24be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s24le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s16be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s16le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +s8 demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u32be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u32le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u24be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u24le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u16be demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u16le demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +u8 demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-channels .D.... (from 0 to INT_MAX) + +rawvideo demuxer AVOptions: +-video_size .D.... set frame size +-pixel_format .D.... set pixel format +-framerate .D.... set frame rate + +RTP demuxer AVOptions: +-rtp_flags .D.... RTP flags + filter_src .D.... Only receive packets from the negotiated peer IP + listen .D.... Wait for incoming connections +-reorder_queue_size .D.... Number of packets to buffer for handling of reordered packets (from -1 to INT_MAX) + +RTSP demuxer AVOptions: +-initial_pause .D.... Don't start playing the stream immediately (from 0 to 1) +-rtsp_transport ED.... RTSP transport protocols + udp ED.... UDP + tcp ED.... TCP + udp_multicast .D.... UDP multicast + http .D.... HTTP tunneling +-rtsp_flags .D.... RTSP flags + filter_src .D.... Only receive packets from the negotiated peer IP + listen .D.... Wait for incoming connections +-allowed_media_types .D.... Media types to accept from the server + video .D.... Video + audio .D.... Audio + data .D.... Data +-min_port ED.... Minimum local UDP port (from 0 to 65535) +-max_port ED.... Maximum local UDP port (from 0 to 65535) +-timeout .D.... Maximum timeout (in seconds) to wait for incoming connections. -1 is infinite. Implies flag listen (from INT_MIN to INT_MAX) +-reorder_queue_size .D.... Number of packets to buffer for handling of reordered packets (from -1 to INT_MAX) + +sbg_demuxer AVOptions: +-sample_rate .D.... (from 0 to INT_MAX) +-frame_size .D.... (from 0 to INT_MAX) +-max_file_size .D.... (from 0 to INT_MAX) + +SDP demuxer AVOptions: +-sdp_flags .D.... SDP flags + filter_src .D.... Only receive packets from the negotiated peer IP + listen .D.... Wait for incoming connections + custom_io .D.... Use custom IO +-allowed_media_types .D.... Media types to accept from the server + video .D.... Video + audio .D.... Audio + data .D.... Data +-reorder_queue_size .D.... Number of packets to buffer for handling of reordered packets (from -1 to INT_MAX) + +tedcaptions_demuxer AVOptions: +-start_time .D...S set the start time (offset) of the subtitles, in ms (from I64_MIN to I64_MAX) + +TTY demuxer AVOptions: +-chars_per_frame .D.... (from 1 to INT_MAX) +-video_size .D.... A string describing frame size, such as 640x480 or hd720. +-framerate .D.... + +vc1 demuxer AVOptions: +-framerate .D.... + +WAV demuxer AVOptions: +-ignore_length .D.... Ignore length (from 0 to 1) + +eXtended BINary text (XBIN) demuxer AVOptions: +-linespeed .D.... set simulated line speed (bytes per second) (from 1 to INT_MAX) +-video_size .D.... set video size, such as 640x480 or hd720. +-framerate .D.... set framerate (frames per second) + +lavfi indev AVOptions: +-graph .D.... set libavfilter graph +-graph_file .D.... set libavfilter graph filename +-dumpgraph .D.... dump graph to stderr + +AIFF muxer AVOptions: + +AST muxer AVOptions: + +f4v muxer AVOptions: + +GIF muxer AVOptions: + +hls muxer AVOptions: + +image2 muxer AVOptions: + +ipod muxer AVOptions: + +ismv muxer AVOptions: + +LATM/LOAS muxer AVOptions: + +mov muxer AVOptions: + +MP3 muxer AVOptions: + +mp4 muxer AVOptions: + +mpeg muxer AVOptions: + +vcd muxer AVOptions: + +dvd muxer AVOptions: + +svcd muxer AVOptions: + +vob muxer AVOptions: + +MPEGTS muxer AVOptions: + +Ogg muxer AVOptions: + +psp muxer AVOptions: + +RTP muxer AVOptions: + +RTSP muxer AVOptions: +-initial_pause .D.... Don't start playing the stream immediately (from 0 to 1) +-rtsp_transport ED.... RTSP transport protocols + udp ED.... UDP + tcp ED.... TCP + udp_multicast .D.... UDP multicast + http .D.... HTTP tunneling +-rtsp_flags .D.... RTSP flags + filter_src .D.... Only receive packets from the negotiated peer IP + listen .D.... Wait for incoming connections +-allowed_media_types .D.... Media types to accept from the server + video .D.... Video + audio .D.... Audio + data .D.... Data +-min_port ED.... Minimum local UDP port (from 0 to 65535) +-max_port ED.... Maximum local UDP port (from 0 to 65535) +-timeout .D.... Maximum timeout (in seconds) to wait for incoming connections. -1 is infinite. Implies flag listen (from INT_MIN to INT_MAX) +-reorder_queue_size .D.... Number of packets to buffer for handling of reordered packets (from -1 to INT_MAX) + +segment muxer AVOptions: + +stream_segment muxer AVOptions: + +smooth streaming muxer AVOptions: + +spdif AVOptions: + +tg2 muxer AVOptions: + +tgp muxer AVOptions: + +WAV muxer AVOptions: + +caca_outdev AVOptions: + diff --git a/tests/fixtures/ffprobe/show_format.json b/tests/fixtures/ffprobe/show_format.json new file mode 100644 index 0000000..de962fa --- /dev/null +++ b/tests/fixtures/ffprobe/show_format.json @@ -0,0 +1,20 @@ +{ + "format": { + "filename": "Interview5mm4500.mp4", + "nb_streams": 2, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "300.011000", + "size": "173833712", + "bit_rate": "4635395", + "tags": { + "major_brand": "isom", + "minor_version": "512", + "compatible_brands": "isomiso2avc1mp41", + "creation_time": "1970-01-01 00:00:00", + "title": "5mm 4500.mp4", + "encoder": "Lavf52.108.0" + } + } +} diff --git a/tests/fixtures/ffprobe/show_format.raw b/tests/fixtures/ffprobe/show_format.raw new file mode 100644 index 0000000..1e857e5 --- /dev/null +++ b/tests/fixtures/ffprobe/show_format.raw @@ -0,0 +1,16 @@ +[FORMAT] +filename=Interview5mm4500.mp4 +nb_streams=2 +format_name=mov,mp4,m4a,3gp,3g2,mj2 +format_long_name=QuickTime / MOV +start_time=0.000000 +duration=300.011000 +size=173833712 +bit_rate=4635395 +TAG:major_brand=isom +TAG:minor_version=512 +TAG:compatible_brands=isomiso2avc1mp41 +TAG:creation_time=1970-01-01 00:00:00 +TAG:title=5mm 4500.mp4 +TAG:encoder=Lavf52.108.0 +[/FORMAT] diff --git a/tests/fixtures/ffprobe/show_streams.json b/tests/fixtures/ffprobe/show_streams.json new file mode 100644 index 0000000..decdc36 --- /dev/null +++ b/tests/fixtures/ffprobe/show_streams.json @@ -0,0 +1,88 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_time_base": "1/100", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 960, + "height": 720, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "4:3", + "pix_fmt": "yuv420p", + "level": 32, + "r_frame_rate": "50/1", + "avg_frame_rate": "50/1", + "time_base": "1/50000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 15000000, + "duration": "300.000000", + "bit_rate": "4499198", + "nb_frames": "15000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "1970-01-01 00:00:00", + "language": "und", + "handler_name": "VideoHandler" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 2, + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 14400512, + "duration": "300.010667", + "bit_rate": "127973", + "nb_frames": "14063", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "1970-01-01 00:00:00", + "language": "und", + "handler_name": "SoundHandler" + } + } + ] +} diff --git a/tests/fixtures/ffprobe/show_streams.raw b/tests/fixtures/ffprobe/show_streams.raw new file mode 100644 index 0000000..6ce06c9 --- /dev/null +++ b/tests/fixtures/ffprobe/show_streams.raw @@ -0,0 +1,84 @@ +[STREAM] +index=0 +codec_name=h264 +codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 +profile=High +codec_type=video +codec_time_base=1/100 +codec_tag_string=avc1 +codec_tag=0x31637661 +width=960 +height=720 +has_b_frames=2 +sample_aspect_ratio=1:1 +display_aspect_ratio=4:3 +pix_fmt=yuv420p +level=32 +timecode=N/A +id=N/A +r_frame_rate=50/1 +avg_frame_rate=50/1 +time_base=1/50000 +start_pts=0 +start_time=0.000000 +duration_ts=15000000 +duration=300.000000 +bit_rate=4499198 +nb_frames=15000 +nb_read_frames=N/A +nb_read_packets=N/A +DISPOSITION:default=0 +DISPOSITION:dub=0 +DISPOSITION:original=0 +DISPOSITION:comment=0 +DISPOSITION:lyrics=0 +DISPOSITION:karaoke=0 +DISPOSITION:forced=0 +DISPOSITION:hearing_impaired=0 +DISPOSITION:visual_impaired=0 +DISPOSITION:clean_effects=0 +DISPOSITION:attached_pic=0 +TAG:creation_time=1970-01-01 00:00:00 +TAG:language=und +TAG:handler_name=VideoHandler +[/STREAM] +[STREAM] +index=1 +codec_name=aac +codec_long_name=AAC (Advanced Audio Coding) +profile=unknown +codec_type=audio +codec_time_base=1/48000 +codec_tag_string=mp4a +codec_tag=0x6134706d +sample_fmt=fltp +sample_rate=48000 +channels=2 +bits_per_sample=0 +id=N/A +r_frame_rate=0/0 +avg_frame_rate=0/0 +time_base=1/48000 +start_pts=0 +start_time=0.000000 +duration_ts=14400512 +duration=300.010667 +bit_rate=127973 +nb_frames=14063 +nb_read_frames=N/A +nb_read_packets=N/A +DISPOSITION:default=0 +DISPOSITION:dub=0 +DISPOSITION:original=0 +DISPOSITION:comment=0 +DISPOSITION:lyrics=0 +DISPOSITION:karaoke=0 +DISPOSITION:forced=0 +DISPOSITION:hearing_impaired=0 +DISPOSITION:visual_impaired=0 +DISPOSITION:clean_effects=0 +DISPOSITION:attached_pic=0 +TAG:creation_time=1970-01-01 00:00:00 +TAG:language=und +TAG:handler_name=SoundHandler +[/STREAM] diff --git a/tests/src/FFMpeg/BinaryTest.php b/tests/src/FFMpeg/BinaryTest.php deleted file mode 100644 index 5863da0..0000000 --- a/tests/src/FFMpeg/BinaryTest.php +++ /dev/null @@ -1,103 +0,0 @@ -logger = new Logger('tests'); - $this->logger->pushHandler(new NullHandler()); - } - - /** - * @covers FFMpeg\Binary::__construct - * @expectedException \FFMpeg\Exception\BinaryNotFoundException - */ - public function testConstruct() - { - $binary = new BinaryTester('pretty_binary', $this->logger); - } - - public function testTimeout() - { - $tester = BinaryTester::load($this->logger, 200); - $this->assertEquals(200, $tester->getTimeout()); - } - - public function testDefaultTimeout() - { - $tester = BinaryTester::load($this->logger); - $this->assertEquals(60, $tester->getTimeout()); - } - - public function testNoTimeout() - { - $tester = BinaryTester::load($this->logger, 0); - $this->assertEquals(0, $tester->getTimeout()); - } - - public function testSetTimeout() - { - $tester = BinaryTester::load($this->logger); - $tester->setTimeout(200); - $this->assertEquals(200, $tester->getTimeout()); - } - - /** - * @expectedException \FFMpeg\Exception\InvalidArgumentException - */ - public function testSetInvalidTimeout() - { - $tester = BinaryTester::load($this->logger); - $tester->setTimeout(-1); - } - - /** - * @covers FFMpeg\Binary::load - */ - public function testLoad() - { - BinaryTester::load($this->logger); - } - - /** - * @covers FFMpeg\Binary::load - * @expectedException \FFMpeg\Exception\BinaryNotFoundException - */ - public function testLoadWrongBinary() - { - BinaryTesterWrongBinary::load($this->logger); - } - -} - -class BinaryTester extends Binary -{ - - protected static function getBinaryName() - { - return array('php'); - } - -} - -class BinaryTesterWrongBinary extends Binary -{ - - protected static function getBinaryName() - { - return array(''); - } - -} diff --git a/tests/src/FFMpeg/FFMpegServiceProviderTest.php b/tests/src/FFMpeg/FFMpegServiceProviderTest.php deleted file mode 100644 index 34ae282..0000000 --- a/tests/src/FFMpeg/FFMpegServiceProviderTest.php +++ /dev/null @@ -1,83 +0,0 @@ -getApplication(); - - $app->register(new FFMpegServiceProvider()); - - $this->assertInstanceOf('\\FFMpeg\\FFProbe', $app['ffmpeg.ffprobe']); - $this->assertInstanceOf('\\FFMpeg\\FFMpeg', $app['ffmpeg.ffmpeg']); - } - - /** - * @expectedException FFMpeg\Exception\BinaryNotFoundException - * @covers FFMpeg\FFMpegServiceProvider::register - */ - public function testRegisterFFMpegFails() - { - $app = $this->getApplication(); - $app->register(new FFMpegServiceProvider(), array('ffmpeg.ffmpeg.binary' => '/path/to/no/ffmpeg')); - - $app['ffmpeg.ffmpeg']; - } - - /** - * @expectedException FFMpeg\Exception\BinaryNotFoundException - * @covers FFMpeg\FFMpegServiceProvider::register - */ - public function testRegisterFFProbeFails() - { - $app = $this->getApplication(); - $app->register(new FFMpegServiceProvider(), array('ffmpeg.ffprobe.binary' => '/path/to/no/ffprobe')); - - $app['ffmpeg.ffprobe']; - } - - /** - * @covers FFMpeg\FFMpegServiceProvider::register - */ - public function testRegisterCustomLogger() - { - $app = $this->getApplication(); - $app['logger'] = $app->share(function(Application $app){ - $logger = new Logger('tests'); - $logger->pushHandler(new NullHandler()); - - return $logger; - }); - $app->register(new FFMpegServiceProvider()); - - $this->assertInstanceOf('\\FFMpeg\\FFProbe', $app['ffmpeg.ffprobe']); - $this->assertInstanceOf('\\FFMpeg\\FFMpeg', $app['ffmpeg.ffmpeg']); - } - - /** - * @covers FFMpeg\FFMpegServiceProvider::register - */ - public function testCustomThreadsFFMpeg() - { - $app = $this->getApplication(); - $app->register(new FFMpegServiceProvider(), array('ffmpeg.threads'=>18)); - - $this->assertEquals(18, $app['ffmpeg.ffmpeg']->getThreads()); - } -} - diff --git a/tests/src/FFMpeg/FFMpegTest.php b/tests/src/FFMpeg/FFMpegTest.php deleted file mode 100644 index 33ff70c..0000000 --- a/tests/src/FFMpeg/FFMpegTest.php +++ /dev/null @@ -1,238 +0,0 @@ -logger = new Logger('tests'); - $this->logger->pushHandler(new NullHandler()); - - $this->object = FFMpeg::load($this->logger); - $this->probe = FFProbe::load($this->logger); - $this->object->setProber($this->probe); - } - - /** - * @covers FFMpeg\FFMpeg::open - * @expectedException \InvalidArgumentException - */ - public function testOpenInvalid() - { - $this->object->open(__DIR__ . '/invalid.files'); - } - - /** - * @covers FFMpeg\FFMpeg::open - */ - public function testOpen() - { - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - } - - /** - * @covers FFMpeg\FFMpeg::extractImage - */ - public function testExtractImage() - { - $dest = __DIR__ . '/../../files/extract_Test.jpg'; - - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - $this->object->extractImage(2, $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::extractImage - */ - public function testExtractImagePng() - { - $dest = __DIR__ . '/../../files/extract_Test.png'; - - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - $this->object->extractImage(2, $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::extractImage - */ - public function testExtractImageGif() - { - $dest = __DIR__ . '/../../files/extract_Test.gif'; - - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - $this->object->extractImage(2, $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::extractImage - * @expectedException \FFMpeg\Exception\LogicException - */ - public function testExtractImageNoMovie() - { - $this->object->extractImage(2, 'Path'); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @expectedException \FFMpeg\Exception\LogicException - */ - public function testEncode() - { - $format = new Format\Video\WebM(); - $format-> setDimensions(32, 32); - $this->object->encode($format, './invalid.file'); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @expectedException FFMpeg\Exception\BinaryNotFoundException - */ - public function testWrongBinary() - { - $logger = new \Monolog\Logger('test'); - $logger->pushHandler(new \Monolog\Handler\NullHandler()); - - $ffmpeg = new FFMpeg('wrongbinary', $logger); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @covers FFMpeg\FFMpeg::encodeAudio - */ - public function testEncodeMp3() - { - $dest = __DIR__ . '/../../files/encode_test.mp3'; - - $this->object->open(__DIR__ . '/../../files/Audio.mp3'); - $this->object->encode(new Format\Audio\Mp3(), $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @covers FFMpeg\FFMpeg::encodeAudio - */ - public function testEncodeFlac() - { - $dest = __DIR__ . '/../../files/encode_test.flac'; - - $this->object->open(__DIR__ . '/../../files/Audio.mp3'); - $this->object->encode(new Format\Audio\Flac(), $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @covers FFMpeg\FFMpeg::encodeVideo - */ - public function testEncodeWebm() - { - $dest = __DIR__ . '/../../files/encode_test.webm'; - - $format = new Format\Video\WebM(); - $format-> setDimensions(32, 32); - - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - $this->object->encode($format, $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @covers FFMpeg\FFMpeg::encodeVideo - */ - public function testEncodeOgg() - { - $dest = __DIR__ . '/../../files/encode_test.ogv'; - - $format = new Format\Video\Ogg(); - $format->setDimensions(32, 32); - - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - $this->object->encode($format, $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::encode - * @covers FFMpeg\FFMpeg::encodeVideo - */ - public function testEncodeX264() - { - $dest = __DIR__ . '/../../files/encode_test.mp4'; - - $format = new Format\Video\X264(); - $format-> setDimensions(32, 32); - - $this->object->open(__DIR__ . '/../../files/Test.ogv'); - $this->object->encode($format, $dest); - - $this->probe->probeFormat($dest); - - unlink($dest); - } - - /** - * @covers FFMpeg\FFMpeg::getMultiple - */ - public function testGetMultiple() - { - $object = FFMpegTester::load($this->logger); - $this->assertEquals(336, $object->getMultipleTester(321, 16)); - $this->assertEquals(322, $object->getMultipleTester(321, 2)); - $this->assertEquals(324, $object->getMultipleTester(321, 4)); - $this->assertEquals(328, $object->getMultipleTester(321, 8)); - $this->assertEquals(320, $object->getMultipleTester(319, 16)); - $this->assertEquals(320, $object->getMultipleTester(313, 16)); - $this->assertEquals(320, $object->getMultipleTester(312, 16)); - $this->assertEquals(336, $object->getMultipleTester(329, 16)); - $this->assertEquals(16, $object->getMultipleTester(8, 16)); - } -} - -class FFMpegTester extends FFMpeg -{ - public function getMultipleTester($value, $multiple) - { - return parent::getMultiple($value, $multiple); - } -} diff --git a/tests/src/FFMpeg/FFProbeTest.php b/tests/src/FFMpeg/FFProbeTest.php deleted file mode 100644 index 26df48c..0000000 --- a/tests/src/FFMpeg/FFProbeTest.php +++ /dev/null @@ -1,77 +0,0 @@ -logger = new Logger('tests'); - $this->logger->pushHandler(new NullHandler()); - - $this->object = FFProbe::load($this->logger); - } - - /** - * @covers FFMpeg\FFProbe::probeFormat - * @covers FFMpeg\FFProbe::probeStreams - * @covers FFMpeg\FFProbe::executeProbe - */ - public function testProbe() - { - $this->object->probeFormat(__DIR__ . '/../../files/Test.ogv'); - $this->object->probeStreams(__DIR__ . '/../../files/Test.ogv'); - } - - /** - * @covers FFMpeg\FFProbe::probeFormat - * @covers FFMpeg\FFProbe::executeProbe - * @expectedException \RuntimeException - */ - public function testProbeInvalidFile() - { - $this->object->probeFormat(__DIR__ . '/../../files/WrongFile.mp4'); - } - - /** - * @covers FFMpeg\FFProbe::probeStreams - * @covers FFMpeg\FFProbe::executeProbe - * @expectedException \FFMpeg\Exception\RuntimeException - */ - public function testProbeStreamsInvalidFile() - { - $this->object->probeStreams(__DIR__ . '/../../files/WrongFile.mp4'); - } - - /** - * @covers FFMpeg\FFProbe::probeStreams - * @covers FFMpeg\FFProbe::executeProbe - * @expectedException \InvalidArgumentException - */ - public function testProbeStreamsInvalidPathFile() - { - $this->object->probeStreams(__DIR__ . '/../../files/unknown.file'); - } - - /** - * @covers FFMpeg\FFProbe::probeFormat - * @covers FFMpeg\FFProbe::executeProbe - * @expectedException \InvalidArgumentException - */ - public function testProbeFormatInvalidPathFile() - { - $this->object->probeFormat(__DIR__ . '/../../files/unknown.file'); - } - -} diff --git a/tests/src/FFMpeg/Format/Audio/DefaultAudioTest.php b/tests/src/FFMpeg/Format/Audio/DefaultAudioTest.php deleted file mode 100644 index d24ecaa..0000000 --- a/tests/src/FFMpeg/Format/Audio/DefaultAudioTest.php +++ /dev/null @@ -1,135 +0,0 @@ -object = new DefaultAudioTester(); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::getExtraParams - */ - public function testGetExtraParams() - { - $this->assertEquals('-f format', $this->object->getExtraParams()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::getAudioCodec - */ - public function testGetAudioCodec() - { - $this->assertEquals('audiocodec1', $this->object->getAudioCodec()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::setAudioCodec - */ - public function testSetAudioCodec() - { - $this->object->setAudioCodec('audiocodec2'); - $this->assertEquals('audiocodec2', $this->object->getAudioCodec()); - $this->object->setAudioCodec('audiocodec1'); - $this->assertEquals('audiocodec1', $this->object->getAudioCodec()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::setAudioCodec - * @expectedException \InvalidArgumentException - */ - public function testSetWrongAudioCodec() - { - $this->object->setAudioCodec('audiocodec4'); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::getAudioSampleRate - */ - public function testGetAudioSampleRate() - { - $this->assertEquals(44100, $this->object->getAudioSampleRate()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::setAudioSampleRate - */ - public function testSetAudioSampleRate() - { - $this->object->setAudioSampleRate(22050); - $this->assertEquals(22050, $this->object->getAudioSampleRate()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::setAudioSampleRate - * @expectedException \InvalidArgumentException - * @dataProvider getWrongAudioSampleRate - */ - public function testSetWrongAudioSampleRate($samplerate) - { - $this->object->setAudioSampleRate($samplerate); - } - - public function getWrongAudioSampleRate() - { - return array(array(-5), array(0)); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::getKiloBitrate - */ - public function testGetKiloBitrate() - { - $this->assertEquals(128, $this->object->getKiloBitrate()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::setKiloBitrate - */ - public function testSetKiloBitrate() - { - $this->object->setKiloBitrate(500); - $this->assertEquals(500, $this->object->getKiloBitrate()); - } - - /** - * @covers FFMpeg\Format\Audio\DefaultAudio::setKiloBitrate - * @dataProvider getWrongKiloBitrate - * @expectedException \InvalidArgumentException - */ - public function testSetWrongKiloBitrate($kbrate) - { - $this->object->setKiloBitrate($kbrate); - } - - public function getWrongKiloBitrate() - { - return array(array(-5), array(0)); - } - -} - -class DefaultAudioTester extends DefaultAudio -{ - - protected $audioCodec = 'audiocodec1'; - - public function getAvailableAudioCodecs() - { - return array('audiocodec1', 'audiocodec2', 'audiocodec3'); - } - - public function getExtraParams() - { - return '-f format'; - } - -} diff --git a/tests/src/FFMpeg/Format/Video/DefaultVideoTest.php b/tests/src/FFMpeg/Format/Video/DefaultVideoTest.php deleted file mode 100644 index 8ee49eb..0000000 --- a/tests/src/FFMpeg/Format/Video/DefaultVideoTest.php +++ /dev/null @@ -1,201 +0,0 @@ -object = new DefaultVideoTester(); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setDimensions - * @covers FFMpeg\Format\Video\DefaultVideo::getWidth - * @covers FFMpeg\Format\Video\DefaultVideo::getHeight - */ - public function testSetDimensions() - { - $this->object->setDimensions(240, 640); - $this->assertEquals(240, $this->object->getWidth()); - $this->assertEquals(640, $this->object->getHeight()); - - $this->object->setDimensions(242, 638); - $this->assertEquals(242, $this->object->getWidth()); - $this->assertEquals(638, $this->object->getHeight()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setDimensions - * @dataProvider getWrongDimensions - * @expectedException \InvalidArgumentException - */ - public function testWrongDimensions($width, $height) - { - $this->object->setDimensions($width, $height); - } - - /** - * Data provider for testWrongDimensions - * - * @return array - */ - public function getWrongDimensions() - { - return array( - array(0, 240), - array(240, 0), - array(-5, 240), - array(240, -5), - array(-5, -5), - array(0, 0) - ); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::getFrameRate - */ - public function testGetFrameRate() - { - $this->assertEquals(25, $this->object->getFrameRate()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setFrameRate - */ - public function testSetFrameRate() - { - $this->object->setFrameRate(12); - $this->assertEquals(12, $this->object->getFrameRate()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setFrameRate - * @dataProvider getWrongFrameRates - * @expectedException \InvalidArgumentException - */ - public function testSetWrongFrameRates($framerate) - { - $this->object->setFrameRate($framerate); - } - - /** - * Data provider for testWrongFrameRates - * - * @return array - */ - public function getWrongFramerates() - { - return array(array(-5), array(0)); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::getVideoCodec - */ - public function testGetVideoCodec() - { - $this->assertEquals('videocodec2', $this->object->getVideoCodec()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setVideoCodec - */ - public function testSetVideoCodec() - { - $this->object->setVideoCodec('videocodec2'); - $this->assertEquals('videocodec2', $this->object->getVideoCodec()); - $this->object->setVideoCodec('videocodec1'); - $this->assertEquals('videocodec1', $this->object->getVideoCodec()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setVideoCodec - * @expectedException \InvalidArgumentException - */ - public function testSetWrongVideoCodec() - { - $this->object->setVideoCodec('videocodec4'); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::getGOPsize - */ - public function testGetGOPsize() - { - $this->assertEquals(25, $this->object->getGOPsize()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setGOPsize - */ - public function testSetGOPsize() - { - $this->object->setGOPsize(100); - $this->assertEquals(100, $this->object->getGOPsize()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::setGOPsize - * @dataProvider getWrongGOPsize - * @expectedException \InvalidArgumentException - */ - public function testSetWrongGOPSize($GOP) - { - $this->object->setGOPsize($GOP); - } - - public function getWrongGOPsize() - { - return array(array(-5), array(0)); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::getKiloBitrate - */ - public function testGetKiloBitrate() - { - $this->assertEquals(1000, $this->object->getKiloBitrate()); - } - - /** - * @covers FFMpeg\Format\Video\DefaultVideo::getExtraParams - */ - public function testGetExtraParams() - { - $this->assertTrue(is_array($this->object->getExtraParams())); - } -} - -class DefaultVideoTester extends DefaultVideo -{ - - protected $audioCodec = 'audiocodec1'; - protected $videoCodec = 'videocodec2'; - - public function supportBFrames() - { - return true; - } - - public function getAvailableAudioCodecs() - { - return array('audiocodec1', 'audiocodec2', 'audiocodec3'); - } - - public function getAvailableVideoCodecs() - { - return array('videocodec1', 'videocodec2'); - } - - public function getExtraParams() - { - return array('-f', 'format'); - } - -} diff --git a/tests/src/FFMpeg/Format/Video/OggTest.php b/tests/src/FFMpeg/Format/Video/OggTest.php deleted file mode 100644 index 60160d4..0000000 --- a/tests/src/FFMpeg/Format/Video/OggTest.php +++ /dev/null @@ -1,35 +0,0 @@ -object = new Ogg(); - $this->object->setDimensions(320, 320); - } - - /** - * @covers FFMpeg\Format\Video\Ogg::getAvailableAudioCodecs - */ - public function testGetAvailableAudioCodecs() - { - $this->object->setAudioCodec('libvorbis'); - } - - /** - * @covers FFMpeg\Format\Video\Ogg::getAvailableVideoCodecs - */ - public function testGetAvailableVideoCodecs() - { - $this->object->setVideoCodec('libtheora'); - } - -} diff --git a/tests/src/FFMpeg/Format/Video/WebMTest.php b/tests/src/FFMpeg/Format/Video/WebMTest.php deleted file mode 100644 index bf7cc88..0000000 --- a/tests/src/FFMpeg/Format/Video/WebMTest.php +++ /dev/null @@ -1,43 +0,0 @@ -object = new WebM(); - $this->object->setDimensions(320, 320); - } - - /** - * @covers FFMpeg\Format\Video\WebM::getAvailableAudioCodecs - */ - public function testGetAvailableAudioCodecs() - { - $this->object->setAudioCodec('libvorbis'); - } - - /** - * @covers FFMpeg\Format\Video\WebM::getAvailableVideoCodecs - */ - public function testGetAvailableVideoCodecs() - { - $this->object->setVideoCodec('libvpx'); - } - - /** - * @covers FFMpeg\Format\Video\WebM::getExtraParams - */ - public function testGetExtraParams() - { - $this->assertTrue(is_array($this->object->getExtraParams())); - } - -} diff --git a/tests/src/FFMpeg/Format/Video/X264Test.php b/tests/src/FFMpeg/Format/Video/X264Test.php deleted file mode 100644 index e107418..0000000 --- a/tests/src/FFMpeg/Format/Video/X264Test.php +++ /dev/null @@ -1,35 +0,0 @@ -object = new X264(); - $this->object->setDimensions(320, 320); - } - - /** - * @covers FFMpeg\Format\Video\X264::getAvailableAudioCodecs - */ - public function testGetAvailableAudioCodecs() - { - $this->object->setAudioCodec('libmp3lame'); - } - - /** - * @covers FFMpeg\Format\Video\X264::getAvailableVideoCodecs - */ - public function testGetAvailableVideoCodecs() - { - $this->object->setVideoCodec('libx264'); - } - -} diff --git a/tests/src/FFMpeg/Progress/ProgressTest.php b/tests/src/FFMpeg/Progress/ProgressTest.php deleted file mode 100644 index 3881314..0000000 --- a/tests/src/FFMpeg/Progress/ProgressTest.php +++ /dev/null @@ -1,93 +0,0 @@ -logger = new Logger('tests'); - $this->logger->pushHandler(new NullHandler()); - - $this->object = FFMpeg::load($this->logger); - $this->probe = FFProbe::load($this->logger); - $this->object->setProber($this->probe); - } - - /** - * @covers FFMpeg\Helper\ProgressHelper::parseProgress - * @covers FFMpeg\Helper\ProgressHelper::convertDuration - * @covers FFMpeg\Helper\ProgressHelper::getProgressInfo - * @covers FFMpeg\Helper\AudioProgressHelper::getPattern - */ - public function testProgressHelper() - { - $progressInfo = array(); - - $audioProgress = new AudioProgressHelper(function($percent, $remaining, $rate) use ($progressInfo ) { - $progressInfo[] = $percent; - }); - - $dest = __DIR__ . '/../../../files/encode_test.mp3'; - - $this->object->open(__DIR__ . '/../../../files/Audio.mp3'); - $this->object->attachHelper($audioProgress); - $this->object->encode(new Mp3(), $dest); - - $this->assertGreaterThanOrEqual(3, $progressInfo); - } - - /** - * @covers FFMpeg\Helper\AudioProgressHelper::getPattern - */ - public function testAudioProgressHelper() - { - $audioProgress = new AudioProgressHelper(function($percent, $remaining, $rate) { }); - $audioProgress->setDuration(500); - - $line = "size= 712kB time=00:00:45.50 bitrate= 128.1kbits/s"; - $audioProgress->parseProgress($line); - - sleep(1); - - $line = "size= 4712kB time=00:01:45.50 bitrate= 128.1kbits/s"; - $progress = $audioProgress->parseProgress($line); - - $this->assertEquals('21.0', $progress['percent']); - } - - /** - * @covers FFMpeg\Helper\VideoProgressHelper::getPattern - */ - public function testVideoProgress() - { - $videoProgress = new VideoProgressHelper(function($percent, $remaining, $rate) {}); - $videoProgress->setDuration(500); - - $line = "frame= 206 fps=202 q=10.0 size= 571kB time=00:00:07.12 bitrate= 656.8kbits/s dup=9 drop=0"; - $videoProgress->parseProgress($line); - - sleep(1); - - $line = "frame= 854 fps=113 q=20.0 size= 4430kB time=00:00:33.04 bitrate=1098.5kbits/s dup=36 drop=0"; - $progress = $videoProgress->parseProgress($line); - - $this->assertEquals('6.0', $progress['percent']); - } -}