Passed
Branch v1 (0c7460)
by Andrew
06:14
created

Transcode::getVideoThumbnailUrl()   B

Complexity

Conditions 8
Paths 31

Size

Total Lines 69
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 38
nc 31
nop 4
dl 0
loc 69
rs 8.0675
c 0
b 0
f 0

How to fix   Long Method   

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
 * Transcoder plugin for Craft CMS 3.x
4
 *
5
 * Transcode videos to various formats, and provide thumbnails of the video
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\transcoder\services;
12
13
use nystudio107\transcoder\Transcoder;
14
15
use Craft;
16
use craft\base\Component;
17
use craft\elements\Asset;
18
use craft\events\AssetThumbEvent;
19
use craft\helpers\FileHelper;
20
use craft\helpers\Json as JsonHelper;
21
use craft\volumes\Local;
22
23
use yii\base\Exception;
24
use yii\validators\UrlValidator;
25
26
use mikehaertl\shellcommand\Command as ShellCommand;
27
use yii\base\InvalidConfigException;
28
29
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
30
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 2 spaces but found 4
Loading history...
31
 * @package   Transcode
0 ignored issues
show
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 3
Loading history...
32
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 3 spaces but found 5
Loading history...
33
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
34
class Transcode extends Component
35
{
36
    // Constants
37
    // =========================================================================
38
39
    // Suffixes to add to the generated filename params
40
    const SUFFIX_MAP = [
41
        'videoFrameRate' => 'fps',
42
        'videoBitRate' => 'bps',
43
        'audioBitRate' => 'bps',
44
        'audioChannels' => 'c',
45
        'height' => 'h',
46
        'width' => 'w',
47
        'timeInSecs' => 's',
48
    ];
49
50
    // Params that should be excluded from being part of the generated filename
51
    const EXCLUDE_PARAMS = [
52
        'videoEncoder',
53
        'audioEncoder',
54
        'fileSuffix',
55
        'sharpen',
56
    ];
57
58
    // Mappings for getFileInfo() summary values
59
    const INFO_SUMMARY = [
60
        'format' => [
61
            'filename' => 'filename',
62
            'duration' => 'duration',
63
            'size' => 'size',
64
        ],
65
        'audio' => [
66
            'codec_name' => 'audioEncoder',
67
            'bit_rate' => 'audioBitRate',
68
            'sample_rate' => 'audioSampleRate',
69
            'channels' => 'audioChannels',
70
        ],
71
        'video' => [
72
            'codec_name' => 'videoEncoder',
73
            'bit_rate' => 'videoBitRate',
74
            'avg_frame_rate' => 'videoFrameRate',
75
            'height' => 'height',
76
            'width' => 'width',
77
        ],
78
    ];
79
80
    // Public Methods
81
    // =========================================================================
82
83
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $videoOptions should have a doc-comment as per coding-style.
Loading history...
84
     * Returns a URL to the transcoded video or "" if it doesn't exist (at which
85
     * time it will create it).
86
     *
87
     * @param $filePath     string  path to the original video -OR- an Asset
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
88
     * @param $videoOptions array   of options for the video
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
89
     *
90
     * @return string       URL of the transcoded video or ""
91
     */
92
    public function getVideoUrl($filePath, $videoOptions): string
