Merge branch 'master' into patch-1
This commit is contained in:
commit
b60a6c9922
112 changed files with 3790 additions and 421 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@
|
|||
|
||||
namespace FFMpeg\FFProbe\DataMapping;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
|
||||
abstract class AbstractData implements \Countable
|
||||
{
|
||||
private $properties;
|
||||
|
|
|
|||
58
src/FFMpeg/Filters/Audio/AddMetadataFilter.php
Normal file
58
src/FFMpeg/Filters/Audio/AddMetadataFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
84
src/FFMpeg/Filters/Audio/AudioClipFilter.php
Normal file
84
src/FFMpeg/Filters/Audio/AudioClipFilter.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
20
src/FFMpeg/Filters/Concat/ConcatFilterInterface.php
Normal file
20
src/FFMpeg/Filters/Concat/ConcatFilterInterface.php
Normal 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);
|
||||
}
|
||||
24
src/FFMpeg/Filters/Concat/ConcatFilters.php
Normal file
24
src/FFMpeg/Filters/Concat/ConcatFilters.php
Normal 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;
|
||||
}
|
||||
}
|
||||
20
src/FFMpeg/Filters/Gif/GifFilterInterface.php
Normal file
20
src/FFMpeg/Filters/Gif/GifFilterInterface.php
Normal 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);
|
||||
}
|
||||
24
src/FFMpeg/Filters/Gif/GifFilters.php
Normal file
24
src/FFMpeg/Filters/Gif/GifFilters.php
Normal 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;
|
||||
}
|
||||
}
|
||||
128
src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php
Normal file
128
src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
59
src/FFMpeg/Filters/Video/PadFilter.php
Normal file
59
src/FFMpeg/Filters/Video/PadFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
src/FFMpeg/Filters/Waveform/WaveformDownmixFilter.php
Normal file
74
src/FFMpeg/Filters/Waveform/WaveformDownmixFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
20
src/FFMpeg/Filters/Waveform/WaveformFilterInterface.php
Normal file
20
src/FFMpeg/Filters/Waveform/WaveformFilterInterface.php
Normal 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);
|
||||
}
|
||||
38
src/FFMpeg/Filters/Waveform/WaveformFilters.php
Normal file
38
src/FFMpeg/Filters/Waveform/WaveformFilters.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,6 @@ class WebM extends DefaultVideo
|
|||
*/
|
||||
public function getAvailableVideoCodecs()
|
||||
{
|
||||
return array('libvpx');
|
||||
return array('libvpx', 'libvpx-vp9');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
264
src/FFMpeg/Media/Concat.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
137
src/FFMpeg/Media/Gif.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
163
src/FFMpeg/Media/Waveform.php
Normal file
163
src/FFMpeg/Media/Waveform.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue