Issues (45)

src/Models/Media/Video.php (1 issue)

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
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);
106
        } elseif ($sourceFile != $this->getValue('BlankPath')) {
107
            return static::getImage($this->getValue('BlankPath'));
108
        }
109
110
        return null;
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
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