Merge branch 'master' into patch-1

This commit is contained in:
CDNRocket 2018-03-01 21:13:47 +01:00 committed by GitHub
commit b60a6c9922
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 3790 additions and 421 deletions

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

@ -89,4 +89,32 @@ class TimeCode
return new static($hours, $minutes, $seconds, $frames);
}
/**
* Returns this timecode in seconds
* @return int
*/
public function toSeconds() {
$seconds = 0;
$seconds += $this->hours * 60 * 60;
$seconds += $this->minutes * 60;
$seconds += $this->seconds;
// TODO: Handle frames?
return (int) $seconds;
}
/**
* Helper function wether `$timecode` is after this one
*
* @param TimeCode $timecode The Timecode to compare
* @return bool
*/
public function isAfter(TimeCode $timecode) {
// convert everything to seconds and compare
return ($this->toSeconds() > $timecode->toSeconds());
}
}

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

@ -11,8 +11,6 @@
namespace FFMpeg\FFProbe\DataMapping;
use FFMpeg\Exception\InvalidArgumentException;
abstract class AbstractData implements \Countable
{
private $properties;

View file

@ -0,0 +1,58 @@
<?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\Filters\Audio;
use FFMpeg\Filters\Audio\AudioFilterInterface;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Media\Audio;
class AddMetadataFilter implements AudioFilterInterface
{
/** @var Array */
private $metaArr;
/** @var Integer */
private $priority;
function __construct($metaArr = null, $priority = 9)
{
$this->metaArr = $metaArr;
$this->priority = $priority;
}
public function getPriority()
{
//must be of high priority in case theres a second input stream (artwork) to register with audio
return $this->priority;
}
public function apply(Audio $audio, AudioInterface $format)
{
$meta = $this->metaArr;
if (is_null($meta)) {
return ['-map_metadata', '-1', '-vn'];
}
$metadata = [];
if (array_key_exists("artwork", $meta)) {
array_push($metadata, "-i", $meta['artwork'], "-map", "0", "-map", "1");
unset($meta['artwork']);
}
foreach ($meta as $k => $v) {
array_push($metadata, "-metadata", "$k=$v");
}
return $metadata;
}
}

View file

@ -0,0 +1,84 @@
<?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\Filters\Audio;
use FFMpeg\Coordinate\TimeCode;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Media\Audio;
class AudioClipFilter implements AudioFilterInterface {
/**
* @var TimeCode
*/
private $start;
/**
* @var TimeCode
*/
private $duration;
/**
* @var int
*/
private $priority;
public function __construct(TimeCode $start, TimeCode $duration = null, $priority = 0) {
$this->start = $start;
$this->duration = $duration;
$this->priority = $priority;
}
/**
* @inheritDoc
*/
public function getPriority() {
return $this->priority;
}
/**
* Returns the start position the audio is being cutted
*
* @return TimeCode
*/
public function getStart() {
return $this->start;
}
/**
* Returns how long the audio is being cutted. Returns null when the duration is infinite,
*
* @return TimeCode|null
*/
public function getDuration() {
return $this->duration;
}
/**
* @inheritDoc
*/
public function apply(Audio $audio, AudioInterface $format) {
$commands = array('-ss', (string) $this->start);
if ($this->duration !== null) {
$commands[] = '-t';
$commands[] = (string) $this->duration;
}
$commands[] = '-acodec';
$commands[] = 'copy';
return $commands;
}
}

View file

@ -1,8 +1,19 @@
<?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\Filters\Audio;
use FFMpeg\Filters\Audio\AddMetadataFilter;
use FFMpeg\Media\Audio;
use FFMpeg\Coordinate\TimeCode;
class AudioFilters
{
@ -26,4 +37,38 @@ class AudioFilters
return $this;
}
/**
* Add metadata to an audio file. If no arguments are given then filter
* will remove all metadata from the audio file
* @param Array|Null $data If array must contain one of these key/value pairs:
* - "title": Title metadata
* - "artist": Artist metadata
* - "composer": Composer metadata
* - "album": Album metadata
* - "track": Track metadata
* - "artwork": Song artwork. String of file path
* - "year": Year metadata
* - "genre": Genre metadata
* - "description": Description metadata
*/
public function addMetadata($data = null)
{
$this->media->addFilter(new AddMetadataFilter($data));
return $this;
}
/**
* Cuts the audio at `$start`, optionally define the end
*
* @param TimeCode $start Where the clipping starts(seek to time)
* @param TimeCode $duration How long the clipped audio should be
* @return AudioFilters
*/
public function clip($start, $duration = null) {
$this->media->addFilter(new AudioClipFilter($start, $duration));
return $this;
}
}

