Completed
Push — master ( f4ebcb...ba1f71 )
by Henry
02:12
created

Video::getValue()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nc 6
nop 1
dl 0
loc 25
rs 9.2222
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * @author Henry Paradiz <[email protected]>
6
 * @copyright 2018 Henry Paradiz <[email protected]>
7
 * @license MIT For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
8
 *
9
 * @since 1.1
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
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
    public function getValue($name)
67
    {
68
        switch ($name) {
69
            case 'ThumbnailMIMEType':
70
                return 'image/jpeg';
71
72
            case 'Extension':
73
74
                switch ($this->MIMEType) {
75
                    case 'video/x-flv':
76
                        return 'flv';
77
78
                    case 'video/mp4':
79
                        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->MIMEType);
86
                }
87
88
                // no break
89
            default:
90
                return parent::getValue($name);
91
        }
92
    }
93
94
95
    // public methods
96
    public function getImage($sourceFile = null)
97
    {
98
        if (!isset($sourceFile)) {
99
            $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...
100
        }
101
102
        $cmd = sprintf(self::$ExtractFrameCommand, $sourceFile, min(self::$ExtractFramePosition, floor($this->Duration)));
103
104
        if ($imageData = shell_exec($cmd)) {
105
            return imagecreatefromstring($imageData);
106
        } elseif ($sourceFile != $this->BlankPath) {
107
            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

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

247
        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...
248
    }
249
250
    public function isVariantAvailable($variant)
251
    {
252
        if (
253
            array_key_exists($variant, static::$encodingProfiles) &&
254
            !empty(static::$encodingProfiles[$variant]['enabled']) &&
255
            is_readable($this->getFilesystemPath($variant))
256
        ) {
257
            return true;
258
        }
259
260
        return parent::isVariantAvailable($variant);
261
    }
262
263
    protected static function _appendAvconvOptions(array &$cmd, array $options)
264
    {
265
        foreach ($options as $key => $value) {
266
            if (!is_int($key)) {
267
                $cmd[] = '-'.$key;
268
            }
269
270
            if ($value) {
271
                $cmd[] = $value;
272
            }
273
        }
274
    }
275
}
276