93
    {
94
        $result = '';
95
        $settings = Transcoder::$plugin->getSettings();
96
        $filePath = $this->getAssetPath($filePath);
97
98
        if (!empty($filePath)) {
99
            $destVideoPath = $settings['transcoderPaths']['video'] ?? $settings['transcoderPaths']['default'];
100
            $destVideoPath = Craft::getAlias($destVideoPath);
101
            $videoOptions = $this->coalesceOptions('defaultVideoOptions', $videoOptions);
102
103
            // Get the video encoder presets to use
104
            $videoEncoders = $settings['videoEncoders'];
105
            $thisEncoder = $videoEncoders[$videoOptions['videoEncoder']];
106
107
            $videoOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
108
109
            // Build the basic command for ffmpeg
110
            $ffmpegCmd = $settings['ffmpegPath']
111
                .' -i '.escapeshellarg($filePath)
112
                .' -vcodec '.$thisEncoder['videoCodec']
113
                .' '.$thisEncoder['videoCodecOptions']
114
                .' -bufsize 1000k'
115
                .' -threads 0';
116
117
            // Set the framerate if desired
118
            if (!empty($videoOptions['videoFrameRate'])) {
119
                $ffmpegCmd .= ' -r '.$videoOptions['videoFrameRate'];
120
            }
121
122
            // Set the bitrate if desired
123
            if (!empty($videoOptions['videoBitRate'])) {
124
                $ffmpegCmd .= ' -b:v '.$videoOptions['videoBitRate'].' -maxrate '.$videoOptions['videoBitRate'];
125
            }
126
127
            // Adjust the scaling if desired
128
            $ffmpegCmd = $this->addScalingFfmpegArgs(
129
                $videoOptions,
130
                $ffmpegCmd
131
            );
132
133
            // Handle any audio transcoding
134
            if (empty($videoOptions['audioBitRate'])
135
                && empty($videoOptions['audioSampleRate'])
136
                && empty($videoOptions['audioChannels'])
137
            ) {
138
                // Just copy the audio if no options are provided
139
                $ffmpegCmd .= ' -c:a copy';
140
            } else {
141
                // Do audio transcoding based on the settings
142
                $ffmpegCmd .= ' -acodec '.$thisEncoder['audioCodec'];
143
                if (!empty($videoOptions['audioBitRate'])) {
144
                    $ffmpegCmd .= ' -b:a '.$videoOptions['audioBitRate'];
145
                }
146
                if (!empty($videoOptions['audioSampleRate'])) {
147
                    $ffmpegCmd .= ' -ar '.$videoOptions['audioSampleRate'];
148
                }
149
                if (!empty($videoOptions['audioChannels'])) {
150
                    $ffmpegCmd .= ' -ac '.$videoOptions['audioChannels'];
151
                }
152
                $ffmpegCmd .= ' '.$thisEncoder['audioCodecOptions'];
153
            }
154
155
            // Create the directory if it isn't there already
156
            if (!is_dir($destVideoPath)) {
157
                try {
158
                    FileHelper::createDirectory($destVideoPath);
159
                } catch (Exception $e) {
160
                    Craft::error($e->getMessage(), __METHOD__);
161
                }
162
            }
163
164
            $destVideoFile = $this->getFilename($filePath, $videoOptions);
165
166
            // File to store the video encoding progress in
167
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.progress';
168
169
            // Assemble the destination path and final ffmpeg command
170
            $destVideoPath .= $destVideoFile;
171
            $ffmpegCmd .= ' -f '
172
                .$thisEncoder['fileFormat']
173
                .' -y '.escapeshellarg($destVideoPath)
174
                .' 1> '.$progressFile.' 2>&1 & echo $!';
175
176
            // Make sure there isn't a lockfile for this video already
177
            $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.lock';
178
            $oldPid = @file_get_contents($lockFile);
179
            if ($oldPid !== false) {
180
                exec("ps $oldPid", $ProcessState);
181
                if (\count($ProcessState) >= 2) {
182
                    return $result;
183
                }
184
                // It's finished transcoding, so delete the lockfile and progress file
185
                @unlink($lockFile);
186
                @unlink($progressFile);
187
            }
188
189
            // If the video file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
190
            if (file_exists($destVideoPath) && (filemtime($destVideoPath) >= filemtime($filePath))) {
191
                $url = $settings['transcoderUrls']['video'] ?? $settings['transcoderUrls']['default'];
192
                $result = Craft::getAlias($url).$destVideoFile;
193
            } else {
194
                // Kick off the transcoding
195
                $pid = $this->executeShellCommand($ffmpegCmd);
196
                Craft::info($ffmpegCmd."\nffmpeg PID: ".$pid, __METHOD__);
197
198
                // Create a lockfile in tmp
199
                file_put_contents($lockFile, $pid);
200
            }
201
        }
202
203
        return $result;
204
    }