View file

@ -1,5 +1,14 @@
<?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\Filters\Audio;
use FFMpeg\Media\Audio;

View file

@ -0,0 +1,20 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Concat;
use FFMpeg\Filters\FilterInterface;
use FFMpeg\Media\Concat;
interface ConcatFilterInterface extends FilterInterface
{
public function apply(Concat $concat);
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Concat;
use FFMpeg\Media\Concat;
class ConcatFilters
{
private $concat;
public function __construct(Concat $concat)
{
$this->concat = $concat;
}
}

View file

@ -0,0 +1,20 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Gif;
use FFMpeg\Filters\FilterInterface;
use FFMpeg\Media\Gif;
interface GifFilterInterface extends FilterInterface
{
public function apply(Gif $gif);
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Gif;
use FFMpeg\Media\Gif;
class GifFilters
{
private $gif;
public function __construct(Gif $gif)
{
$this->gif = $gif;
}
}

View file

@ -0,0 +1,128 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <romain@strime.io>
*
* 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\Exception\InvalidArgumentException;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Media\Video;
use FFMpeg\Format\VideoInterface;
class ExtractMultipleFramesFilter implements VideoFilterInterface
{
/** will extract a frame every second */
const FRAMERATE_EVERY_SEC = '1/1';
/** will extract a frame every 2 seconds */
const FRAMERATE_EVERY_2SEC = '1/2';
/** will extract a frame every 5 seconds */
const FRAMERATE_EVERY_5SEC = '1/5';
/** will extract a frame every 10 seconds */
const FRAMERATE_EVERY_10SEC = '1/10';
/** will extract a frame every 30 seconds */
const FRAMERATE_EVERY_30SEC = '1/30';
/** will extract a frame every minute */
const FRAMERATE_EVERY_60SEC = '1/60';
/** @var integer */
private $priority;
private $frameRate;
private $destinationFolder;
public function __construct($frameRate = self::FRAMERATE_EVERY_SEC, $destinationFolder = __DIR__, $priority = 0)
{
$this->priority = $priority;
$this->frameRate = $frameRate;
// Make sure that the destination folder has a trailing slash
if(strcmp( substr($destinationFolder, -1), "/") != 0)
$destinationFolder .= "/";
// Set the destination folder
$this->destinationFolder = $destinationFolder;
}
/**
* {@inheritdoc}
*/
public function getPriority()
{
return $this->priority;
}
/**
* {@inheritdoc}
*/
public function getFrameRate()
{
return $this->frameRate;
}
/**
* {@inheritdoc}
*/
public function getDestinationFolder()
{
return $this->destinationFolder;
}
/**
* {@inheritdoc}
*/
public function apply(Video $video, VideoInterface $format)
{
$commands = array();
$duration = 0;
try {
// Get the duration of the video
foreach ($video->getStreams()->videos() as $stream) {
if ($stream->has('duration')) {
$duration = $stream->get('duration');
}
}
// Get the number of frames per second we have to extract.
if(preg_match('/(\d+)(?:\s*)([\+\-\*\/])(?:\s*)(\d+)/', $this->frameRate, $matches) !== FALSE){
$operator = $matches[2];
switch($operator){
case '/':
$nbFramesPerSecond = $matches[1] / $matches[3];
break;
default:
throw new InvalidArgumentException('The frame rate is not a proper division: ' . $this->frameRate);
break;
}
}
// Set the number of digits to use in the exported filenames
$nbImages = ceil( $duration * $nbFramesPerSecond );
if($nbImages < 100)
$nbDigitsInFileNames = "02";
elseif($nbImages < 1000)
$nbDigitsInFileNames = "03";
else
$nbDigitsInFileNames = "06";
// Set the parameters
$commands[] = '-vf';
$commands[] = 'fps=' . $this->frameRate;
$commands[] = $this->destinationFolder . 'frame-%'.$nbDigitsInFileNames.'d.jpg';
}
catch (RuntimeException $e) {
throw new RuntimeException('An error occured while extracting the frames: ' . $e->getMessage() . '. The code: ' . $e->getCode());
}
return $commands;
}
}

View file

@ -0,0 +1,59 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* 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 PadFilter implements VideoFilterInterface
{
/** @var Dimension */
private $dimension;
/** @var integer */
private $priority;
public function __construct(Dimension $dimension, $priority = 0)
{
$this->dimension = $dimension;
$this->priority = $priority;
}
/**
* {@inheritdoc}
*/
public function getPriority()
{
return $this->priority;
}
/**
* @return Dimension
*/
public function getDimension()
{
return $this->dimension;
}
/**
* {@inheritdoc}
*/
public function apply(Video $video, VideoInterface $format)
{
$commands = array();
$commands[] = '-vf';
$commands[] = 'scale=iw*min(' . $this->dimension->getWidth() . '/iw\,' . $this->dimension->getHeight() .'/ih):ih*min(' . $this->dimension->getWidth() . '/iw\,' . $this->dimension->getHeight() .'/ih),pad=' . $this->dimension->getWidth() . ':' . $this->dimension->getHeight() . ':(' . $this->dimension->getWidth() . '-iw)/2:(' . $this->dimension->getHeight() .'-ih)/2';
return $commands;
}
}

View file

@ -98,8 +98,10 @@ class ResizeFilter implements VideoFilterInterface
if (null !== $dimensions) {
$dimensions = $this->getComputedDimensions($dimensions, $format->getModulus());
$commands[] = '-s';
$commands[] = $dimensions->getWidth() . 'x' . $dimensions->getHeight();
// Using Filter to have ordering
$commands[] = '-vf';
$commands[] = '[in]scale=' . $dimensions->getWidth() . ':' . $dimensions->getHeight() . ' [out]';
}
return $commands;

View file

@ -46,7 +46,7 @@ class VideoFilters extends AudioFilters
* Changes the video framerate.
*
* @param FrameRate $framerate
* @param type $gop
* @param Integer $gop
*
* @return VideoFilters
*/
@ -57,6 +57,21 @@ class VideoFilters extends AudioFilters
return $this;
}
/**
* Extract multiple frames from the video
*
* @param string $frameRate
* @param string $destinationFolder
*
* @return $this
*/
public function extractMultipleFrames($frameRate = ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, $destinationFolder = __DIR__)
{
$this->media->addFilter(new ExtractMultipleFramesFilter($frameRate, $destinationFolder));
return $this;
}
/**
* Synchronizes audio and video.
*
@ -98,6 +113,20 @@ class VideoFilters extends AudioFilters
return $this;
}
/**
* Adds padding (black bars) to a video.
*
* @param Dimension $dimension
*
* @return VideoFilters
*/
public function pad(Dimension $dimension)
{
$this->media->addFilter(new PadFilter($dimension));
return $this;
}
public function rotate($angle)
{
$this->media->addFilter(new RotateFilter($angle, 30));
@ -132,4 +161,18 @@ class VideoFilters extends AudioFilters
return $this;
}
/**
* Applies a custom filter: -vf foo bar
*
* @param string $parameters
*
* @return VideoFilters
*/
public function custom($parameters)
{
$this->media->addFilter(new CustomFilter($parameters));
return $this;
}
}

