Merge branch 'master' into patch-1

This commit is contained in:
Jens Hausdorf 2019-02-26 06:44:47 +01:00 committed by GitHub
commit d48cda2861
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1014 additions and 306 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ composer.phar
composer.lock
phpunit.xml
sami.phar
.idea/

View file

@ -5,6 +5,12 @@ dist: trusty
branches:
only:
- master
- v1.x
cache:
directories:
- $HOME/.composer/cache
- $HOME/.cache
php:
- 5.4
@ -12,11 +18,10 @@ php:
- 5.6
- 7.0
- 7.1
- hhvm
- 7.2
- 7.3
matrix:
allow_failures:
- php: hhvm
include:
- php: 5.4
env: COMPOSER_FLAGS="--prefer-lowest"
@ -29,7 +34,7 @@ before_install:
install:
- sudo apt-get install -y ffmpeg
- composer update --prefer-source $COMPOSER_FLAGS
- composer update --prefer-dist $COMPOSER_FLAGS
script:
- vendor/bin/phpunit --verbose

113
README.md
View file

@ -1,10 +1,10 @@
# PHP FFmpeg
# php-ffmpeg
[![Build Status](https://secure.travis-ci.org/PHP-FFMpeg/PHP-FFMpeg.png?branch=master)](http://travis-ci.org/PHP-FFMpeg/PHP-FFMpeg)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/607f3111-e2d7-44e8-8bcc-54dd64521983/big.png)](https://insight.sensiolabs.com/projects/607f3111-e2d7-44e8-8bcc-54dd64521983)
An Object Oriented library to convert video/audio files with FFmpeg / AVConv.
An Object-Oriented library to convert video/audio files with FFmpeg / AVConv.
Check another amazing repo: [PHP FFMpeg extras](https://github.com/alchemy-fr/PHP-FFMpeg-Extras), you will find lots of Audio/Video formats there.
@ -16,7 +16,7 @@ This library requires a working FFMpeg install. You will need both FFMpeg and FF
Be sure that these binaries can be located with system PATH to get the benefit of the binary detection,
otherwise you should have to explicitly give the binaries path on load.
For Windows users : Please find the binaries at http://ffmpeg.zeranoe.com/builds/.
For Windows users: Please find the binaries at http://ffmpeg.zeranoe.com/builds/.
### Known issues:
@ -35,6 +35,9 @@ $ composer require php-ffmpeg/php-ffmpeg
## Basic Usage
```php
require 'vendor/autoload.php';
$ffmpeg = FFMpeg\FFMpeg::create();
$video = $ffmpeg->open('video.mpg');
$video
@ -125,7 +128,7 @@ $video->save($format, 'video.avi');
```
Transcoding progress can be monitored in realtime, see Format documentation
below for more informations.
below for more information.
##### Extracting image
@ -141,7 +144,7 @@ $frame = $video->frame(FFMpeg\Coordinate\TimeCode::fromSeconds(42));
$frame->save('image.jpg');
```
If you want to extract multiple images from your video, you can use the following filter:
If you want to extract multiple images from the video, you can use the following filter:
```php
$video
@ -152,6 +155,38 @@ $video
$video
->save(new FFMpeg\Format\Video\X264(), '/path/to/new/file');
```
By default, this will save the frames as `jpg` images.
You are able to override this using `setFrameFileType` to save the frames in another format:
```php
$frameFileType = 'jpg'; // either 'jpg', 'jpeg' or 'png'
$filter = new ExtractMultipleFramesFilter($frameRate, $destinationFolder);
$filter->setFrameFileType($frameFileType);
$video->addFilter($filter);
```
##### Clip
Cuts the video at a desired point. Use input seeking method. It is faster option than use filter clip.
```php
$clip = $video->clip(FFMpeg\Coordinate\TimeCode::fromSeconds(30), FFMpeg\Coordinate\TimeCode::fromSeconds(15));
$clip->save(new FFMpeg\Format\Video\X264(), 'video.avi');
```
The clip filter takes two parameters:
- `$start`, an instance of `FFMpeg\Coordinate\TimeCode`, specifies the start point of the clip
- `$duration`, optional, an instance of `FFMpeg\Coordinate\TimeCode`, specifies the duration of the clip
On clip you can apply same filters as on video. For example resizing filter.
```php
$clip = $video->clip(FFMpeg\Coordinate\TimeCode::fromSeconds(30), FFMpeg\Coordinate\TimeCode::fromSeconds(15));
$clip->filters()->resize(new FFMpeg\Coordinate\Dimension(320, 240), FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET, true);
$clip->save(new FFMpeg\Format\Video\X264(), 'video.avi');
```
##### Generate a waveform
@ -159,13 +194,13 @@ You can generate a waveform of an audio file using the `FFMpeg\Media\Audio::wave
method.
This code returns a `FFMpeg\Media\Waveform` instance.
You can optionally pass dimensions as arguments, see dedicated
You can optionally pass dimensions as the first two arguments and an array of hex string colors for ffmpeg to use for the waveform, see dedicated
documentation below for more information.
The ouput file MUST use the PNG extension.
The output file MUST use the PNG extension.
```php
$waveform = $audio->waveform(640, 120);
$waveform = $audio->waveform(640, 120, array('#00FF00'));
$waveform->save('waveform.png');
```
@ -178,8 +213,8 @@ $video = $ffmpeg->open( 'video.mp4' );
// Set an audio format
$audio_format = new FFMpeg\Format\Audio\Mp3();
// Extract the audio into a new file
$video->save('audio.mp3');
// Extract the audio into a new file as mp3
$video->save($audio_format, 'audio.mp3');
// Set the audio file
$audio = $ffmpeg->open( 'audio.mp3' );
@ -298,7 +333,7 @@ The framerate filter takes two parameters:
Synchronizes audio and video.
Some containers may use a delay that results in desynchronized outputs. This
filters solves this issue.
filter solves this issue.
```php
$video->filters()->synchronize();
@ -317,6 +352,18 @@ The clip filter takes two parameters:
- `$start`, an instance of `FFMpeg\Coordinate\TimeCode`, specifies the start point of the clip
- `$duration`, optional, an instance of `FFMpeg\Coordinate\TimeCode`, specifies the duration of the clip
###### Crop
Crops the video based on a width and height(a `Point`)
```php
$video->filters()->crop(new FFMpeg\Coordinate\Point("t*100", 0, true), new FFMpeg\Coordinate\Dimension(200, 600));
```
It takes two parameters:
- `$point`, an instance of `FFMpeg\Coordinate\Point`, specifies the point to crop
- `$dimension`, an instance of `FFMpeg\Coordinate\Dimension`, specifies the dimension of the output video
### Audio
`FFMpeg\Media\Audio` can be transcoded too, ie: change codec, isolate audio or
@ -346,7 +393,7 @@ $audio->save($format, 'track.flac');
```
Transcoding progress can be monitored in realtime, see Format documentation
below for more informations.
below for more information.
##### Filters
@ -399,7 +446,7 @@ The resample filter takes two parameters :
#### Frame
A frame is a image at a timecode of a video ; see documentation above about
A frame is an image at a timecode of a video; see documentation above about
frame extraction.
You can save frames using the `FFMpeg\Media\Frame::save` method.
@ -409,7 +456,7 @@ $frame->save('target.jpg');
```
This method has a second optional boolean parameter. Set it to true to get
accurate images ; it takes more time to execute.
accurate images; it takes more time to execute.
#### Gif
@ -447,7 +494,7 @@ To concatenate videos encoded with the same codec, do as follow:
```php
// In order to instantiate the video object, you HAVE TO pass a path to a valid video file.
// We recommand that you put there the path of any of the video you want to use in this concatenation.
// We recommend that you put there the path of any of the video you want to use in this concatenation.
$video = $ffmpeg->open( '/path/to/video' );
$video
->concat(array('/path/to/video1', '/path/to/video2'))
@ -460,7 +507,7 @@ To concatenate videos encoded with the same codec, do as follow:
```php
// In order to instantiate the video object, you HAVE TO pass a path to a valid video file.
// We recommand that you put there the path of any of the video you want to use in this concatenation.
// We recommend that you put there the path of any of the video you want to use in this concatenation.
$video = $ffmpeg->open( '/path/to/video' );
$format = new FFMpeg\Format\Video\X264();
@ -479,10 +526,10 @@ A format implements `FFMpeg\Format\FormatInterface`. To save to a video file,
use `FFMpeg\Format\VideoInterface`, and `FFMpeg\Format\AudioInterface` for
audio files.
Format can also extends `FFMpeg\Format\ProgressableInterface` to get realtime
informations about the transcoding.
A format can also extend `FFMpeg\Format\ProgressableInterface` to get realtime
information about the transcoding.
Predefined formats already provide progress informations as events.
Predefined formats already provide progress information as events.
```php
$format = new FFMpeg\Format\Video\X264();
@ -542,12 +589,12 @@ class CustomWMVFormat extends FFMpeg\Format\Video\DefaultVideo
#### Coordinates
FFMpeg use many units for time and space coordinates.
FFMpeg uses many units for time and space coordinates.
- `FFMpeg\Coordinate\AspectRatio` represents an aspect ratio.
- `FFMpeg\Coordinate\Dimension` represent a dimension.
- `FFMpeg\Coordinate\FrameRate` represent a framerate.
- `FFMpeg\Coordinate\Point` represent a point.
- `FFMpeg\Coordinate\Point` represent a point. (Supports dynamic points since v0.10.0)
- `FFMpeg\Coordinate\TimeCode` represent a timecode.
### FFProbe
@ -571,9 +618,19 @@ $ffprobe
->get('duration'); // returns the duration property
```
### Validating media files
(since 0.10.0)
You can validate media files using PHP-FFMpeg's FFProbe wrapper.
```php
$ffprobe = FFMpeg\FFProbe::create();
$ffprobe->isValid('/path/to/file/to/check'); // returns bool
```
## Using with Silex Microframework
Service provider is easy to set up:
The service provider is easy to set up:
```php
$app = new Silex\Application();
@ -597,10 +654,14 @@ $app->register(new FFMpeg\FFMpegServiceProvider(), array(
));
```
## API Browser
Browse the [API](https://ffmpeg-php.readthedocs.io/en/latest/_static/API/)
## License
This project is licensed under the [MIT license](http://opensource.org/licenses/MIT).
Music: "Favorite Secrets" by Waylon Thornton
From the Free Music Archive
[CC BY NC SA](http://creativecommons.org/licenses/by-nc-sa/3.0/us/)
Music: "Siesta" by Jahzzar
From the Free Music Archive
[CC BY SA](https://creativecommons.org/licenses/by-sa/3.0/)

View file

@ -24,11 +24,16 @@
"name": "Romain Biard",
"email": "romain.biard@gmail.com",
"homepage": "https://www.strime.io/"
},
{
"name": "Jens Hausdorf",
"email": "hello@jens-hausdorf.de",
"homepage": "https://jens-hausdorf.de"
}
],
"require": {
"php": "^5.3.9 || ^7.0",
"alchemy/binary-driver": "^1.5",
"alchemy/binary-driver": "^1.5 || ~2.0.0 || ^5.0",
"doctrine/cache": "^1.0",
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"neutron/temporary-filesystem": "^2.1.1"
@ -39,7 +44,7 @@
"require-dev": {
"sami/sami": "~1.0",
"silex/silex": "~1.0",
"phpunit/phpunit": "^4.8"
"phpunit/phpunit": "^4.8.36"
},
"autoload": {
"psr-0": {

View file

@ -20,6 +20,12 @@ class AspectRatio
const AR_4_3 = '4/3';
// named 16:9 or 1.77:1 HD video standard
const AR_16_9 = '16/9';
// named 8:5 or 16:10 or 1.6:1
const AR_8_5 = '8/5';
// named 25:16 or 1.56:1
const AR_25_16 = '25/16';
// named 3:2 or 1.5:1 see http://en.wikipedia.org/wiki/135_film
const AR_3_2 = '3/2';
@ -160,6 +166,10 @@ class AspectRatio
return 4 / 3;
case static::AR_16_9:
return 16 / 9;
case static::AR_8_5:
return 8 / 5;
case static::AR_25_16:
return 25 / 16;
case static::AR_1_1:
return 1 / 1;
case static::AR_1_DOT_85_1:
@ -207,6 +217,8 @@ class AspectRatio
$availables = array(
static::AR_4_3 => static::valueFromName(static::AR_4_3),
static::AR_16_9 => static::valueFromName(static::AR_16_9),
static::AR_8_5 => static::valueFromName(static::AR_8_5),
static::AR_25_16 => static::valueFromName(static::AR_25_16),
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),

View file

@ -16,10 +16,15 @@ class Point
private $x;
private $y;
public function __construct($x, $y)
public function __construct($x, $y, $dynamic = false)
{
$this->x = (int) $x;
$this->y = (int) $y;
if ($dynamic) {
$this->x = $x;
$this->y = $y;
} else {
$this->x = (int)$x;
$this->y = (int)$y;
}
}
/**

View file

@ -170,6 +170,25 @@ class FFProbe
return $this->probe($pathfile, '-show_format', static::TYPE_FORMAT);
}
/**
* @api
*
* Checks wether the given `$pathfile` is considered a valid media file.
*
* @param string $pathfile
* @return bool
* @since 0.10.0
*/
public function isValid($pathfile)
{
try {
return $this->format($pathfile)->get('duration') > 0;
} catch(\Exception $e) {
// complete invalid data
return false;
}
}
/**
* @api
*

View file

@ -71,4 +71,18 @@ class AudioFilters
return $this;
}
/**
* Applies a custom filter
*
* @param string $parameters
*
* @return AudioFilters
*/
public function custom($parameters)
{
$this->media->addFilter(new CustomFilter($parameters));
return $this;
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <dev.team@alchemy.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Audio;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Media\Audio;
class CustomFilter implements AudioFilterInterface
{
/** @var string */
private $filter;
/** @var integer */
private $priority;
/**
* A custom filter, useful if you want to build complex filters
*
* @param string $filter
* @param int $priority
*/
public function __construct($filter, $priority = 0)
{
$this->filter = $filter;
$this->priority = $priority;
}
/**
* {@inheritdoc}
*/
public function getPriority()
{
return $this->priority;
}
/**
* {@inheritdoc}
*/
public function apply(Audio $audio, AudioInterface $format)
{
$commands = array('-af', $this->filter);
return $commands;
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <dev.team@alchemy.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Frame;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Media\Frame;
class CustomFrameFilter implements FrameFilterInterface
{
/** @var string */
private $filter;
/** @var integer */
private $priority;
/**
* A custom filter, useful if you want to build complex filters
*
* @param string $filter
* @param int $priority
*/
public function __construct($filter, $priority = 0)
{
$this->filter = $filter;
$this->priority = $priority;
}
/**
* {@inheritdoc}
*/
public function getPriority()
{
return $this->priority;
}
/**
* {@inheritdoc}
*/
public function apply(Frame $frame)
{
$commands = array('-vf', $this->filter);
return $commands;
}
}

View file

@ -36,4 +36,18 @@ class FrameFilters
return $this;
}
/**
* Applies a custom filter: -vf foo bar
*
* @param string $parameters
*
* @return FrameFilters
*/
public function custom($parameters)
{
$this->frame->addFilter(new CustomFrameFilter($parameters));
return $this;
}
}

View file

@ -35,6 +35,10 @@ class ExtractMultipleFramesFilter implements VideoFilterInterface
private $priority;
private $frameRate;
private $destinationFolder;
private $frameFileType = 'jpg';
/** @var array */
private static $supportedFrameFileTypes = ['jpg', 'jpeg', 'png'];
public function __construct($frameRate = self::FRAMERATE_EVERY_SEC, $destinationFolder = __DIR__, $priority = 0)
{
@ -49,6 +53,20 @@ class ExtractMultipleFramesFilter implements VideoFilterInterface
$this->destinationFolder = $destinationFolder;
}
/**
* @param string $frameFileType
* @throws \FFMpeg\Exception\InvalidArgumentException
* @return ExtractMultipleFramesFilter
*/
public function setFrameFileType($frameFileType) {
if (in_array($frameFileType, self::$supportedFrameFileTypes)) {
$this->frameFileType = $frameFileType;
return $this;
}
throw new InvalidArgumentException('Invalid frame file type, use: ' . implode(',', self::$supportedFrameFileTypes));
}
/**
* {@inheritdoc}
*/
@ -117,7 +135,7 @@ class ExtractMultipleFramesFilter implements VideoFilterInterface
// Set the parameters
$commands[] = '-vf';
$commands[] = 'fps=' . $this->frameRate;
$commands[] = $this->destinationFolder . 'frame-%'.$nbDigitsInFileNames.'d.jpg';
$commands[] = $this->destinationFolder . 'frame-%'.$nbDigitsInFileNames.'d.' . $this->frameFileType;
}
catch (RuntimeException $e) {
throw new RuntimeException('An error occured while extracting the frames: ' . $e->getMessage() . '. The code: ' . $e->getCode());

View file

@ -121,10 +121,10 @@ abstract class DefaultAudio extends EventEmitter implements AudioInterface, Prog
/**
* {@inheritdoc}
*/
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total)
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total, $duration = 0)
{
$format = $this;
$listener = new AudioProgressListener($ffprobe, $media->getPathfile(), $pass, $total);
$listener = new AudioProgressListener($ffprobe, $media->getPathfile(), $pass, $total, $duration);
$listener->on('progress', function () use ($media, $format) {
$format->emit('progress', array_merge(array($media, $format), func_get_args()));
});

View file

@ -80,12 +80,13 @@ abstract class AbstractProgressListener extends EventEmitter implements Listener
*
* @throws RuntimeException
*/
public function __construct(FFProbe $ffprobe, $pathfile, $currentPass, $totalPass)
public function __construct(FFProbe $ffprobe, $pathfile, $currentPass, $totalPass, $duration = 0)
{
$this->ffprobe = $ffprobe;
$this->pathfile = $pathfile;
$this->currentPass = $currentPass;
$this->totalPass = $totalPass;
$this->duration = $duration;
}
/**
@ -254,9 +255,8 @@ abstract class AbstractProgressListener extends EventEmitter implements Listener
return;
}
$this->totalSize = $format->get('size') / 1024;
$this->duration = $format->get('duration');
$this->duration = (int) $this->duration > 0 ? $this->duration : $format->get('duration');
$this->totalSize = $format->get('size') / 1024 * ($this->duration / $format->get('duration'));
$this->initialized = true;
}
}

View file

@ -24,8 +24,9 @@ interface ProgressableInterface extends EventEmitterInterface
* @param FFProbe $ffprobe
* @param Integer $pass The current pas snumber
* @param Integer $total The total pass number
* @param Integer $duration The new video duration
*
* @return array An array of listeners
*/
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total);
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total, $duration = 0);
}

View file

@ -125,10 +125,10 @@ abstract class DefaultVideo extends DefaultAudio implements VideoInterface
/**
* {@inheritdoc}
*/
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total)
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total, $duration = 0)
{
$format = $this;
$listeners = array(new VideoProgressListener($ffprobe, $media->getPathfile(), $pass, $total));
$listeners = array(new VideoProgressListener($ffprobe, $media->getPathfile(), $pass, $total, $duration));
foreach ($listeners as $listener) {
$listener->on('progress', function () use ($format, $media) {

View file

@ -52,6 +52,6 @@ class WebM extends DefaultVideo
*/
public function getAvailableVideoCodecs()
{
return array('libvpx');
return array('libvpx', 'libvpx-vp9');
}
}

View file

@ -0,0 +1,292 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Alchemy <info@alchemy.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Media;
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
use FFMpeg\Filters\Audio\SimpleFilter;
use FFMpeg\Exception\InvalidArgumentException;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Filters\Video\VideoFilters;
use FFMpeg\Filters\FilterInterface;
use FFMpeg\Format\FormatInterface;
use FFMpeg\Format\ProgressableInterface;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Format\VideoInterface;
use Neutron\TemporaryFilesystem\Manager as FsManager;
use FFMpeg\Filters\Video\ClipFilter;
abstract class AbstractVideo extends Audio
{
/**
* FileSystem Manager instance
* @var Manager
*/
protected $fs;
/**
* FileSystem Manager ID
* @var int
*/
protected $fsId;
/**
* @inheritDoc
* @return VideoFilters
*/
public function filters()
{
return new VideoFilters($this);
}
/**
* @inheritDoc
* @return Video
*/
public function addFilter(FilterInterface $filter)
{
$this->filters->add($filter);
return $this;
}
/**
* Exports the video in the desired format, applies registered filters.
*
* @param FormatInterface $format
* @param string $outputPathfile
* @return Video
* @throws RuntimeException
*/
public function save(FormatInterface $format, $outputPathfile)
{
$passes = $this->buildCommand($format, $outputPathfile);
$failure = null;
$totalPasses = $format->getPasses();
foreach ($passes as $pass => $passCommands) {
try {
/** add listeners here */
$listeners = null;
if ($format instanceof ProgressableInterface) {
$filters = clone $this->filters;
$duration = 0;
// check the filters of the video, and if the video has the ClipFilter then
// take the new video duration and send to the
// FFMpeg\Format\ProgressListener\AbstractProgressListener class
foreach ($filters as $filter) {
if ($filter instanceof ClipFilter) {
$duration = $filter->getDuration()->toSeconds();
break;
}
}
$listeners = $format->createProgressListener($this, $this->ffprobe, $pass + 1, $totalPasses, $duration);
}
$this->driver->command($passCommands, false, $listeners);
} catch (ExecutionFailureException $e) {
$failure = $e;
break;
}
}
$this->fs->clean($this->fsId);
if (null !== $failure) {
throw new RuntimeException('Encoding failed', $failure->getCode(), $failure);
}
return $this;
}
/**
* NOTE: This method is different to the Audio's one, because Video is using passes.
* @inheritDoc
*/
public function getFinalCommand(FormatInterface $format, $outputPathfile)
{
$finalCommands = array();
foreach ($this->buildCommand($format, $outputPathfile) as $pass => $passCommands) {
$finalCommands[] = implode(' ', $passCommands);
}
$this->fs->clean($this->fsId);
return $finalCommands;
}
/**
* **NOTE:** This creates passes instead of a single command!
*
* @inheritDoc
* @return string[][]
*/
protected function buildCommand(FormatInterface $format, $outputPathfile)
{
$commands = $this->basePartOfCommand();
$filters = clone $this->filters;
$filters->add(new SimpleFilter($format->getExtraParams(), 10));
if ($this->driver->getConfiguration()->has('ffmpeg.threads')) {
$filters->add(new SimpleFilter(array('-threads', $this->driver->getConfiguration()->get('ffmpeg.threads'))));
}
if ($format instanceof VideoInterface) {
if (null !== $format->getVideoCodec()) {
$filters->add(new SimpleFilter(array('-vcodec', $format->getVideoCodec())));
}
}
if ($format instanceof AudioInterface) {
if (null !== $format->getAudioCodec()) {
$filters->add(new SimpleFilter(array('-acodec', $format->getAudioCodec())));
}
}
foreach ($filters as $filter) {
$commands = array_merge($commands, $filter->apply($this, $format));
}
if ($format instanceof VideoInterface) {
$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';
}
if ($format instanceof AudioInterface) {
if (null !== $format->getAudioKiloBitrate()) {
$commands[] = '-b:a';
$commands[] = $format->getAudioKiloBitrate() . 'k';
}
if (null !== $format->getAudioChannels()) {
$commands[] = '-ac';
$commands[] = $format->getAudioChannels();
}
}
// If the user passed some additional parameters
if ($format instanceof VideoInterface) {
if (null !== $format->getAdditionalParameters()) {
foreach ($format->getAdditionalParameters() as $additionalParameter) {
$commands[] = $additionalParameter;
}
}
}
// Merge Filters into one command
$videoFilterVars = $videoFilterProcesses = array();
for ($i = 0; $i < count($commands); $i++) {
$command = $commands[$i];
if ($command === '-vf') {
$commandSplits = explode(";", $commands[$i + 1]);
if (count($commandSplits) == 1) {
$commandSplit = $commandSplits[0];
$command = trim($commandSplit);
if (preg_match("/^\[in\](.*?)\[out\]$/is", $command, $match)) {
$videoFilterProcesses[] = $match[1];
} else {
$videoFilterProcesses[] = $command;
}
} else {
foreach ($commandSplits as $commandSplit) {
$command = trim($commandSplit);
if (preg_match("/^\[[^\]]+\](.*?)\[[^\]]+\]$/is", $command, $match)) {
$videoFilterProcesses[] = $match[1];
} else {
$videoFilterVars[] = $command;
}
}
}
unset($commands[$i]);
unset($commands[$i + 1]);
$i++;
}
}
$videoFilterCommands = $videoFilterVars;
$lastInput = 'in';
foreach ($videoFilterProcesses as $i => $process) {
$command = '[' . $lastInput . ']';
$command .= $process;
$lastInput = 'p' . $i;
if ($i === (count($videoFilterProcesses) - 1)) {
$command .= '[out]';
} else {
$command .= '[' . $lastInput . ']';
}
$videoFilterCommands[] = $command;
}
$videoFilterCommand = implode(';', $videoFilterCommands);
if ($videoFilterCommand) {
$commands[] = '-vf';
$commands[] = $videoFilterCommand;
}
$this->fs = FsManager::create();
$this->fsId = uniqid('ffmpeg-passes');
$passPrefix = $this->fs->createTemporaryDirectory(0777, 50, $this->fsId) . '/' . uniqid('pass-');
$passes = array();
$totalPasses = $format->getPasses();
if (!$totalPasses) {
throw new InvalidArgumentException('Pass number should be a positive value.');
}
for ($i = 1; $i <= $totalPasses; $i++) {
$pass = $commands;
if ($totalPasses > 1) {
$pass[] = '-pass';
$pass[] = $i;
$pass[] = '-passlogfile';
$pass[] = $passPrefix;
}
$pass[] = $outputPathfile;
$passes[] = $pass;
}
return $passes;
}
/**
* Return base part of command.
*
* @return array
*/
protected function basePartOfCommand()
{
return array('-y', '-i', $this->pathfile);
}
}

View file

@ -52,11 +52,9 @@ class Audio extends AbstractStreamableMedia
/**
* Exports the audio in the desired format, applies registered filters.
*
* @param FormatInterface $format
* @param string $outputPathfile
*
* @param FormatInterface $format
* @param string $outputPathfile
* @return Audio
*
* @throws RuntimeException
*/
public function save(FormatInterface $format, $outputPathfile)
@ -64,9 +62,42 @@ class Audio extends AbstractStreamableMedia
$listeners = null;
if ($format instanceof ProgressableInterface) {
$listeners = $format->createProgressListener($this, $this->ffprobe, 1, 1);
$listeners = $format->createProgressListener($this, $this->ffprobe, 1, 1, 0);
}
$commands = $this->buildCommand($format, $outputPathfile);
try {
$this->driver->command($commands, false, $listeners);
} catch (ExecutionFailureException $e) {
$this->cleanupTemporaryFile($outputPathfile);
throw new RuntimeException('Encoding failed', $e->getCode(), $e);
}
return $this;
}
/**
* Returns the final command as a string, useful for debugging purposes.
*
* @param FormatInterface $format
* @param string $outputPathfile
* @return string
* @since 0.11.0
*/
public function getFinalCommand(FormatInterface $format, $outputPathfile) {
return implode(' ', $this->buildCommand($format, $outputPathfile));
}
/**
* Builds the command which will be executed with the provided format
*
* @param FormatInterface $format
* @param string $outputPathfile
* @return string[] An array which are the components of the command
* @since 0.11.0
*/
protected function buildCommand(FormatInterface $format, $outputPathfile) {
$commands = array('-y', '-i', $this->pathfile);
$filters = clone $this->filters;
@ -93,14 +124,7 @@ class Audio extends AbstractStreamableMedia
}
$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;
return $commands;
}
/**
@ -108,10 +132,22 @@ class Audio extends AbstractStreamableMedia
*
* @param integer $width
* @param integer $height
* @param array $colors Array of colors for ffmpeg to use. Color format is #000000 (RGB hex string with #)
* @return Waveform
*/
public function waveform($width = 640, $height = 120)
public function waveform($width = 640, $height = 120, $colors = array(Waveform::DEFAULT_COLOR))
{
return new Waveform($this, $this->driver, $this->ffprobe, $width, $height);
return new Waveform($this, $this->driver, $this->ffprobe, $width, $height, $colors);
}
/**
* Concatenates a list of audio files into one unique audio file.
*
* @param array $sources
* @return Concat
*/
public function concat($sources)
{
return new Concat($sources, $this->driver, $this->ffprobe);
}
}

60
src/FFMpeg/Media/Clip.php Normal file
View file

@ -0,0 +1,60 @@
<?php
namespace FFMpeg\Media;
use FFMpeg\Driver\FFMpegDriver;
use FFMpeg\FFProbe;
use FFMpeg\Coordinate\TimeCode;
/**
* Video clip.
*
* Use input seeking, see http://trac.ffmpeg.org/wiki/Seeking
*/
class Clip extends Video
{
/** @var TimeCode Start time */
private $start;
/** @var TimeCode Duration */
private $duration;
/** @var Video Parrent video */
private $video;
public function __construct(Video $video, FFMpegDriver $driver, FFProbe $ffprobe, TimeCode $start, TimeCode $duration = null)
{
$this->start = $start;
$this->duration = $duration;
$this->video = $video;
parent::__construct($video->getPathfile(), $driver, $ffprobe);
}
/**
* Returns the video related to the frame.
*
* @return Video
*/
public function getVideo()
{
return $this->video;
}
/**
* Return base part of command.
*
* @return array
*/
protected function basePartOfCommand()
{
$arr = array('-y', '-ss', (string) $this->start, '-i', $this->pathfile);
if (is_null($this->duration) === false) {
$arr[] = '-t';
$arr[] = (string) $this->duration;
}
return $arr;
}
}

View file

@ -72,8 +72,8 @@ class Concat extends AbstractMediaType
/**
* Saves the concatenated video in the given array, considering that the sources videos are all encoded with the same codec.
*
* @param array $outputPathfile
* @param string $streamCopy
* @param string $outputPathfile
* @param bool $streamCopy
*
* @return Concat
*
@ -105,7 +105,7 @@ class Concat extends AbstractMediaType
if($count_videos != 0)
$line .= "\n";
$line .= "file ".$videoPath;
$line .= "file " . addcslashes($videoPath, '\'"\\\0 ');
fwrite($fileStream, $line);
@ -144,6 +144,7 @@ class Concat extends AbstractMediaType
$this->driver->command($commands);
} catch (ExecutionFailureException $e) {
$this->cleanupTemporaryFile($outputPathfile);
// TODO@v1: paste this line into an `finally` block.
$this->cleanupTemporaryFile($sourcesFile);
throw new RuntimeException('Unable to save concatenated video', $e->getCode(), $e);
}

View file

@ -115,7 +115,9 @@ class Frame extends AbstractMediaType
$commands = array_merge($commands, $filter->apply($this));
}
$commands = array_merge($commands, array($pathfile));
if (!$returnBase64) {
$commands = array_merge($commands, array($pathfile));
}
try {
if(!$returnBase64) {

View file

@ -1,5 +1,4 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
@ -8,228 +7,13 @@
* 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\Coordinate\Dimension;
use FFMpeg\Filters\Audio\SimpleFilter;
use FFMpeg\Exception\InvalidArgumentException;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Filters\Video\VideoFilters;
use FFMpeg\Filters\FilterInterface;
use FFMpeg\Format\FormatInterface;
use FFMpeg\Format\ProgressableInterface;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Format\VideoInterface;
use Neutron\TemporaryFilesystem\Manager as FsManager;
class Video extends Audio
class Video extends AbstractVideo
{
/**
* {@inheritdoc}
*
* @return VideoFilters
*/
public function filters()
{
return new VideoFilters($this);
}
/**
* {@inheritdoc}
*
* @return Video
*/
public function addFilter(FilterInterface $filter)
{
$this->filters->add($filter);
return $this;
}
/**
* Exports the video in the desired format, applies registered filters.
*
* @param FormatInterface $format
* @param string $outputPathfile
*
* @return Video
*
* @throws RuntimeException
*/
public function save(FormatInterface $format, $outputPathfile)
{
$commands = array('-y', '-i', $this->pathfile);
$filters = clone $this->filters;
$filters->add(new SimpleFilter($format->getExtraParams(), 10));
if ($this->driver->getConfiguration()->has('ffmpeg.threads')) {
$filters->add(new SimpleFilter(array('-threads', $this->driver->getConfiguration()->get('ffmpeg.threads'))));
}
if ($format instanceof VideoInterface) {
if (null !== $format->getVideoCodec()) {
$filters->add(new SimpleFilter(array('-vcodec', $format->getVideoCodec())));
}
}
if ($format instanceof AudioInterface) {
if (null !== $format->getAudioCodec()) {
$filters->add(new SimpleFilter(array('-acodec', $format->getAudioCodec())));
}
}
foreach ($filters as $filter) {
$commands = array_merge($commands, $filter->apply($this, $format));
}
if ($format instanceof VideoInterface) {
$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';
}
if ($format instanceof AudioInterface) {
if (null !== $format->getAudioKiloBitrate()) {
$commands[] = '-b:a';
$commands[] = $format->getAudioKiloBitrate() . 'k';
}
if (null !== $format->getAudioChannels()) {
$commands[] = '-ac';
$commands[] = $format->getAudioChannels();
}
}
// If the user passed some additional parameters
if ($format instanceof VideoInterface) {
if (null !== $format->getAdditionalParameters()) {
foreach ($format->getAdditionalParameters() as $additionalParameter) {
$commands[] = $additionalParameter;
}
}
}
// Merge Filters into one command
$videoFilterVars = $videoFilterProcesses = [];
for($i=0;$i<count($commands);$i++) {
$command = $commands[$i];
if ( $command == '-vf' ) {
$commandSplits = explode(";", $commands[$i + 1]);
if ( count($commandSplits) == 1 ) {
$commandSplit = $commandSplits[0];
$command = trim($commandSplit);
if ( preg_match("/^\[in\](.*?)\[out\]$/is", $command, $match) ) {
$videoFilterProcesses[] = $match[1];
} else {
$videoFilterProcesses[] = $command;
}
} else {
foreach($commandSplits as $commandSplit) {
$command = trim($commandSplit);
if ( preg_match("/^\[[^\]]+\](.*?)\[[^\]]+\]$/is", $command, $match) ) {
$videoFilterProcesses[] = $match[1];
} else {
$videoFilterVars[] = $command;
}
}
}
unset($commands[$i]);
unset($commands[$i + 1]);
$i++;
}
}
$videoFilterCommands = $videoFilterVars;
$lastInput = 'in';
foreach($videoFilterProcesses as $i => $process) {
$command = '[' . $lastInput .']';
$command .= $process;
$lastInput = 'p' . $i;
if ( $i == count($videoFilterProcesses) - 1 ) {
$command .= '[out]';
} else {
$command .= '[' . $lastInput . ']';
}
$videoFilterCommands[] = $command;
}
$videoFilterCommand = implode(";", $videoFilterCommands);
if ( $videoFilterCommand ) {
$commands[] = '-vf';
$commands[] = $videoFilterCommand;
}
$fs = FsManager::create();
$fsId = uniqid('ffmpeg-passes');
$passPrefix = $fs->createTemporaryDirectory(0777, 50, $fsId) . '/' . uniqid('pass-');
$passes = array();
$totalPasses = $format->getPasses();
if (1 > $totalPasses) {
throw new InvalidArgumentException('Pass number should be a positive value.');
}
for ($i = 1; $i <= $totalPasses; $i++) {
$pass = $commands;
if ($totalPasses > 1) {
$pass[] = '-pass';
$pass[] = $i;
$pass[] = '-passlogfile';
$pass[] = $passPrefix;
}
$pass[] = $outputPathfile;
$passes[] = $pass;
}
$failure = null;
foreach ($passes as $pass => $passCommands) {
try {
/** add listeners here */
$listeners = null;
if ($format instanceof ProgressableInterface) {
$listeners = $format->createProgressListener($this, $this->ffprobe, $pass + 1, $totalPasses);
}
$this->driver->command($passCommands, false, $listeners);
} catch (ExecutionFailureException $e) {
$failure = $e;
break;
}
}
$fs->clean($fsId);
if (null !== $failure) {
throw new RuntimeException('Encoding failed', $failure->getCode(), $failure);
}
return $this;
}
/**
* Gets the frame at timecode.
@ -265,4 +49,16 @@ class Video extends Audio
{
return new Concat($sources, $this->driver, $this->ffprobe);
}
/**
* Clips the video at the given time(s).
*
* @param TimeCode $start Start time
* @param TimeCode $duration Duration
* @return \FFMpeg\Media\Clip
*/
public function clip(TimeCode $start, TimeCode $duration = null)
{
return new Clip($this, $this->driver, $this->ffprobe, $start, $duration);
}
}

View file

@ -12,6 +12,7 @@
namespace FFMpeg\Media;
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
use FFMpeg\Exception\InvalidArgumentException;
use FFMpeg\Filters\Waveform\WaveformFilterInterface;
use FFMpeg\Filters\Waveform\WaveformFilters;
use FFMpeg\Driver\FFMpegDriver;
@ -20,17 +21,26 @@ use FFMpeg\Exception\RuntimeException;
class Waveform extends AbstractMediaType
{
/** @var Video */
private $audio;
private $width;
private $height;
const DEFAULT_COLOR = '#000000';
public function __construct(Audio $audio, FFMpegDriver $driver, FFProbe $ffprobe, $width, $height)
/** @var Video */
protected $audio;
protected $width;
protected $height;
/**
* @var array
*/
protected $colors;
public function __construct(Audio $audio, FFMpegDriver $driver, FFProbe $ffprobe, $width, $height, $colors = array(self::DEFAULT_COLOR))
{
parent::__construct($audio->getPathfile(), $driver, $ffprobe);
$this->audio = $audio;
$this->width = $width;
$this->height = $height;
$this->setColors($colors);
}
/**
@ -65,6 +75,55 @@ class Waveform extends AbstractMediaType
return $this;
}
/**
* Parameter should be an array containing at least one valid color represented as a HTML color string. For
* example #FFFFFF or #000000. By default the color is set to black. Keep in mind that if you save the waveform
* as jpg file, it will appear completely black and to avoid this you can set the waveform color to white (#FFFFFF).
* Saving waveforms to png is strongly suggested.
*
* @param array $colors
*/
public function setColors(array $colors)
{
foreach ($colors as $row => $value)
{
if (!preg_match('/^#(?:[0-9a-fA-F]{6})$/', $value))
{
//invalid color
//unset($colors[$row]);
throw new InvalidArgumentException("The provided color '$value' is invalid");
}
}
if (count($colors))
{
$this->colors = $colors;
}
}
/**
* Returns an array of colors that will be passed to ffmpeg to use for waveform generation. Colors are applied ONLY
* to the waveform. Background cannot be controlled that easily and it is probably easier to save the waveform
* as a transparent png file and then add background of choice.
*
* @return array
*/
public function getColors()
{
return $this->colors;
}
/**
* Compiles the selected colors into a string, using a pipe separator.
*
* @return string
*/
protected function compileColors()
{
return implode('|', $this->colors);
}
/**
* Saves the waveform in the given filename.
*
@ -81,8 +140,8 @@ class Waveform extends AbstractMediaType
* @see http://ffmpeg.org/ffmpeg.html#Main-options
*/
$commands = array(
'-i', $this->pathfile, '-filter_complex',
'showwavespic=s='.$this->width.'x'.$this->height,
'-y', '-i', $this->pathfile, '-filter_complex',
'showwavespic=colors='.$this->compileColors().':s='.$this->width.'x'.$this->height,
'-frames:v', '1'
);

View file

@ -0,0 +1,31 @@
<?php
namespace Tests\FFMpeg\Functional;
use FFMpeg\Format\Audio\Mp3;
use FFMpeg\Media\Audio;
class AudioConcatenationTest extends FunctionalTestCase
{
public function testSimpleAudioFileConcatTest()
{
$ffmpeg = $this->getFFMpeg();
$files = [
__DIR__ . '/../files/Jahzzar_-_05_-_Siesta.mp3',
__DIR__ . '/../files/02_-_Favorite_Secrets.mp3',
];
$audio = $ffmpeg->open(reset($files));
$this->assertInstanceOf('FFMpeg\Media\Audio', $audio);
clearstatcache();
$filename = __DIR__ . '/output/concat-output.mp3';
$audio->concat($files)->saveFromSameCodecs($filename, TRUE);
$this->assertFileExists($filename);
unlink($filename);
}
}

View file

@ -12,10 +12,23 @@ class FFProbeTest extends FunctionalTestCase
$this->assertGreaterThan(0, count($ffprobe->streams(__DIR__ . '/../files/Audio.mp3')));
}
public function testValidateExistingFile()
{
$ffprobe = FFProbe::create();
$this->assertTrue($ffprobe->isValid(__DIR__ . '/../files/sample.3gp'));
}
public function testValidateNonExistingFile()
{
$ffprobe = FFProbe::create();
$this->assertFalse($ffprobe->isValid(__DIR__ . '/../files/WrongFile.mp4'));
}
/**
* @expectedException FFMpeg\Exception\RuntimeException
*/
public function testProbeOnUnexistantFile()
public function testProbeOnNonExistantFile()
{
$ffprobe = FFProbe::create();
$ffprobe->streams('/path/to/no/file');

View file

@ -3,8 +3,9 @@
namespace Tests\FFMpeg\Functional;
use FFMpeg\FFMpeg;
use PHPUnit\Framework\TestCase;
abstract class FunctionalTestCase extends \PHPUnit_Framework_TestCase
abstract class FunctionalTestCase extends TestCase
{
/**
* @return FFMpeg

View file

@ -13,4 +13,11 @@ class PointTest extends TestCase
$this->assertEquals(4, $point->getX());
$this->assertEquals(25, $point->getY());
}
public function testDynamicPointGetters()
{
$point = new Point("t*100", "t", true);
$this->assertEquals("t*100", $point->getX());
$this->assertEquals("t", $point->getY());
}
}

View file

@ -4,8 +4,9 @@ namespace Tests\FFMpeg\Unit;
use FFMpeg\FFMpegServiceProvider;
use Silex\Application;
use PHPUnit\Framework\TestCase as BaseTestCase;
class FFMpegServiceProviderTest extends \PHPUnit_Framework_TestCase
class FFMpegServiceProviderTest extends BaseTestCase
{
public function testWithConfig()
{

View file

@ -0,0 +1,20 @@
<?php
namespace Tests\FFMpeg\Unit\Filters\Audio;
use FFMpeg\Filters\Audio\CustomFilter;
use FFMpeg\Filters\Audio\FrameRateFilter;
use Tests\FFMpeg\Unit\TestCase;
use FFMpeg\Coordinate\FrameRate;
class CustomFilterTest extends TestCase
{
public function testApplyCustomFilter()
{
$audio = $this->getAudioMock();
$format = $this->getMock('FFMpeg\Format\AudioInterface');
$filter = new CustomFilter('whatever i put would end up as a filter');
$this->assertEquals(array('-af', 'whatever i put would end up as a filter'), $filter->apply($audio, $format));
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Tests\FFMpeg\Unit\Filters\Frame;
use FFMpeg\Filters\Frame\CustomFrameFilter;
use Tests\FFMpeg\Unit\TestCase;
class CustomFrameFilterTest extends TestCase
{
public function testApplyCustomFrameFilter()
{
$frame = $this->getFrameMock();
$filter = new CustomFrameFilter('whatever i put would end up as a filter');
$this->assertEquals(array('-vf', 'whatever i put would end up as a filter'), $filter->apply($frame));
}
}

View file

@ -12,7 +12,7 @@ class ExtractMultipleFramesFilterTest extends TestCase
/**
* @dataProvider provideFrameRates
*/
public function testApply($frameRate, $destinationFolder, $duration, $modulus, $expected)
public function testApply($frameRate, $frameFileType,$destinationFolder, $duration, $modulus, $expected)
{
$video = $this->getVideoMock();
$pathfile = '/path/to/file'.mt_rand();
@ -34,18 +34,41 @@ class ExtractMultipleFramesFilterTest extends TestCase
->will($this->returnValue($streams));
$filter = new ExtractMultipleFramesFilter($frameRate, $destinationFolder);
$filter->setFrameFileType($frameFileType);
$this->assertEquals($expected, $filter->apply($video, $format));
}
public function provideFrameRates()
{
return array(
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_SEC, '/', 100, 2, array('-vf', 'fps=1/1', '/frame-%03d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, '/', 100, 2, array('-vf', 'fps=1/2', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_5SEC, '/', 100, 2, array('-vf', 'fps=1/5', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_10SEC, '/', 100, 2, array('-vf', 'fps=1/10', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_30SEC, '/', 100, 2, array('-vf', 'fps=1/30', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_60SEC, '/', 100, 2, array('-vf', 'fps=1/60', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_SEC, 'jpg', '/', 100, 2, array('-vf', 'fps=1/1', '/frame-%03d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, 'jpg', '/', 100, 2, array('-vf', 'fps=1/2', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_5SEC, 'jpg', '/', 100, 2, array('-vf', 'fps=1/5', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_10SEC, 'jpg', '/', 100, 2, array('-vf', 'fps=1/10', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_30SEC, 'jpg', '/', 100, 2, array('-vf', 'fps=1/30', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_60SEC, 'jpg', '/', 100, 2, array('-vf', 'fps=1/60', '/frame-%02d.jpg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_SEC, 'jpeg', '/', 100, 2, array('-vf', 'fps=1/1', '/frame-%03d.jpeg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, 'jpeg', '/', 100, 2, array('-vf', 'fps=1/2', '/frame-%02d.jpeg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_5SEC, 'jpeg', '/', 100, 2, array('-vf', 'fps=1/5', '/frame-%02d.jpeg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_10SEC, 'jpeg', '/', 100, 2, array('-vf', 'fps=1/10', '/frame-%02d.jpeg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_30SEC, 'jpeg', '/', 100, 2, array('-vf', 'fps=1/30', '/frame-%02d.jpeg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_60SEC, 'jpeg', '/', 100, 2, array('-vf', 'fps=1/60', '/frame-%02d.jpeg')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_SEC, 'png', '/', 100, 2, array('-vf', 'fps=1/1', '/frame-%03d.png')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, 'png', '/', 100, 2, array('-vf', 'fps=1/2', '/frame-%02d.png')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_5SEC, 'png', '/', 100, 2, array('-vf', 'fps=1/5', '/frame-%02d.png')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_10SEC, 'png', '/', 100, 2, array('-vf', 'fps=1/10', '/frame-%02d.png')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_30SEC, 'png', '/', 100, 2, array('-vf', 'fps=1/30', '/frame-%02d.png')),
array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_60SEC, 'png', '/', 100, 2, array('-vf', 'fps=1/60', '/frame-%02d.png')),
);
}
/**
* @expectedException \FFMpeg\Exception\InvalidArgumentException
*/
public function testInvalidFrameFileType() {
$filter = new ExtractMultipleFramesFilter('1/1', '/');
$filter->setFrameFileType('webm');
}
}

View file

@ -11,7 +11,7 @@ class VideoProgressListenerTest extends TestCase
/**
* @dataProvider provideData
*/
public function testHandle($size, $duration,
public function testHandle($size, $duration, $newVideoDuration,
$data, $expectedPercent, $expectedRemaining, $expectedRate,
$data2, $expectedPercent2, $expectedRemaining2, $expectedRate2,
$currentPass, $totalPass
@ -26,7 +26,7 @@ class VideoProgressListenerTest extends TestCase
'duration' => $duration,
))));
$listener = new VideoProgressListener($ffprobe, __FILE__, $currentPass, $totalPass);
$listener = new VideoProgressListener($ffprobe, __FILE__, $currentPass, $totalPass, $newVideoDuration);
$phpunit = $this;
$n = 0;
$listener->on('progress', function ($percent, $remaining, $rate) use (&$n, $phpunit, $expectedPercent, $expectedRemaining, $expectedRate, $expectedPercent2, $expectedRemaining2, $expectedRate2) {
@ -57,6 +57,7 @@ class VideoProgressListenerTest extends TestCase
array(
147073958,
281.147533,
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,
@ -71,6 +72,7 @@ class VideoProgressListenerTest extends TestCase
array(
147073958,
281.147533,
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,
@ -81,6 +83,21 @@ class VideoProgressListenerTest extends TestCase
3868,
1,
2
),
array(
147073958,
281.147533,
35,
'frame= 206 fps=202 q=10.0 size= 571kB time=00:00:07.12 bitrate= 656.8kbits/s dup=9 drop=0',
60,
0,
0,
'frame= 854 fps=113 q=20.0 size= 4430kB time=00:00:33.04 bitrate=1098.5kbits/s dup=36 drop=0',
97,
0,
3868,
2,
2
)
);
}

View file

@ -0,0 +1,67 @@
<?php
namespace Tests\FFMpeg\Unit\Media;
use FFMpeg\Media\Clip;
class ClipTest extends AbstractMediaTestCase
{
/**
* @dataProvider provideBuildOptions
*/
public function testBuildCommand($startValue, $durationValue, $commands)
{
$configuration = $this->getConfigurationMock();
$driver = $this->getFFMpegDriverMock();
$driver->expects($this->any())
->method('getConfiguration')
->will($this->returnValue($configuration));
$ffprobe = $this->getFFProbeMock();
$start = $this->getTimeCodeMock();
$start->expects($this->once())
->method('__toString')
->will($this->returnValue($startValue));
$duration = null;
if (null !== $durationValue) {
$duration = $this->getTimeCodeMock();
$duration->expects($this->once())
->method('__toString')
->will($this->returnValue($durationValue));
}
$outputPathfile = '/target/file';
$format = $this->getMock('FFMpeg\Format\VideoInterface');
$format->expects($this->any())
->method('getPasses')
->will($this->returnValue(1));
$format->expects($this->any())
->method('getExtraParams')
->will($this->returnValue(array()));
$clip = new Clip($this->getVideoMock(__FILE__), $driver, $ffprobe, $start, $duration);
$fc = $clip->getFinalCommand($format, $outputPathfile);
$this->assertCount(1, $fc);
$this->assertStringStartsWith(implode(' ', $commands), $fc[0]);
}
public function provideBuildOptions()
{
return array(
array('SS01', null, array(
'-y', '-ss', 'SS01',
'-i', __FILE__)
),
array('SS02', 'D02', array(
'-y', '-ss', 'SS02',
'-i', __FILE__,
'-t', 'D02')
)
);
}
}

View file

@ -61,7 +61,9 @@ class FrameTest extends AbstractMediaTestCase
$pathfile = '/target/destination';
array_push($commands, $pathfile);
if (!$base64) {
array_push($commands, $pathfile);
}
$driver->expects($this->once())
->method('command')

View file

@ -52,7 +52,7 @@ class WaveformTest extends AbstractMediaTestCase
->method('command')
->with($commands);
$waveform = new Waveform($this->getAudioMock(__FILE__), $driver, $ffprobe, 640, 120);
$waveform = new Waveform($this->getAudioMock(__FILE__), $driver, $ffprobe, 640, 120, ['#FFFFFF']);
$this->assertSame($waveform, $waveform->save($pathfile));
}
@ -61,8 +61,8 @@ class WaveformTest extends AbstractMediaTestCase
return array(
array(
array(
'-i', NULL, '-filter_complex',
'showwavespic=s=640x120',
'-y', '-i', NULL, '-filter_complex',
'showwavespic=colors=#FFFFFF:s=640x120',
'-frames:v', '1',
),
),

View file

@ -2,7 +2,9 @@
namespace Tests\FFMpeg\Unit;
class TestCase extends \PHPUnit_Framework_TestCase
use PHPUnit\Framework\TestCase as BaseTestCase;
class TestCase extends BaseTestCase
{
public function assertScalar($value)
{

Binary file not shown.

Binary file not shown.