205
206
    /**
207
     * Returns a URL to a video thumbnail
208
     *
209
     * @param string $filePath         path to the original video or an Asset
210
     * @param array  $thumbnailOptions of options for the thumbnail
211
     * @param bool   $generate         whether the thumbnail should be
212
     *                                 generated if it doesn't exists
213
     * @param bool   $asPath           Whether we should return a path or not
214
     *
215
     * @return string|false|null URL or path of the video thumbnail
216
     */
217
    public function getVideoThumbnailUrl($filePath, $thumbnailOptions, $generate = true, $asPath = false)
218
    {
219
220
        $result = null;
221
        $settings = Transcoder::$plugin->getSettings();
222
        $filePath = $this->getAssetPath($filePath);
223
224
        if (!empty($filePath)) {
225
            $destThumbnailPath = $settings['transcoderPaths']['thumbnail'] ?? $settings['transcoderPaths']['default'];
226
            $destThumbnailPath = Craft::getAlias($destThumbnailPath);
227
228
            $thumbnailOptions = $this->coalesceOptions('defaultThumbnailOptions', $thumbnailOptions);
229
230
            // Build the basic command for ffmpeg
231
            $ffmpegCmd = $settings['ffmpegPath']
232
                .' -i '.escapeshellarg($filePath)
233
                .' -vcodec mjpeg'
234
                .' -vframes 1';
235
236
            // Adjust the scaling if desired
237
            $ffmpegCmd = $this->addScalingFfmpegArgs(
238
                $thumbnailOptions,
239
                $ffmpegCmd
240
            );
241
242
            // Set the timecode to get the thumbnail from if desired
243
            if (!empty($thumbnailOptions['timeInSecs'])) {
244
                $timeCode = gmdate('H:i:s', $thumbnailOptions['timeInSecs']);
245
                $ffmpegCmd .= ' -ss '.$timeCode.'.00';
246
            }
247
248
            // Create the directory if it isn't there already
249
            if (!is_dir($destThumbnailPath)) {
250
                try {
251
                    FileHelper::createDirectory($destThumbnailPath);
252
                } catch (Exception $e) {
253
                    Craft::error($e->getMessage(), __METHOD__);
254
                }
255
            }
256
257
            $destThumbnailFile = $this->getFilename($filePath, $thumbnailOptions);
258
259
            // Assemble the destination path and final ffmpeg command
260
            $destThumbnailPath .= $destThumbnailFile;
261
            $ffmpegCmd .= ' -f image2 -y '.escapeshellarg($destThumbnailPath).' >/dev/null 2>/dev/null &';
262
263
            // If the thumbnail file already exists, return it.  Otherwise, generate it and return it
264
            if (!file_exists($destThumbnailPath)) {
265
                if ($generate) {
266
                    /** @noinspection PhpUnusedLocalVariableInspection */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
267
                    $shellOutput = $this->executeShellCommand($ffmpegCmd);
268
                    Craft::info($ffmpegCmd, __METHOD__);
269
                } else {
270
                    Craft::info('Thumbnail does not exist, but not asked to generate it: '.$filePath, __METHOD__);
271
272
                    // The file doesn't exist, and we weren't asked to generate it
273
                    return false;
274
                }
275
            }
276
            // Return either a path or a URL
277
            if ($asPath) {
278
                $result = $destThumbnailPath;
279
            } else {
280
                $url = $settings['transcoderUrls']['thumbnail'] ?? $settings['transcoderUrls']['default'];
281
                $result = Craft::getAlias($url).$destThumbnailFile;
282
            }
283
        }
284
285
        return $result;
286
    }
287
288
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $audioOptions should have a doc-comment as per coding-style.
Loading history...
289
     * Returns a URL to the transcoded audio file or "" if it doesn't exist
290
     * (at which time it will create it).
291
     *
292
     * @param $filePath     string path to the original audio file -OR- an Asset
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
293
     * @param $audioOptions array of options for the audio file
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
294
     *
295
     * @return string       URL of the transcoded audio file or ""
296
     */
297
    public function getAudioUrl($filePath, $audioOptions): string