View file

@ -0,0 +1,74 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Waveform;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Media\Waveform;
class WaveformDownmixFilter implements WaveformFilterInterface
{
/** @var boolean */
private $downmix;
/** @var integer */
private $priority;
// By default, the downmix value is set to FALSE.
public function __construct($downmix = FALSE, $priority = 0)
{
$this->downmix = $downmix;
$this->priority = $priority;
}
/**
* {@inheritdoc}
*/
public function getDownmix()
{
return $this->downmix;
}
/**
* {@inheritdoc}
*/
public function getPriority()
{
return $this->priority;
}
/**
* {@inheritdoc}
*/
public function apply(Waveform $waveform)
{
$commands = array();
foreach ($waveform->getAudio()->getStreams() as $stream) {
if ($stream->isAudio()) {
try {
// If the downmix parameter is set to TRUE, we add an option to the FFMPEG command
if($this->downmix == TRUE) {
$commands[] = '"aformat=channel_layouts=mono"';
}
break;
} catch (RuntimeException $e) {
}
}
}
return $commands;
}
}

View file

@ -0,0 +1,20 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Waveform;
use FFMpeg\Filters\FilterInterface;
use FFMpeg\Media\Waveform;
interface WaveformFilterInterface extends FilterInterface
{
public function apply(Waveform $waveform);
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FFMpeg\Filters\Waveform;
use FFMpeg\Media\Waveform;
class WaveformFilters
{
private $waveform;
public function __construct(Waveform $waveform)
{
$this->waveform = $waveform;
}
/**
* Sets the downmix of the output waveform.
*
* If you want a simpler waveform, sets the downmix to TRUE.
*
* @return WaveformFilters
*/
public function setDownmix()
{
$this->waveform->addFilter(new WaveformDownmixFilter());
return $this;
}
}

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;
}
/**
@ -120,6 +121,14 @@ abstract class AbstractProgressListener extends EventEmitter implements Listener
return $this->totalPass;
}
/**
* @return int
*/
public function getCurrentTime()
{
return $this->currentTime;
}
/**
* {@inheritdoc}
*/
@ -171,6 +180,12 @@ abstract class AbstractProgressListener extends EventEmitter implements Listener
if ($this->lastOutput !== null) {
$delta = $currentTime - $this->lastOutput;
// Check the type of the currentSize variable and convert it to an integer if needed.
if(!is_numeric($currentSize)) {
$currentSize = (int)$currentSize;
}
$deltaSize = $currentSize - $this->currentSize;
$rate = $deltaSize * $delta;
if ($rate > 0) {
@ -240,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

@ -32,6 +32,9 @@ abstract class DefaultVideo extends DefaultAudio implements VideoInterface
/** @var Integer */
protected $modulus = 16;
/** @var Array */
protected $additionalParamaters;
/**
* {@inheritdoc}
*/
@ -97,10 +100,35 @@ abstract class DefaultVideo extends DefaultAudio implements VideoInterface
/**
* {@inheritdoc}
*/
public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total)
public function getAdditionalParameters()
{
return $this->additionalParamaters;
}
/**
* Sets additional parameters.
*
* @param array $additionalParamaters
* @throws InvalidArgumentException
*/
public function setAdditionalParameters($additionalParamaters)
{
if (!is_array($additionalParamaters)) {
throw new InvalidArgumentException('Wrong additionalParamaters value');
}
$this->additionalParamaters = $additionalParamaters;
return $this;
}
/**
* {@inheritdoc}
*/
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

@ -19,6 +19,9 @@ class X264 extends DefaultVideo
/** @var boolean */
private $bframesSupport = true;
/** @var integer */
private $passes = 2;
public function __construct($audioCodec = 'libfaac', $videoCodec = 'libx264')
{
$this
@ -51,7 +54,7 @@ class X264 extends DefaultVideo
*/
public function getAvailableAudioCodecs()
{
return array('libvo_aacenc', 'libfaac', 'libmp3lame', 'libfdk_aac');
return array('aac', 'libvo_aacenc', 'libfaac', 'libmp3lame', 'libfdk_aac');
}
/**
@ -62,12 +65,23 @@ class X264 extends DefaultVideo
return array('libx264');
}
/**
* @param $passes
*
* @return X264
*/
public function setPasses($passes)
{
$this->passes = $passes;
return $this;
}
/**
* {@inheritDoc}
*/
public function getPasses()
{
return 2;
return $this->passes;
}
/**

View file

@ -54,4 +54,11 @@ interface VideoInterface extends AudioInterface
* @return array
*/
public function getAvailableVideoCodecs();
/**
* Returns the list of available video codecs for this format.
*
* @return array
*/
public function getAdditionalParameters();
}

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,13 +124,19 @@ 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 $commands;
}
return $this;
/**
* Gets the waveform of the video.
*
* @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, $colors = array(Waveform::DEFAULT_COLOR))
{
return new Waveform($this, $this->driver, $this->ffprobe, $width, $height, $colors);
}
}

264
src/FFMpeg/Media/Concat.php Normal file
View file

@ -0,0 +1,264 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* 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 Alchemy\BinaryDriver\Exception\InvalidArgumentException;
use FFMpeg\Filters\Concat\ConcatFilterInterface;
use FFMpeg\Filters\Concat\ConcatFilters;
use FFMpeg\Driver\FFMpegDriver;
use FFMpeg\FFProbe;
use FFMpeg\Filters\Audio\SimpleFilter;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Format\FormatInterface;
use FFMpeg\Filters\FilterInterface;
use FFMpeg\Format\ProgressableInterface;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Format\VideoInterface;
use Neutron\TemporaryFilesystem\Manager as FsManager;
class Concat extends AbstractMediaType
{
/** @var array */
private $sources;
public function __construct($sources, FFMpegDriver $driver, FFProbe $ffprobe)
{
parent::__construct($sources, $driver, $ffprobe);
$this->sources = $sources;
}
/**
* Returns the path to the sources.
*
* @return string
*/
public function getSources()
{
return $this->sources;
}
/**
* {@inheritdoc}
*
* @return ConcatFilters
*/
public function filters()
{
return new ConcatFilters($this);
}
/**
* {@inheritdoc}
*
* @return Concat
*/
public function addFilter(ConcatFilterInterface $filter)
{
$this->filters->add($filter);
return $this;
}
/**
* 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
*
* @return Concat
*
* @throws RuntimeException
*/
public function saveFromSameCodecs($outputPathfile, $streamCopy = TRUE)
{
/**
* @see https://ffmpeg.org/ffmpeg-formats.html#concat
* @see https://trac.ffmpeg.org/wiki/Concatenate
*/
// Create the file which will contain the list of videos
$fs = FsManager::create();
$sourcesFile = $fs->createTemporaryFile('ffmpeg-concat');
// Set the content of this file
$fileStream = @fopen($sourcesFile, 'w');
if($fileStream === false) {
throw new ExecutionFailureException('Cannot open the temporary file.');
}
$count_videos = 0;
if(is_array($this->sources) && (count($this->sources) > 0)) {
foreach ($this->sources as $videoPath) {
$line = "";
if($count_videos != 0)
$line .= "\n";
$line .= "file ".$videoPath;
fwrite($fileStream, $line);
$count_videos++;
}
}
else {
throw new InvalidArgumentException('The list of videos is not a valid array.');
}
fclose($fileStream);
$commands = array(
'-f', 'concat', '-safe', '0',
'-i', $sourcesFile
);
// Check if stream copy is activated
if($streamCopy === TRUE) {
$commands[] = '-c';
$commands[] = 'copy';
}
// If not, we can apply filters
else {
foreach ($this->filters as $filter) {
$commands = array_merge($commands, $filter->apply($this));
}
}
// Set the output file in the command
$commands = array_merge($commands, array($outputPathfile));
// Execute the command
try {
$this->driver->command($commands);
} catch (ExecutionFailureException $e) {
$this->cleanupTemporaryFile($outputPathfile);
$this->cleanupTemporaryFile($sourcesFile);
throw new RuntimeException('Unable to save concatenated video', $e->getCode(), $e);
}
$this->cleanupTemporaryFile($sourcesFile);
return $this;
}
/**
* Saves the concatenated video in the given filename, considering that the sources videos are all encoded with the same codec.
*
* @param string $outputPathfile
*
* @return Concat
*
* @throws RuntimeException
*/
public function saveFromDifferentCodecs(FormatInterface $format, $outputPathfile)
{
/**
* @see https://ffmpeg.org/ffmpeg-formats.html#concat
* @see https://trac.ffmpeg.org/wiki/Concatenate
*/
// Check the validity of the parameter
if(!is_array($this->sources) || (count($this->sources) == 0)) {
throw new InvalidArgumentException('The list of videos is not a valid array.');
}
// Create the commands variable
$commands = array();
// Prepare the parameters
$nbSources = 0;
$files = array();
// For each source, check if this is a legit file
// and prepare the parameters
foreach ($this->sources as $videoPath) {
$files[] = '-i';
$files[] = $videoPath;
$nbSources++;
}
$commands = array_merge($commands, $files);
// Set the parameters of the request
$commands[] = '-filter_complex';
$complex_filter = '';
for($i=0; $i<$nbSources; $i++) {
$complex_filter .= '['.$i.':v:0] ['.$i.':a:0] ';
}
$complex_filter .= 'concat=n='.$nbSources.':v=1:a=1 [v] [a]';
$commands[] = $complex_filter;
$commands[] = '-map';
$commands[] = '[v]';
$commands[] = '-map';
$commands[] = '[a]';
// Prepare the filters
$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())));
}
}
// Add the filters
foreach ($this->filters as $filter) {
$commands = array_merge($commands, $filter->apply($this));
}
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;
}
}
}
// Set the output file in the command
$commands[] = $outputPathfile;
$failure = null;
try {
$this->driver->command($commands);
} catch (ExecutionFailureException $e) {
throw new RuntimeException('Encoding failed', $e->getCode(), $e);
}
return $this;
}
}

