AbstractVideoGenerator   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 152
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 69
c 2
b 1
f 0
dl 0
loc 152
rs 10
wmc 22

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getFFMpeg() 0 6 1
A getDuration() 0 6 1
A __destruct() 0 9 5
A addTempFileToRemove() 0 3 1
B generate() 0 69 8
A __construct() 0 14 3
A getCutPoints() 0 9 2
A getRatio() 0 6 1
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\Filters\Video\ClipFilter;
13
use FFMpeg\Filters\Video\FrameRateFilter;
14
use FFMpeg\Filters\Video\ResizeFilter;
15
use FFMpeg\Media\Video;
16
use Jackal\Giffhanger\Configuration\Configuration;
17
use Jackal\Giffhanger\Exception\GiffhangerException;
18
use Jackal\Giffhanger\FFMpeg\ext\Filters\CropCenterFilter;
19
20
abstract class AbstractVideoGenerator implements GeneratorInterface
21
{
22
    abstract protected function getVideoFormat();
23
24
    protected $destination;
25
    protected $sourceFile;
26
27
    protected $options = [];
28
    private $tempFilesToRemove = [];
29
30
    public function __construct($sourceFile, $destionationFile, Configuration $options)
31
    {
32
        $this->options = $options;
33
34
        if (!is_dir($this->options->getTempFolder())) {
35
            if (!mkdir($this->options->getTempFolder(), 0777, true)) {
36
                $this->__destruct();
37
38
                throw new \Exception('Cannot create temp folder in path "' . $this->options->getTempFolder() . '"');
39
            }
40
        }
41
42
        $this->sourceFile = $sourceFile;
43
        $this->destination = $destionationFile;
44
    }
45
46
    /**
47
     * @return FFMpeg
48
     */
49
    protected function getFFMpeg() : FFMpeg
50
    {
51
        return FFMpeg::create([
52
            'ffmpeg.binaries' => $this->options->getFFMpegBinaries(),
53
            'ffprobe.binaries' => $this->options->getFFProbeBinaries(),
54
            'timeout' => 3600,
55
        ]);
56
    }
57
58
    protected function getDuration() : int
59
    {
60
        $ffmpeg = $this->getFFMpeg();
61
        $ffmpeg = $ffmpeg->open($this->sourceFile);
62
63
        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...
64
    }
65
66
    protected function getCutPoints() : array
67
    {
68
        $cutPoints = [];
69
        $videoDuration = $this->getDuration();
70
        for ($i = 1;$i <= $this->options->getNumberOfFrames();$i++) {
71
            $cutPoints[] = (($videoDuration / $this->options->getNumberOfFrames()) - ($videoDuration / $this->options->getNumberOfFrames() / 2)) * $i;
72
        }
73
74
        return $cutPoints;
75
    }
76
77
    protected function getRatio() : float
78
    {
79
        $ffmpeg = $this->getFFMpeg();
80
        $ffmpeg = $ffmpeg->open($this->sourceFile);
81
82
        return $ffmpeg->getStreams()->videos()->first()->getDimensions()->getRatio()->getValue();
83
    }
84
85
    public function __destruct()
86
    {
87
        foreach (array_unique($this->tempFilesToRemove) as $fileToRemove) {
88
            if (is_file($fileToRemove)) {
89
                unlink($fileToRemove);
90
            }
91
            //if folder is empty, remove
92
            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

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