scratch – Rev 115

Subversion Repositories:
Rev:
<?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;
    }
}