View file

@ -85,26 +85,31 @@ class Frame extends AbstractMediaType
*
* @throws RuntimeException
*/
public function save($pathfile, $accurate = false)
public function save($pathfile, $accurate = false, $returnBase64 = false)
{
/**
* might be optimized with http://ffmpeg.org/trac/ffmpeg/wiki/Seeking%20with%20FFmpeg
* @see http://ffmpeg.org/ffmpeg.html#Main-options
*/
$outputFormat = $returnBase64 ? "image2pipe" : "image2";
if (!$accurate) {
$commands = array(
'-y', '-ss', (string) $this->timecode,
'-i', $this->pathfile,
'-vframes', '1',
'-f', 'image2'
'-f', $outputFormat
);
} else {
$commands = array(
'-y', '-i', $this->pathfile,
'-vframes', '1', '-ss', (string) $this->timecode,
'-f', 'image2'
'-f', $outputFormat
);
}
if($returnBase64) {
array_push($commands, "-");
}
foreach ($this->filters as $filter) {
$commands = array_merge($commands, $filter->apply($this));
@ -113,12 +118,16 @@ class Frame extends AbstractMediaType
$commands = array_merge($commands, array($pathfile));
try {
$this->driver->command($commands);
if(!$returnBase64) {
$this->driver->command($commands);
return $this;
}
else {
return $this->driver->command($commands);
}
} catch (ExecutionFailureException $e) {
$this->cleanupTemporaryFile($pathfile);
throw new RuntimeException('Unable to save frame', $e->getCode(), $e);
}
return $this;
}
}

