Completed
Push — master ( f8ff86...ec22b1 )
by Henry
03:57
created

Video::writeFile()   C

Complexity

Conditions 13
Paths 131

Size

Total Lines 84
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 38
nc 131
nop 1
dl 0
loc 84
rs 6.3583
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Divergence\Models\Media;
3
4
use Exception;
5
6
class Video extends Media
7
{
8
    // configurables
9
    public static $ExtractFrameCommand = 'avconv -ss %2$u -i %1$s -an -vframes 1 -f mjpeg -'; // 1=video path, 2=position
10
    public static $ExtractFramePosition = 3;
11
    public static $encodingProfiles = [
12
        // from https://www.virag.si/2012/01/web-video-encoding-tutorial-with-ffmpeg-0-9/
13
        'h264-high-480p' => [
14
            'enabled' => true,
15
            'extension' => 'mp4',
16
            'mimeType' => 'video/mp4',
17
            'inputOptions' => [],
18
            'videoCodec' => 'h264',
19
            'videoOptions' => [
20
                'profile:v' => 'high',
21
                'preset' => 'slow',
22
                'b:v' => '500k',
23
                'maxrate' => '500k',
24
                'bufsize' => '1000k',
25
                'vf' => 'scale="trunc(oh*a/2)*2:480"', // http://superuser.com/questions/571141/ffmpeg-avconv-force-scaled-output-to-be-divisible-by-2
26
            ],
27
            'audioCodec' => 'aac',
28
            'audioOptions' => [
29
                'strict' => 'experimental',
30
            ],
31
        ],
32
33
        // from http://superuser.com/questions/556463/converting-video-to-webm-with-ffmpeg-avconv
34
        'webm-480p' => [
35
            'enabled' => true,
36
            'extension' => 'webm',
37
            'mimeType' => 'video/webm',
38
            'inputOptions' => [],
39
            'videoCodec' => 'libvpx',
40
            'videoOptions' => [
41
                'vf' => 'scale=-1:480',
42
            ],
43
            'audioCodec' => 'libvorbis',
44
        ],
45
    ];
46
47
48
    // magic methods
49
    public static function __classLoaded()
50
    {
51
        $className = get_called_class();
52
53
        Media::$mimeHandlers['video/x-flv'] = $className;
54
        Media::$mimeHandlers['video/mp4'] = $className;
55
        Media::$mimeHandlers['video/quicktime'] = $className;
56
57
        parent::__classLoaded();
0 ignored issues
show
introduced by
The method __classLoaded() does not exist on Divergence\Models\Media\Media. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

57
        parent::/** @scrutinizer ignore-call */ 
58
                __classLoaded();
Loading history...
58
    }
59
60
61
    public function getValue($name)
62
    {
63
        switch ($name) {
64
            case 'ThumbnailMIMEType':
65
                return 'image/jpeg';
66
67
            case 'Extension':
68
69
                switch ($this->MIMEType) {
70
                    case 'video/x-flv':
71
                        return 'flv';
72
73
                    case 'video/mp4':
74
                        return 'mp4';
75
76
                    case 'video/quicktime':
77
                        return 'mov';
78
79
                    default:
80
                        throw new Exception('Unable to find video extension for mime-type: '.$this->MIMEType);
81
                }
82
83
                // no break
84
            default:
85
                return parent::getValue($name);
86
        }
87
    }
88
89
90
    // public methods
91
    public function getImage($sourceFile = null)
92
    {
93
        if (!isset($sourceFile)) {
94
            $sourceFile = $this->FilesystemPath ? $this->FilesystemPath : $this->BlankPath;
0 ignored issues
show
Bug Best Practice introduced by
The property FilesystemPath does not exist on Divergence\Models\Media\Video. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property BlankPath does not exist on Divergence\Models\Media\Video. Since you implemented __get, consider adding a @property annotation.
Loading history...
95
        }
96
97
        $cmd = sprintf(self::$ExtractFrameCommand, $sourceFile, min(self::$ExtractFramePosition, floor($this->Duration)));
98
99
        if ($imageData = shell_exec($cmd)) {
100
            return imagecreatefromstring($imageData);
101
        } elseif ($sourceFile != $this->BlankPath) {
102
            return static::getImage($this->BlankPath);
0 ignored issues
show
Bug Best Practice introduced by
The method Divergence\Models\Media\Video::getImage() is not static, but was called statically. ( Ignorable by Annotation )

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

102
            return static::/** @scrutinizer ignore-call */ getImage($this->BlankPath);
Loading history...
103
        }
104
105
        return null;
106
    }
107
108
    // static methods
109
    public static function analyzeFile($filename, $mediaInfo = [])
110
    {
111
        // examine media with avprobe
112
        $output = shell_exec("avprobe -of json -show_streams -v quiet $filename");
113
114
        if (!$output || !($output = json_decode($output, true)) || empty($output['streams'])) {
115
            throw new MediaTypeException('Unable to examine video with avprobe, ensure lib-avtools is installed on the host system');
0 ignored issues
show
Bug introduced by
The type Divergence\Models\Media\MediaTypeException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
116
        }
117
118
        // extract video streams
119
        $videoStreams = array_filter($output['streams'], function ($streamInfo) {
120
            return $streamInfo['codec_type'] == 'video';
121
        });
122
123
        if (!count($videoStreams)) {
124
            throw new Exception('avprobe did not detect any video streams');
125
        }
126
127
        // convert and write interesting information to mediaInfo
128
        $mediaInfo['streams'] = $output['streams'];
129
        $mediaInfo['videoStream'] = array_shift($videoStreams);
130
131
        $mediaInfo['width'] = (int)$mediaInfo['videoStream']['width'];
132
        $mediaInfo['height'] = (int)$mediaInfo['videoStream']['height'];
133
        $mediaInfo['duration'] = (double)$mediaInfo['videoStream']['duration'];
134
135
        return $mediaInfo;
136
    }
137
138
    public function writeFile($sourceFile)
139
    {
140
        parent::writeFile($sourceFile);
141
142
143
        // determine rotation metadata with exiftool
144
        $exifToolOutput = exec("exiftool -S -Rotation $this->FilesystemPath");
0 ignored issues
show
Bug Best Practice introduced by
The property FilesystemPath does not exist on Divergence\Models\Media\Video. Since you implemented __get, consider adding a @property annotation.
Loading history...
145
146
        if (!$exifToolOutput || !preg_match('/Rotation\s*:\s*(?<rotation>\d+)/', $exifToolOutput, $matches)) {
147
            throw new Exception('Unable to examine video with exiftool, ensure libimage-exiftool-perl is installed on the host system');
148
        }
149
150
        $sourceRotation = intval($matches['rotation']);
151
152
153
        // fork encoding job with each configured profile
154
        foreach (static::$encodingProfiles as $profileName => $profile) {
155
            if (empty($profile['enabled'])) {
156
                continue;
157
            }
158
159
160
            // build paths and create directories if needed
161
            $outputPath = $this->getFilesystemPath($profileName);
162
            if (!is_dir($outputDir = dirname($outputPath))) {
163
                mkdir($outputDir, static::$newDirectoryPermissions, true);
164
            }
165
166
            $tmpOutputPath = $outputDir.'/'.'tmp-'.basename($outputPath);
167
            ;
168
169
170
            // build avconv command
171
            $cmd = ['avconv', '-loglevel quiet'];
172
173
            // -- input options
174
            if (!empty($profile['inputOptions'])) {
175
                static::_appendAvconvOptions($cmd, $profile['inputOptions']);
176
            }
177
            $cmd[] = '-i';
178
            $cmd[] = $this->FilesystemPath;
179
180
            // -- video output options
181
            $cmd[] = '-codec:v';
182
            $cmd[] = $profile['videoCodec'];
183
            if (!empty($profile['videoOptions'])) {
184
                static::_appendAvconvOptions($cmd, $profile['videoOptions']);
185
            }
186
187
            // -- audio output options
188
            $cmd[] = '-codec:a';
189
            $cmd[] = $profile['audioCodec'];
190
            if (!empty($profile['audioOptions'])) {
191
                static::_appendAvconvOptions($cmd, $profile['audioOptions']);
192
            }
193
194
            // -- normalize smartphone rotation
195
            $cmd[] = '-metadata:s:v rotate="0"';
196
197
            if ($sourceRotation == 90) {
198
                $cmd[] = '-vf "transpose=1"';
199
            } elseif ($sourceRotation == 180) {
200
                $cmd[] = '-vf "transpose=1,transpose=1"';
201
            } elseif ($sourceRotation == 270) {
202
                $cmd[] = '-vf "transpose=1,transpose=1,transpose=1"';
203
            }
204
205
            // -- general output options
206
            if (!empty($profile['outputOptions'])) {
207
                static::_appendAvconvOptions($cmd, $profile['outputOptions']);
208
            }
209
            $cmd[] = $tmpOutputPath;
210
211
212
            // move to final path after it finished
213
            $cmd[] = "&& mv $tmpOutputPath $outputPath";
214
215
216
            // convert command to string and decorate for process control
217
            $cmd = '(nohup '.implode(' ', $cmd).') > /dev/null 2>/dev/null & echo $! &';
218
219
220
            // execute command and retrieve the spawned PID
221
            $pid = exec($cmd);
0 ignored issues
show
Unused Code introduced by
The assignment to $pid is dead and can be removed.
Loading history...
222
            // TODO: store PID somewhere in APCU cache so we can do something smarter when a video is requested before it's done encoding
223
        }
224
    }
225
226
    public function getFilesystemPath($variant = 'original', $filename = null)
227
    {
228
        if (!$filename && array_key_exists($variant, static::$encodingProfiles)) {
229
            $filename = $this->ID.'.'.static::$encodingProfiles[$variant]['extension'];
230
            $variant = 'video-'.$variant;
231
        }
232
233
        return parent::getFilesystemPath($variant, $filename);
234
    }
235
236
    public function getMIMEType($variant = 'original')
237
    {
238
        if (array_key_exists($variant, static::$encodingProfiles)) {
239
            return static::$encodingProfiles[$variant]['mimeType'];
240
        }
241
242
        return parent::getMIMEType($variant, $filename);
0 ignored issues
show
Unused Code introduced by
The call to Divergence\Models\Media\Media::getMIMEType() has too many arguments starting with $filename. ( Ignorable by Annotation )

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

242
        return parent::/** @scrutinizer ignore-call */ getMIMEType($variant, $filename);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Comprehensibility Best Practice introduced by
The variable $filename seems to be never defined.
Loading history...
243
    }
244
245
    public function isVariantAvailable($variant)
246
    {
247
        if (
248
            array_key_exists($variant, static::$encodingProfiles) &&
249
            !empty(static::$encodingProfiles[$variant]['enabled']) &&
250
            is_readable($this->getFilesystemPath($variant))
251
        ) {
252
            return true;
253
        }
254
255
        return parent::isVariantAvailable($variant);
256
    }
257
258
    protected static function _appendAvconvOptions(array &$cmd, array $options)
259
    {
260
        foreach ($options as $key => $value) {
261
            if (!is_int($key)) {
262
                $cmd[] = '-'.$key;
263
            }
264
265
            if ($value) {
266
                $cmd[] = $value;
267
            }
268
        }
269
    }
270
}
271