298
    {
299
300
        $result = '';
301
        $settings = Transcoder::$plugin->getSettings();
302
        $filePath = $this->getAssetPath($filePath);
303
304
        if (!empty($filePath)) {
305
            $destAudioPath = $settings['transcoderPaths']['audio'] ?? $settings['transcoderPaths']['default'];
306
            $destAudioPath = Craft::getAlias($destAudioPath);
307
308
            $audioOptions = $this->coalesceOptions('defaultAudioOptions', $audioOptions);
309
310
            // Get the audio encoder presets to use
311
            $audioEncoders = $settings['audioEncoders'];
312
            $thisEncoder = $audioEncoders[$audioOptions['audioEncoder']];
313
314
            $audioOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
315
316
            // Build the basic command for ffmpeg
317
            $ffmpegCmd = $settings['ffmpegPath']
318
                .' -i '.escapeshellarg($filePath)
319
                .' -acodec '.$thisEncoder['audioCodec']
320
                .' '.$thisEncoder['audioCodecOptions']
321
                .' -bufsize 1000k'
322
                .' -threads 0';
323
324
            // Set the bitrate if desired
325
            if (!empty($audioOptions['audioBitRate'])) {
326
                $ffmpegCmd .= ' -b:a '.$audioOptions['audioBitRate'];
327
            }
328
            // Set the sample rate if desired
329
            if (!empty($audioOptions['audioSampleRate'])) {
330
                $ffmpegCmd .= ' -ar '.$audioOptions['audioSampleRate'];
331
            }
332
            // Set the audio channels if desired
333
            if (!empty($audioOptions['audioChannels'])) {
334
                $ffmpegCmd .= ' -ac '.$audioOptions['audioChannels'];
335
            }
336
            $ffmpegCmd .= ' '.$thisEncoder['audioCodecOptions'];
337
338
339
            // Create the directory if it isn't there already
340
            if (!is_dir($destAudioPath)) {
341
                try {
342
                    FileHelper::createDirectory($destAudioPath);
343
                } catch (Exception $e) {
344
                    Craft::error($e->getMessage(), __METHOD__);
345
                }
346
            }
347
348
            $destAudioFile = $this->getFilename($filePath, $audioOptions);
349
350
            // File to store the audio encoding progress in
351
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destAudioFile.'.progress';
352
353
            // Assemble the destination path and final ffmpeg command
354
            $destAudioPath .= $destAudioFile;
355
            $ffmpegCmd .= ' -f '
356
                .$thisEncoder['fileFormat']
357
                .' -y '.escapeshellarg($destAudioPath)
358
                .' 1> '.$progressFile.' 2>&1 & echo $!';
359
360
            // Make sure there isn't a lockfile for this audio file already
361
            $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destAudioFile.'.lock';
362
            $oldPid = @file_get_contents($lockFile);
363
            if ($oldPid !== false) {
364
                exec("ps $oldPid", $ProcessState);
365
                if (\count($ProcessState) >= 2) {
366
                    return $result;
367
                }
368
                // It's finished transcoding, so delete the lockfile and progress file
369
                @unlink($lockFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

369
                /** @scrutinizer ignore-unhandled */ @unlink($lockFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
370
                @unlink($progressFile);
371
            }
372
373
            // If the audio file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
374
            if (file_exists($destAudioPath) && (filemtime($destAudioPath) >= filemtime($filePath))) {
375
                $url = $settings['transcoderUrls']['audio'] ?? $settings['transcoderUrls']['default'];
376
                $result = Craft::getAlias($url).$destAudioFile;
377
            } else {
378
                // Kick off the transcoding
379
                $pid = $this->executeShellCommand($ffmpegCmd);
380
                Craft::info($ffmpegCmd."\nffmpeg PID: ".$pid, __METHOD__);
381
382
                // Create a lockfile in tmp
383
                file_put_contents($lockFile, $pid);
384
            }
385
        }
386
387
        return $result;
388
    }
389
390
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
391
     * Extract information from a video/audio file
392
     *
393
     * @param      $filePath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 6
Loading history...
394
     * @param bool $summary
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 5 spaces after parameter type; 1 found
Loading history...
395
     *
396
     * @return array
397
     */
398
    public function getFileInfo($filePath, $summary = false): array
399
    {
400
401
        $result = null;
402
        $settings = Transcoder::$plugin->getSettings();
403
        $filePath = $this->getAssetPath($filePath);
404
405
        if (!empty($filePath)) {
406
            // Build the basic command for ffprobe
407
            $ffprobeOptions = $settings['ffprobeOptions'];
408
            $ffprobeCmd = $settings['ffprobePath']
409
                .' '.$ffprobeOptions
410
                .' '.escapeshellarg($filePath);
411
412
            $shellOutput = $this->executeShellCommand($ffprobeCmd);
413
            Craft::info($ffprobeCmd, __METHOD__);
414
            $result = JsonHelper::decodeIfJson($shellOutput, true);
415
            Craft::info(print_r($result, true), __METHOD__);
416
417
            // Trim down the arrays to just a summary
418
            if ($summary && !empty($result)) {
419
                $summaryResult = [];
420
                foreach ($result as $topLevelKey => $topLevelValue) {
421
                    switch ($topLevelKey) {
422
                        // Format info
423
                        case 'format':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
424
                            foreach (self::INFO_SUMMARY['format'] as $settingKey => $settingValue) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
425
                                if (!empty($topLevelValue[$settingKey])) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
426
                                    $summaryResult[$settingValue] = $topLevelValue[$settingKey];
427
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
428
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
429
                            break;
430
                        // Stream info
431
                        case 'streams':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
432
                            foreach ($topLevelValue as $stream) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
433
                                $infoSummaryType = $stream['codec_type'];
434
                                foreach (self::INFO_SUMMARY[$infoSummaryType] as $settingKey => $settingValue) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
435
                                    if (!empty($stream[$settingKey])) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 32 spaces, found 36
Loading history...
436
                                        $summaryResult[$settingValue] = $stream[$settingKey];
437
                                    }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 32 spaces, found 36
Loading history...
438
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
439
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
440
                            break;
441
                        // Unknown info
442
                        default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
443
                            break;
444
                    }
445
                }
446
                // Handle cases where the framerate is returned as XX/YY
447
                if (!empty($summaryResult['videoFrameRate'])
448
                    && (strpos($summaryResult['videoFrameRate'], '/') !== false)
449
                ) {
450
                    $parts = explode('/', $summaryResult['videoFrameRate']);
451
                    $summaryResult['videoFrameRate'] = (float)$parts[0] / (float)$parts[1];
452
                }
453
                $result = $summaryResult;
454
            }
455
        }
456
457
        return $result;
458
    }
459
460
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $videoOptions should have a doc-comment as per coding-style.
Loading history...
461
     * Get the name of a video file from a path and options
462
     *
463
     * @param $filePath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
464
     * @param $videoOptions
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
465
     *
466
     * @return string
467
     */
468
    public function getVideoFilename($filePath, $videoOptions): string
469
    {
470
        $settings = Transcoder::$plugin->getSettings();
471
        $videoOptions = $this->coalesceOptions('defaultVideoOptions', $videoOptions);
472
473
        // Get the video encoder presets to use
474
        $videoEncoders = $settings['videoEncoders'];
475
        $thisEncoder = $videoEncoders[$videoOptions['videoEncoder']];
476
477
        $videoOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
478
479
        return $this->getFilename($filePath, $videoOptions);
480
    }
481
482
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $audioOptions should have a doc-comment as per coding-style.
Loading history...
483
     * Get the name of an audio file from a path and options
484
     *
485
     * @param $filePath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
486
     * @param $audioOptions
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
487
     *
488
     * @return string
489
     */
490
    public function getAudioFilename($filePath, $audioOptions): string
491
    {
492
        $settings = Transcoder::$plugin->getSettings();
493
        $audioOptions = $this->coalesceOptions('defaultAudioOptions', $audioOptions);
494
495
        // Get the video encoder presets to use
496
        $audioEncoders = $settings['audioEncoders'];
497
        $thisEncoder = $audioEncoders[$audioOptions['audioEncoder']];
498
499
        $audioOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
500
501
        return $this->getFilename($filePath, $audioOptions);
502
    }
503
504
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $gifOptions should have a doc-comment as per coding-style.
Loading history...
505
     * Get the name of a gif video file from a path and options
506
     *
507
     * @param $filePath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
508
     * @param $gifOptions
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
509
     *
510
     * @return string
511
     */
512
    public function getGifFilename($filePath, $gifOptions): string
513
    {
514
        $settings = Transcoder::$plugin->getSettings();
515
        $gifOptions = $this->coalesceOptions('defaultGifOptions', $gifOptions);
516
517
        // Get the video encoder presets to use
518
        $videoEncoders = $settings['videoEncoders'];
519
        $thisEncoder = $videoEncoders[$gifOptions['videoEncoder']];
520
521
        $gifOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
522
523
        return $this->getFilename($filePath, $gifOptions);
524
    }
525
526
    /**
527
     * Handle generated a thumbnail for the AdminCP
528
     *
529
     * @param AssetThumbEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
530
     *
531
     * @return null|false|string
532
     */
533
    public function handleGetAssetThumbPath(AssetThumbEvent $event)
534
    {
535
        $options = [
536
            'width' => $event->width,
537
            'height' => $event->height,
538
        ];
539
        $thumbPath = $this->getVideoThumbnailUrl($event->asset, $options, $event->generate, true);
540
541
        return $thumbPath;
542
    }
543
544
    // Protected Methods
545
    // =========================================================================
546
547
    /**
548
     * Returns a URL to a encoded GIF file (mp4)
549
     *
550
     * @param string $filePath         path to the original video or an Asset
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 9 found
Loading history...
551
     * @param array  $gifOptions       of options for the GIF file
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 7 found
Loading history...
552
     *
553
     * @return string|false|null URL or path of the GIF file
554
     */
0 ignored issues
show
Coding Style introduced by
There must be no blank lines after the function comment
Loading history...
555
556
    public function getGifUrl($filePath, $gifOptions): string
557
    {
558
        $result = '';
559
        $settings = Transcoder::$plugin->getSettings();
560
        $filePath = $this->getAssetPath($filePath);
561
562
        if (!empty($filePath)) {
563
            // Dest path
564
            $destVideoPath = $settings['transcoderPaths']['gif'] ?? $settings['transcoderPaths']['default'];
565
            $destVideoPath = Craft::getAlias($destVideoPath);
566
567
            // Options
568
            $gifOptions = $this->coalesceOptions('defaultGifOptions', $gifOptions);
569
570
            // Get the video encoder presets to use
571
            $videoEncoders = $settings['videoEncoders'];
572
            $thisEncoder = $videoEncoders[$gifOptions['videoEncoder']];
573
            $gifOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
574
575
            // Build the basic command for ffmpeg
576
            $ffmpegCmd = $settings['ffmpegPath']
577
                .' -f gif'
578
                .' -i '.escapeshellarg($filePath)
579
                .' -vcodec '.$thisEncoder['videoCodec']
580
                .' '.$thisEncoder['videoCodecOptions'];
581
582
583
            // Create the directory if it isn't there already
584
            if (!is_dir($destVideoPath)) {
585
                try {
586
                    FileHelper::createDirectory($destVideoPath);
587
                } catch (Exception $e) {
588
                    Craft::error($e->getMessage(), __METHOD__);
589
                }
590
            }
591
592
            $destVideoFile = $this->getFilename($filePath, $gifOptions);
593
594
            // File to store the video encoding progress in
595
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.progress';
596
597
            // Assemble the destination path and final ffmpeg command
598
            $destVideoPath .= $destVideoFile;
599
            $ffmpegCmd .= ' '
600
                .' -y '.escapeshellarg($destVideoPath)
601
                .' 1> '.$progressFile.' 2>&1 & echo $!';
602
603
            // Make sure there isn't a lockfile for this video already
604
            $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.lock';
605
            $oldPid = @file_get_contents($lockFile);
606
            if ($oldPid !== false) {
607
                exec("ps $oldPid", $ProcessState);
608
                if (\count($ProcessState) >= 2) {
609
                    return $result;
610
                }
611
                // It's finished transcoding, so delete the lockfile and progress file
612
                @unlink($lockFile);
613
                @unlink($progressFile);
614
            }
615
616
            // If the video file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
617
            if (file_exists($destVideoPath) && (filemtime($destVideoPath) >= filemtime($filePath))) {
618
                $url = $settings['transcoderUrls']['gif'] ?? $settings['transcoderUrls']['default'];
619
                $result = Craft::getAlias($url).$destVideoFile;
620
            } else {
621
                // Kick off the transcoding
622
                $pid = $this->executeShellCommand($ffmpegCmd);
623
                Craft::info($ffmpegCmd."\nffmpeg PID: ".$pid, __METHOD__);
624
625
                // Create a lockfile in tmp
626
                file_put_contents($lockFile, $pid);
627
            }
628
        }
629
630
        return $result;
631
    }
632
633
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $options should have a doc-comment as per coding-style.
Loading history...
634
     * Get the name of a file from a path and options
635
     *
636
     * @param $filePath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
637
     * @param $options
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
638
     *
639
     * @return string
640
     */
641
    protected function getFilename($filePath, $options): string
642
    {
643
        $settings = Transcoder::$plugin->getSettings();
644
        $filePath = $this->getAssetPath($filePath);
645
646
        $validator = new UrlValidator();
647
        $error = '';
648
        if ($validator->validate($filePath, $error)) {
649
            $urlParts = parse_url($filePath);
650
            $pathParts = pathinfo($urlParts['path']);
651
        } else {
652
            $pathParts = pathinfo($filePath);
653
        }
654
        $fileName = $pathParts['filename'];
655
656
        // Add our options to the file name
657
        foreach ($options as $key => $value) {
658
            if (!empty($value)) {
659
                $suffix = '';
660
                if (!empty(self::SUFFIX_MAP[$key])) {
661
                    $suffix = self::SUFFIX_MAP[$key];
662
                }
663
                if (\is_bool($value)) {
664
                    $value = $value ? $key : 'no'.$key;
665
                }
666
                if (!\in_array($key, self::EXCLUDE_PARAMS, true)) {
667
                    $fileName .= '_'.$value.$suffix;
668
                }
669
            }
670
        }
671
        // See if we should use a hash instead
672
        if ($settings['useHashedNames']) {
673
            $fileName = $pathParts['filename'].md5($fileName);
674
        }
675
        $fileName .= $options['fileSuffix'];
676
677
        return $fileName;
678
    }
679
680
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $filePath should have a doc-comment as per coding-style.
Loading history...
681
     * Extract a file system path if $filePath is an Asset object
682
     *
683
     * @param $filePath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
684
     *
685
     * @return string
686
     */
687
    protected function getAssetPath($filePath): string
688
    {
689
        // If we're passed an Asset, extract the path from it
690
        if (\is_object($filePath) && ($filePath instanceof Asset)) {
691
            /** @var Asset $asset */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
692
            $asset = $filePath;
693
            $assetVolume = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $assetVolume is dead and can be removed.
Loading history...
694
            try {
695
                $assetVolume = $asset->getVolume();
696
            } catch (InvalidConfigException $e) {
697
                Craft::error($e->getMessage(), __METHOD__);
698
            }
699
700
            if ($assetVolume) {
701
                // If it's local, get a path to the file
702
                if ($assetVolume instanceof Local) {
703
                    $sourcePath = rtrim($assetVolume->path, DIRECTORY_SEPARATOR);
704
                    $sourcePath .= '' === $sourcePath ? '': DIRECTORY_SEPARATOR;
705
                    $folderPath = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $folderPath is dead and can be removed.
Loading history...
706
                    try {
707
                        $folderPath = rtrim($asset->getFolder()->path, DIRECTORY_SEPARATOR);
708
                    } catch (InvalidConfigException $e) {
709
                        Craft::error($e->getMessage(), __METHOD__);
710
                    }
711
                    $folderPath .= '' === $folderPath ? '': DIRECTORY_SEPARATOR;
712
713
                    $filePath = $sourcePath.$folderPath.$asset->filename;
714
                } else {
715
                    // Otherwise, get a URL
716
                    $filePath = $asset->getUrl();
717
                }
718
            }
719
        }
720
721
        $filePath = Craft::getAlias($filePath);
722
723
        // Make sure that $filePath is either an existing file, or a valid URL
724
        if (!file_exists($filePath)) {
725
            $validator = new UrlValidator();
726
            $error = '';
727
            if (!$validator->validate($filePath, $error)) {
728
                Craft::error($error, __METHOD__);
729
                $filePath = '';
730
            }
731
        }
732
733
        return $filePath;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $filePath could return the type boolean which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
734
    }
735
736
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $options should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $ffmpegCmd should have a doc-comment as per coding-style.
Loading history...
737
     * Set the width & height if desired
738
     *
739
     * @param $options
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
740
     * @param $ffmpegCmd
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
741
     *
742
     * @return string
743
     */
744
    protected function addScalingFfmpegArgs($options, $ffmpegCmd): string
745
    {
746
        if (!empty($options['width']) && !empty($options['height'])) {
747
            // Handle "none", "crop", and "letterbox" aspectRatios
748
            $aspectRatio = '';
749
            if (!empty($options['aspectRatio'])) {
750
                switch ($options['aspectRatio']) {
751
                    // Scale to the appropriate aspect ratio, padding
752
                    case 'letterbox':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
753
                        $letterboxColor = '';
754
                        if (!empty($options['letterboxColor'])) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
755
                            $letterboxColor = ':color='.$options['letterboxColor'];
756
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
757
                        $aspectRatio = ':force_original_aspect_ratio=decrease'
758
                            .',pad='.$options['width'].':'.$options['height'].':(ow-iw)/2:(oh-ih)/2'
759
                            .$letterboxColor;
760
                        break;
761
                    // Scale to the appropriate aspect ratio, cropping
762
                    case 'crop':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
763
                        $aspectRatio = ':force_original_aspect_ratio=increase'
764
                            .',crop='.$options['width'].':'.$options['height'];
765
                        break;
766
                    // No aspect ratio scaling at all
767
                    default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
768
                        $aspectRatio = ':force_original_aspect_ratio=disable';
769
                        $options['aspectRatio'] = 'none';
770
                        break;
771
                }
772
            }
773
            $sharpen = '';
774
            if (!empty($options['sharpen']) && ($options['sharpen'] !== false)) {
775
                $sharpen = ',unsharp=5:5:1.0:5:5:0.0';
776
            }
777
            $ffmpegCmd .= ' -vf "scale='
778
                .$options['width'].':'.$options['height']
779
                .$aspectRatio
780
                .$sharpen
781
                .'"';
782
        }
783
784
        return $ffmpegCmd;
785
    }
786
787
    // Protected Methods
788
    // =========================================================================
789
790
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $defaultName should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $options should have a doc-comment as per coding-style.
Loading history...
791
     * Combine the options arrays
792
     *
793
     * @param $defaultName
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
794
     * @param $options
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
795
     *
796
     * @return array
797
     */
798
    protected function coalesceOptions($defaultName, $options): array
799
    {
800
        // Default options
801
        $settings = Transcoder::$plugin->getSettings();
802
        $defaultOptions = $settings[$defaultName];
803
804
        // Coalesce the passed in $options with the $defaultOptions
805
        $options = array_merge($defaultOptions, $options);
806
807
        return $options;
808
    }
809
810
    /**
811
     * Execute a shell command
812
     *
813
     * @param string $command
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
814
     *
815
     * @return string
816
     */
817
    protected function executeShellCommand(string $command): string
818
    {
819
        // Create the shell command
820
        $shellCommand = new ShellCommand();
821
        $shellCommand->setCommand($command);
822
823
        // If we don't have proc_open, maybe we've got exec
824
        if (!\function_exists('proc_open') && \function_exists('exec')) {
825
            $shellCommand->useExec = true;
826
        }
827
828
        // Return the result of the command's output or error
829
        if ($shellCommand->execute()) {
830
            $result = $shellCommand->getOutput();
831
        } else {
832
            $result = $shellCommand->getError();
833
        }
834
835
        return $result;
836
    }
837
}
838