137
src/FFMpeg/Media/Gif.php Normal file
View file

@ -0,0 +1,137 @@
<?php
/*
* This file is part of PHP-FFmpeg.
*
* (c) Strime <contact@strime.io>
*
* 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\Gif\GifFilterInterface;
use FFMpeg\Filters\Gif\GifFilters;
use FFMpeg\Driver\FFMpegDriver;
use FFMpeg\FFProbe;
use FFMpeg\Exception\RuntimeException;
use FFMpeg\Coordinate\TimeCode;
use FFMpeg\Coordinate\Dimension;
class Gif extends AbstractMediaType
{
/** @var TimeCode */
private $timecode;
/** @var Dimension */
private $dimension;
/** @var integer */
private $duration;
/** @var Video */
private $video;
public function __construct(Video $video, FFMpegDriver $driver, FFProbe $ffprobe, TimeCode $timecode, Dimension $dimension, $duration = null)
{
parent::__construct($video->getPathfile(), $driver, $ffprobe);
$this->timecode = $timecode;
$this->dimension = $dimension;
$this->duration = $duration;
$this->video = $video;
}
/**
* Returns the video related to the gif.
*
* @return Video
*/
public function getVideo()
{
return $this->video;
}
/**
* {@inheritdoc}
*
* @return GifFilters
*/
public function filters()
{
return new GifFilters($this);
}
/**
* {@inheritdoc}
*
* @return Gif
*/
public function addFilter(GifFilterInterface $filter)
{
$this->filters->add($filter);
return $this;
}
/**
* @return TimeCode
*/
public function getTimeCode()
{
return $this->timecode;
}
/**
* @return Dimension
*/
public function getDimension()
{
return $this->dimension;
}
/**
* Saves the gif in the given filename.
*
* @param string $pathfile
*
* @return Gif
*
* @throws RuntimeException
*/
public function save($pathfile)
{
/**
* @see http://ffmpeg.org/ffmpeg.html#Main-options
*/
$commands = array(
'-ss', (string)$this->timecode
);
if(null !== $this->duration) {
$commands[] = '-t';
$commands[] = (string)$this->duration;
}
$commands[] = '-i';
$commands[] = $this->pathfile;
$commands[] = '-vf';
$commands[] = 'scale=' . $this->dimension->getWidth() . ':-1';
$commands[] = '-gifflags';
$commands[] = '+transdiff';
$commands[] = '-y';
foreach ($this->filters as $filter) {
$commands = array_merge($commands, $filter->apply($this));
}
$commands = array_merge($commands, array($pathfile));
try {
$this->driver->command($commands);
} catch (ExecutionFailureException $e) {
$this->cleanupTemporaryFile($pathfile);
throw new RuntimeException('Unable to save gif', $e->getCode(), $e);
}
return $this;
}
}

