Video::writeFile()   C
last analyzed

Complexity

Conditions 13
Paths 131

Size

Total Lines 88
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 13.9057

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 39
c 1
b 0
f 0
nc 131
nop 1
dl 0
loc 88
ccs 33
cts 40
cp 0.825
crap 13.9057
rs 6.3583

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
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\Models\Media;
12
13
use Exception;
14
15
/**
16
 * Video Media Model
17
 *
18
 * @author Henry Paradiz <[email protected]>
19
 * @author Chris Alfano <[email protected]>
20
 *
21
 * {@inheritDoc}
22
 */
23
class Video extends Media
0 ignored issues
show
Bug introduced by
The type Divergence\Models\Media\Media 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...
24
{
25
    // configurables
26
    public static $ExtractFrameCommand = 'avconv -ss %2$u -i %1$s -an -vframes 1 -f mjpeg -'; // 1=video path, 2=position
27
    public static $ExtractFramePosition = 3;
28
    public static $encodingProfiles = [
29
        // from https://www.virag.si/2012/01/web-video-encoding-tutorial-with-ffmpeg-0-9/
30
        'h264-high-480p' => [
31
            'enabled' => true,
32
            'extension' => 'mp4',
33
            'mimeType' => 'video/mp4',
34
            'inputOptions' => [],
35
            'videoCodec' => 'h264',
36
            'videoOptions' => [
37
                'profile:v' => 'high',
38
                'preset' => 'slow',
39
                'b:v' => '500k',
40
                'maxrate' => '500k',
41
                'bufsize' => '1000k',
42
                'vf' => 'scale="trunc(oh*a/2)*2:480"', // http://superuser.com/questions/571141/ffmpeg-avconv-force-scaled-output-to-be-divisible-by-2
43
            ],
44
            'audioCodec' => 'aac',
45
            'audioOptions' => [
46
                'strict' => 'experimental',
47
            ],
48
        ],
49
50
        // from http://superuser.com/questions/556463/converting-video-to-webm-with-ffmpeg-avconv
51
        'webm-480p' => [
52
            'enabled' => true,
53
            'extension' => 'webm',
54
            'mimeType' => 'video/webm',
55
            'inputOptions' => [],
56
            'videoCodec' => 'libvpx',
57
            'videoOptions' => [
58
                'vf' => 'scale=-1:480',
59
            ],
60
            'audioCodec' => 'libvorbis',
61
        ],
62
    ];
63
64
65
66 9
    public function getValue($name)
67
    {
68
        switch ($name) {
69 9
            case 'ThumbnailMIMEType':
70
                return 'image/jpeg';
71
72 9
            case 'Extension':
73
74 9
                switch ($this->getValue('MIMEType')) {
75 9
                    case 'video/x-flv':
76
                        return 'flv';
77
78 9
                    case 'video/mp4':
79 9
                        return 'mp4';
80
81
                    case 'video/quicktime':
82
                        return 'mov';
83
84
                    default:
85
                        throw new Exception('Unable to find video extension for mime-type: '.$this->getValue('MIMEType'));
86
                }
87
88
                // no break
89
            default:
90 9
                return parent::getValue($name);
91
        }
92
    }
93
94
95
    // public methods
96
    public function getImage($sourceFile = null): false|\GdImage
97
    {
98
        if (!isset($sourceFile)) {
99
            $sourceFile = $this->getValue('FilesystemPath') ? $this->getValue('FilesystemPath') : $this->getValue('BlankPath');
100
        }
101
102
        $cmd = sprintf(self::$ExtractFrameCommand, $sourceFile, min(self::$ExtractFramePosition, floor($this->getValue('Duration'))));
103
104
        if ($imageData = shell_exec($cmd)) {
105
            return imagecreatefromstring($imageData);
0 ignored issues
show
Bug Best Practice introduced by
The expression return imagecreatefromstring($imageData) could return the type resource which is incompatible with the type-hinted return GdImage|false. Consider adding an additional type-check to rule them out.
Loading history...
106
        } elseif ($sourceFile != $this->getValue('BlankPath')) {
107
            return static::getImage($this->getValue('BlankPath'));
108
        }
109
110
        return null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return null returns the type null which is incompatible with the type-hinted return GdImage|false.
Loading history...
111
    }
112
113
    /**
114
     * Uses ffprobe to analyze the given file and returns meta data from the first video stream found
115
     *
116
     * @param string $filename
117
     * @param array $mediaInfo
118
     * @return array
119
     */
120 1
    public static function analyzeFile($filename, $mediaInfo = [])
121
    {
122
        // examine media with ffprobe
123 1
        $output = shell_exec("ffprobe -of json -show_streams -v quiet $filename");
124
125 1
        if (!$output || !($json = json_decode($output, true)) || empty($json['streams'])) {
126
            throw new \Exception('Unable to examine video with ffprobe, ensure ffmpeg with ffprobe is installed');
127
        }
128
129
        // extract video streams
130 1
        $videoStreams = array_filter($json['streams'], function ($streamInfo) {
131 1
            return $streamInfo['codec_type'] == 'video';
132 1
        });
133
134 1
        if (!count($videoStreams)) {
135
            throw new Exception('avprobe did not detect any video streams');
136
        }
137
138
        // convert and write interesting information to mediaInfo
139 1
        $mediaInfo['streams'] = $json['streams'];
140 1
        $mediaInfo['videoStream'] = array_shift($videoStreams);
141
142 1
        $mediaInfo['width'] = (int)$mediaInfo['videoStream']['width'];
143 1
        $mediaInfo['height'] = (int)$mediaInfo['videoStream']['height'];
144 1
        $mediaInfo['duration'] = (float)$mediaInfo['videoStream']['duration'];
145
146 1
        return $mediaInfo;
147
    }
148
149 1
    public function writeFile($sourceFile): bool
150
    {
151 1
        parent::writeFile($sourceFile);
152
153
154
        // determine rotation metadata with exiftool
155 1
        $exifToolOutput = exec("exiftool -S -Rotation $this->FilesystemPath");
156
157 1
        if (!$exifToolOutput || !preg_match('/Rotation\s*:\s*(?<rotation>\d+)/', $exifToolOutput, $matches)) {
158
            throw new Exception('Unable to examine video with exiftool, ensure libimage-exiftool-perl is installed on the host system');
159
        }
160
161 1
        $sourceRotation = intval($matches['rotation']);
162
163
164
        // fork encoding job with each configured profile
165 1
        foreach (static::$encodingProfiles as $profileName => $profile) {
166 1
            if (empty($profile['enabled'])) {
167
                continue;
168
            }
169
170
171
            // build paths and create directories if needed
172 1
            $outputPath = $this->getFilesystemPath($profileName);
173 1
            if (!is_dir($outputDir = dirname($outputPath))) {
174 1
                mkdir($outputDir, static::$newDirectoryPermissions, true);
175
            }
176
177 1
            $tmpOutputPath = $outputDir.'/'.'tmp-'.basename($outputPath);
178
            ;
179
180
181
            // build avconv command
182 1
            $cmd = ['avconv', '-loglevel quiet'];
183
184
            // -- input options
185 1
            if (!empty($profile['inputOptions'])) {
186
                static::_appendAvconvOptions($cmd, $profile['inputOptions']);
187
            }
188 1
            $cmd[] = '-i';
189 1
            $cmd[] = $this->FilesystemPath;
190
191
            // -- video output options
192 1
            $cmd[] = '-codec:v';
193 1
            $cmd[] = $profile['videoCodec'];
194 1
            if (!empty($profile['videoOptions'])) {
195 1
                static::_appendAvconvOptions($cmd, $profile['videoOptions']);
196
            }
197
198
            // -- audio output options
199 1
            $cmd[] = '-codec:a';
200 1
            $cmd[] = $profile['audioCodec'];
201 1
            if (!empty($profile['audioOptions'])) {
202 1
                static::_appendAvconvOptions($cmd, $profile['audioOptions']);
203
            }
204
205
            // -- normalize smartphone rotation
206 1
            $cmd[] = '-metadata:s:v rotate="0"';
207
208 1
            if ($sourceRotation == 90) {
209
                $cmd[] = '-vf "transpose=1"';
210 1
            } elseif ($sourceRotation == 180) {
211
                $cmd[] = '-vf "transpose=1,transpose=1"';
212 1
            } elseif ($sourceRotation == 270) {
213
                $cmd[] = '-vf "transpose=1,transpose=1,transpose=1"';
214
            }
215
216
            // -- general output options
217 1
            if (!empty($profile['outputOptions'])) {
218
                static::_appendAvconvOptions($cmd, $profile['outputOptions']);
219
            }
220 1
            $cmd[] = $tmpOutputPath;
221
222
223
            // move to final path after it finished
224 1
            $cmd[] = "&& mv $tmpOutputPath $outputPath";
225
226
227
            // convert command to string and decorate for process control
228 1
            $cmd = '(nohup '.implode(' ', $cmd).') > /dev/null 2>/dev/null & echo $! &';
229
230
231
            // execute command and retrieve the spawned PID
232 1
            $pid = exec($cmd);
0 ignored issues
show
Unused Code introduced by
The assignment to $pid is dead and can be removed.
Loading history...
233
            // TODO: store PID somewhere in APCU cache so we can do something smarter when a video is requested before it's done encoding
234
        }
235
236 1
        return true;
237
    }
238
239 9
    public function getFilesystemPath($variant = 'original', $filename = null): string
240
    {
241 9
        if (!$filename && array_key_exists($variant, static::$encodingProfiles)) {
242 1
            $filename = $this->ID.'.'.static::$encodingProfiles[$variant]['extension'];
243 1
            $variant = 'video-'.$variant;
244
        }
245
246 9
        return parent::getFilesystemPath($variant, $filename);
247
    }
248
249
    public function getMIMEType($variant = 'original'): string
250
    {
251
        if (array_key_exists($variant, static::$encodingProfiles)) {
252
            return static::$encodingProfiles[$variant]['mimeType'];
253
        }
254
255
        return parent::getMIMEType($variant);
256
    }
257
258
    public function isVariantAvailable($variant)
259
    {
260
        if (
261
            array_key_exists($variant, static::$encodingProfiles) &&
262
            !empty(static::$encodingProfiles[$variant]['enabled']) &&
263
            is_readable($this->getFilesystemPath($variant))
264
        ) {
265
            return true;
266
        }
267
268
        return parent::isVariantAvailable($variant);
269
    }
270
271 1
    protected static function _appendAvconvOptions(array &$cmd, array $options)
272
    {
273 1
        foreach ($options as $key => $value) {
274 1
            if (!is_int($key)) {
275 1
                $cmd[] = '-'.$key;
276
            }
277
278 1
            if ($value) {
279 1
                $cmd[] = $value;
280
            }
281
        }
282
    }
283
}
284