Passed
Push — master ( b52452...60ff6d )
by Luca
04:00 queued 01:57
created

AbstractVideoGenerator::getFFMpeg()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 4
c 1
b 1
f 0
nc 1
nop 0
dl 0
loc 6
rs 10
1
<?php
2
3
namespace Jackal\Giffhanger\Generator;
4
5
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
6
use FFMpeg\Coordinate\Dimension;
7
use FFMpeg\Coordinate\FrameRate;
8
use FFMpeg\Coordinate\TimeCode;
9
use FFMpeg\Exception\RuntimeException;
10
use FFMpeg\FFMpeg;
11
use FFMpeg\Filters\Audio\SimpleFilter;
12
use FFMpeg\Media\Video;
13
use Jackal\Giffhanger\Configuration\Configuration;
14
use Jackal\Giffhanger\Exception\GiffhangerException;
15
use Jackal\Giffhanger\FFMpeg\ext\Filters\CropCenterFilter;
16
17
abstract class AbstractVideoGenerator implements GeneratorInterface
18
{
19
    abstract protected function getVideoFormat();
20
21
    protected $destination;
22
    protected $sourceFile;
23
24
    protected $options = [];
25
    private $tempFilesToRemove = [];
26
27
    public function __construct($sourceFile, $destionationFile, Configuration $options)
28
    {
29
        $this->options = $options;
30
31
        if (!is_dir($this->options->getTempFolder())) {
32
            if (!mkdir($this->options->getTempFolder(), 0777, true)) {
33
                $this->__destruct();
34
35
                throw new \Exception('Cannot create temp folder in path "' . $this->options->getTempFolder() . '"');
36
            }
37
        }
38
39
        $this->sourceFile = $sourceFile;
40
        $this->destination = $destionationFile;
41
    }
42
43
    /**
44
     * @return FFMpeg
45
     */
46
    protected function getFFMpeg() : FFMpeg
47
    {
48
        return FFMpeg::create([
49
            'ffmpeg.binaries' => $this->options->getFFMpegBinaries(),
50
            'ffprobe.binaries' => $this->options->getFFProbeBinaries(),
51
            'timeout' => 3600,
52
        ]);
53
    }
54
55
    protected function getDuration() : int
56
    {
57
        $ffmpeg = $this->getFFMpeg();
58
        $ffmpeg = $ffmpeg->open($this->sourceFile);
59
60
        return $ffmpeg->getFFProbe()->format($this->sourceFile)->get('duration');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ffmpeg->getFFPro...eFile)->get('duration') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
61
    }
62
63
    protected function getCutPoints() : array
64
    {
65
        $cutPoints = [];
66
        $videoDuration = $this->getDuration();
67
        for ($i = 1;$i <= $this->options->getNumberOfFrames();$i++) {
68
            $cutPoints[] = (($videoDuration / $this->options->getNumberOfFrames()) - ($videoDuration / $this->options->getNumberOfFrames() / 2)) * $i;
69
        }
70
71
        return $cutPoints;
72
    }
73
74
    protected function getRatio() : float
75
    {
76
        $ffmpeg = $this->getFFMpeg();
77
        $ffmpeg = $ffmpeg->open($this->sourceFile);
78
79
        return $ffmpeg->getStreams()->first()->getDimensions()->getRatio()->getValue();
80
    }
81
82
    public function __destruct()
83
    {
84
        foreach (array_unique($this->tempFilesToRemove) as $fileToRemove) {
85
            if (is_file($fileToRemove)) {
86
                unlink($fileToRemove);
87
            }
88
            //if folder is empty, remove
89
            if (is_dir(dirname($fileToRemove)) and count(scandir(dirname($fileToRemove))) == 2) {
0 ignored issues
show
Bug introduced by
It seems like scandir(dirname($fileToRemove)) can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
            if (is_dir(dirname($fileToRemove)) and count(/** @scrutinizer ignore-type */ scandir(dirname($fileToRemove))) == 2) {
Loading history...
90
                rmdir(dirname($fileToRemove));
91
            }
92
        }
93
    }
94
95
    protected function addTempFileToRemove($filePath) : void
96
    {
97
        $this->tempFilesToRemove[] = $filePath;
98
    }
99
100
    public function generate() : void
101
    {
102
        $ffmpeg = $this->getFFMpeg();
103
        $videoFormat = $this->getVideoFormat();
104
105
        try {
106
            $cutPoints = $this->getCutPoints();
107
108
            /** @var Video $video */
109
            $video = $ffmpeg->open($this->sourceFile);
110
111
            $files = [];
112
            foreach ($cutPoints as $k => $cutPoint) {
113
                $partFile = $this->options->getTempFolder() . '/' . md5($this->sourceFile) . '_' . ($k + 1) . '.avi';
114
115
                //remove audio track
116
                $video->addFilter(new SimpleFilter(['-an']));
117
118
                $video->filters()->clip(
119
                    TimeCode::fromSeconds($cutPoint),
120
                    TimeCode::fromSeconds($this->options->getOutputDuration() / count($cutPoints))
121
                );
122
123
                $video->filters()->resize(
124
                    new Dimension(
125
                        (int) round($this->options->getDimensionWidth()),
126
                        (int) round($this->options->getDimensionWidth() / $this->getRatio())
127
                    )
128
                );
129
                $video->filters()->framerate(new FrameRate($this->options->getFrameRate()), 1);
130
                $video->save($videoFormat, $partFile);
131
132
                $files[] = $partFile;
133
                $this->addTempFileToRemove($partFile);
134
            }
135
136
            if (count($files) == 1) {
137
                rename($files[0], $this->destination);
138
            } else {
139
                /** @var Video $video */
140
                $video = $ffmpeg->open($files[0]);
141
142
                if (is_file($this->destination)) {
143
                    unlink($this->destination);
144
                }
145
146
                $video
147
                    ->concat($files)
148
                    ->saveFromSameCodecs($this->destination);
149
            }
150
151
            if ($this->options->getCropRatio() and ($this->options->getCropRatio() != $this->getRatio())) {
152
                $fileCropped = $this->options->getTempFolder() . '/' . md5($this->sourceFile) . '_cropped.avi';
153
154
                $video = $ffmpeg->open($this->destination);
155
                $video->addFilter(new CropCenterFilter($this->options->getCropRatio()));
156
                $video->save($videoFormat, $fileCropped);
157
                rename($fileCropped, $this->destination);
158
            }
159
        } catch (RuntimeException $e) {
160
            $message = $e->getMessage();
161
162
            if ($e->getPrevious() instanceof ExecutionFailureException) {
163
                $message = $e->getPrevious()->getMessage();
164
            }
165
166
            throw new GiffhangerException(sprintf('%s', $message));
167
        }
168
    }
169
}
170