View file

@ -13,6 +13,7 @@ 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;
@ -23,12 +24,24 @@ use FFMpeg\Format\ProgressableInterface;
use FFMpeg\Format\AudioInterface;
use FFMpeg\Format\VideoInterface;
use Neutron\TemporaryFilesystem\Manager as FsManager;
use FFMpeg\Filters\Video\ClipFilter;
class Video extends Audio
{
/**
* {@inheritdoc}
*
* FileSystem Manager instance
* @var Manager
*/
protected $fs;
/**
* FileSystem Manager ID
* @var int
*/
protected $fsId;
/**
* @inheritDoc
* @return VideoFilters
*/
public function filters()
@ -37,8 +50,7 @@ class Video extends Audio
}
/**
* {@inheritdoc}
*
* @inheritDoc
* @return Video
*/
public function addFilter(FilterInterface $filter)
@ -51,15 +63,78 @@ class Video extends Audio
/**
* Exports the video in the desired format, applies registered filters.
*
* @param FormatInterface $format
* @param string $outputPathfile
*
* @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 = array('-y', '-i', $this->pathfile);
$filters = clone $this->filters;
@ -119,17 +194,76 @@ class Video extends Audio
}
}
$fs = FsManager::create();
$fsId = uniqid('ffmpeg-passes');
$passPrefix = $fs->createTemporaryDirectory(0777, 50, $fsId) . '/' . uniqid('pass-');
// 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 (1 > $totalPasses) {
if(!$totalPasses) {
throw new InvalidArgumentException('Pass number should be a positive value.');
}
for ($i = 1; $i <= $totalPasses; $i++) {
for($i = 1; $i <= $totalPasses; $i++) {
$pass = $commands;
if ($totalPasses > 1) {
@ -144,31 +278,7 @@ class Video extends Audio
$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;
return $passes;
}
/**
@ -181,4 +291,28 @@ class Video extends Audio
{
return new Frame($this, $this->driver, $this->ffprobe, $at);
}
/**
* Extracts a gif from a sequence of the video.
*
* @param TimeCode $at
* @param Dimension $dimension
* @param integer $duration
* @return Gif
*/
public function gif(TimeCode $at, Dimension $dimension, $duration = null)
{
return new Gif($this, $this->driver, $this->ffprobe, $at, $dimension, $duration);
}
/**
* Concatenates a list of videos into one unique video.
*
* @param array $sources
* @return Concat
*/
public function concat($sources)
{
return new Concat($sources, $this->driver, $this->ffprobe);
}
}

View file

@ -0,0 +1,163 @@
<?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\Exception\InvalidArgumentException;
use FFMpeg\Filters\Waveform\WaveformFilterInterface;
use FFMpeg\Filters\Waveform\WaveformFilters;
use FFMpeg\Driver\FFMpegDriver;
use FFMpeg\FFProbe;
use FFMpeg\Exception\RuntimeException;
class Waveform extends AbstractMediaType
{
const DEFAULT_COLOR = '#000000';
/** @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);
}
/**
* Returns the audio related to the waveform.
*
* @return Audio
*/
public function getAudio()
{
return $this->audio;
}
/**
* {@inheritdoc}
*
* @return WaveformFilters
*/
public function filters()
{
return new WaveformFilters($this);
}
/**
* {@inheritdoc}
*
* @return Waveform
*/
public function addFilter(WaveformFilterInterface $filter)
{
$this->filters->add($filter);
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.
*
* @param string $pathfile
*
* @return Waveform
*
* @throws RuntimeException
*/
public function save($pathfile)
{
/**
* might be optimized with http://ffmpeg.org/trac/ffmpeg/wiki/Seeking%20with%20FFmpeg
* @see http://ffmpeg.org/ffmpeg.html#Main-options
*/
$commands = array(
'-y', '-i', $this->pathfile, '-filter_complex',
'showwavespic=colors='.$this->compileColors().':s='.$this->width.'x'.$this->height,
'-frames:v', '1'
);
foreach ($this->filters as $filter) {
$commands = array_merge($commands, $filter->apply($this));
}
$commands = array_merge($commands, array($pathfile));
try {
$this->driver->command($commands);
} catch (ExecutionFailureException $e) {
$this->cleanupTemporaryFile($pathfile);
throw new RuntimeException('Unable to save waveform', $e->getCode(), $e);
}
return $this;
}
}