Passed
Push — develop ( 647bc4...1bccb7 )
by Andrew
08:42 queued 04:02
created

Transcode::getAudioUrl()   F

Complexity

Conditions 18
Paths 1922

Size

Total Lines 113
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 0 Features 0
Metric Value
cc 18
eloc 64
c 9
b 0
f 0
nc 1922
nop 2
dl 0
loc 113
rs 0.7

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
 * 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 for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
31
 * @package   Transcode
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag 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 for @since tag 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
    /**
84
     * Returns a URL to the transcoded video or "" if it doesn't exist (at
85
     * which
86
     * time it will create it).
87
     *
88
     * @param      $filePath     string  path to the original video -OR- an
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 6
Loading history...
89
     *                           Asset
90
     * @param      $videoOptions array   of options for the video
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 6
Loading history...
91
     * @param bool $generate     whether the video should be encoded
92
     *
93
     * @return string       URL of the transcoded video or ""
94
     */
95
    public function getVideoUrl($filePath, $videoOptions, $generate = true): string
96
    {
97
        $result = '';
98
        $settings = Transcoder::$plugin->getSettings();
99
        $subfolder = '';
100
101
        // sub folder check
102
        if (\is_object($filePath) && ($filePath instanceof Asset) && $settings['createSubfolders']) {
103
            $subfolder = $filePath->folderPath;
104
        }
105
106
        // file path
107
        $filePath = $this->getAssetPath($filePath);
108
109
        if (!empty($filePath)) {
110
            $destVideoPath = $settings['transcoderPaths']['video'].$subfolder ?? $settings['transcoderPaths']['default'];
111
            $destVideoPath = Craft::getAlias($destVideoPath);
112
            $videoOptions = $this->coalesceOptions('defaultVideoOptions', $videoOptions);
113
114
            // Get the video encoder presets to use
115
            $videoEncoders = $settings['videoEncoders'];
116
            $thisEncoder = $videoEncoders[$videoOptions['videoEncoder']];
117
118
            $videoOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
119
120
            // Build the basic command for ffmpeg
121
            $ffmpegCmd = $settings['ffmpegPath']
122
                .' -i '.escapeshellarg($filePath)
123
                .' -vcodec '.$thisEncoder['videoCodec']
124
                .' '.$thisEncoder['videoCodecOptions']
125
                .' -bufsize 1000k'
126
                .' -threads '.$thisEncoder['threads'];
127
128
            // Set the framerate if desired
129
            if (!empty($videoOptions['videoFrameRate'])) {
130
                $ffmpegCmd .= ' -r '.$videoOptions['videoFrameRate'];
131
            }
132
133
            // Set the bitrate if desired
134
            if (!empty($videoOptions['videoBitRate'])) {
135
                $ffmpegCmd .= ' -b:v '.$videoOptions['videoBitRate'].' -maxrate '.$videoOptions['videoBitRate'];
136
            }
137
138
            // Adjust the scaling if desired
139
            $ffmpegCmd = $this->addScalingFfmpegArgs(
140
                $videoOptions,
141
                $ffmpegCmd
142
            );
143
144
            // Handle any audio transcoding
145
            if (empty($videoOptions['audioBitRate'])
146
                && empty($videoOptions['audioSampleRate'])
147
                && empty($videoOptions['audioChannels'])
148
            ) {
149
                // Just copy the audio if no options are provided
150
                $ffmpegCmd .= ' -c:a copy';
151
            } else {
152
                // Do audio transcoding based on the settings
153
                $ffmpegCmd .= ' -acodec '.$thisEncoder['audioCodec'];
154
                if (!empty($videoOptions['audioBitRate'])) {
155
                    $ffmpegCmd .= ' -b:a '.$videoOptions['audioBitRate'];
156
                }
157
                if (!empty($videoOptions['audioSampleRate'])) {
158
                    $ffmpegCmd .= ' -ar '.$videoOptions['audioSampleRate'];
159
                }
160
                if (!empty($videoOptions['audioChannels'])) {
161
                    $ffmpegCmd .= ' -ac '.$videoOptions['audioChannels'];
162
                }
163
                $ffmpegCmd .= ' '.$thisEncoder['audioCodecOptions'];
164
            }
165
166
            // Create the directory if it isn't there already
167
            if (!is_dir($destVideoPath)) {
168
                try {
169
                    FileHelper::createDirectory($destVideoPath);
170
                } catch (Exception $e) {
171
                    Craft::error($e->getMessage(), __METHOD__);
172
                }
173
            }
174
175
            $destVideoFile = $this->getFilename($filePath, $videoOptions);
176
177
            // File to store the video encoding progress in
178
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.progress';
179
180
            // Assemble the destination path and final ffmpeg command
181
            $destVideoPath .= $destVideoFile;
182
            $ffmpegCmd .= ' -f '
183
                .$thisEncoder['fileFormat']
184
                .' -y '.escapeshellarg($destVideoPath)
185
                .' 1> '.$progressFile.' 2>&1 & echo $!';
186
187
            // Make sure there isn't a lockfile for this video already
188
            $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.lock';
189
            $oldPid = @file_get_contents($lockFile);
190
            if ($oldPid !== false) {
191
                exec("ps $oldPid", $ProcessState);
192
                if (\count($ProcessState) >= 2) {
193
                    return $result;
194
                }
195
                // It's finished transcoding, so delete the lockfile and progress file
196
                @unlink($lockFile);
197
                @unlink($progressFile);
198
            }
199
200
            // If the video file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
201
            if (file_exists($destVideoPath) && (@filemtime($destVideoPath) >= @filemtime($filePath))) {
202
                $url = $settings['transcoderUrls']['video'].$subfolder ?? $settings['transcoderUrls']['default'];
203
                $result = Craft::getAlias($url).$destVideoFile;
204
                // skip encoding
205
            } elseif (!$generate) {
206
                $result = "";
207
            } else {
208
                // Kick off the transcoding
209
                $pid = $this->executeShellCommand($ffmpegCmd);
210
                Craft::info($ffmpegCmd."\nffmpeg PID: ".$pid, __METHOD__);
211
212
                // Create a lockfile in tmp
213
                file_put_contents($lockFile, $pid);
214
            }
215
        }
216
217
        return $result;
218
    }
219
220
    /**
221
     * Returns a URL to a video thumbnail
222
     *
223
     * @param string $filePath         path to the original video or an Asset
224
     * @param array  $thumbnailOptions of options for the thumbnail
225
     * @param bool   $generate         whether the thumbnail should be
226
     *                                 generated if it doesn't exists
227
     * @param bool   $asPath           Whether we should return a path or not
228
     *
229
     * @return string|false|null URL or path of the video thumbnail
230
     */
231
    public function getVideoThumbnailUrl($filePath, $thumbnailOptions, $generate = true, $asPath = false)
232
    {
233
        $result = null;
234
        $settings = Transcoder::$plugin->getSettings();
235
        $subfolder = '';
236
237
        // sub folder check
238
        if (\is_object($filePath) && ($filePath instanceof Asset) && $settings['createSubfolders']) {
0 ignored issues
show
introduced by
The condition is_object($filePath) is always false.
Loading history...
239
            $subfolder = $filePath->folderPath;
240
        }
241
242
        $filePath = $this->getAssetPath($filePath);
243
244
        if (!empty($filePath)) {
245
            $destThumbnailPath = $settings['transcoderPaths']['thumbnail'].$subfolder ?? $settings['transcoderPaths']['default'];
246
            $destThumbnailPath = Craft::getAlias($destThumbnailPath);
247
248
            $thumbnailOptions = $this->coalesceOptions('defaultThumbnailOptions', $thumbnailOptions);
249
250
            // Build the basic command for ffmpeg
251
            $ffmpegCmd = $settings['ffmpegPath']
252
                .' -i '.escapeshellarg($filePath)
253
                .' -vcodec mjpeg'
254
                .' -vframes 1';
255
256
            // Adjust the scaling if desired
257
            $ffmpegCmd = $this->addScalingFfmpegArgs(
258
                $thumbnailOptions,
259
                $ffmpegCmd
260
            );
261
262
            // Set the timecode to get the thumbnail from if desired
263
            if (!empty($thumbnailOptions['timeInSecs'])) {
264
                $timeCode = gmdate('H:i:s', $thumbnailOptions['timeInSecs']);
265
                $ffmpegCmd .= ' -ss '.$timeCode.'.00';
266
            }
267
268
            // Create the directory if it isn't there already
269
            if (!is_dir($destThumbnailPath)) {
270
                try {
271
                    FileHelper::createDirectory($destThumbnailPath);
272
                } catch (Exception $e) {
273
                    Craft::error($e->getMessage(), __METHOD__);
274
                }
275
            }
276
277
            $destThumbnailFile = $this->getFilename($filePath, $thumbnailOptions);
278
279
            // Assemble the destination path and final ffmpeg command
280
            $destThumbnailPath .= $destThumbnailFile;
281
            $ffmpegCmd .= ' -f image2 -y '.escapeshellarg($destThumbnailPath).' >/dev/null 2>/dev/null &';
282
283
            // If the thumbnail file already exists, return it.  Otherwise, generate it and return it
284
            if (!file_exists($destThumbnailPath)) {
285
                if ($generate) {
286
                    /** @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...
287
                    $shellOutput = $this->executeShellCommand($ffmpegCmd);
288
                    Craft::info($ffmpegCmd, __METHOD__);
289
290
                    // if ffmpeg fails which we can't check because the process is ran in the background
291
                    // dont return the future path of the image or else we can't check this in the front end
292
293
                    return false;
294
                } else {
295
                    Craft::info('Thumbnail does not exist, but not asked to generate it: '.$filePath, __METHOD__);
296
297
                    // The file doesn't exist, and we weren't asked to generate it
298
                    return false;
299
                }
300
            }
301
            // Return either a path or a URL
302
            if ($asPath) {
303
                $result = $destThumbnailPath;
304
            } else {
305
                $url = $settings['transcoderUrls']['thumbnail'].$subfolder ?? $settings['transcoderUrls']['default'];
306
                $result = Craft::getAlias($url).$destThumbnailFile;
307
            }
308
        }
309
310
        return $result;
311
    }
312
313
    /**
314
     * Returns a URL to the transcoded audio file or "" if it doesn't exist
315
     * (at which time it will create it).
316
     *
317
     * @param $filePath     string path to the original audio file -OR- an Asset
318
     * @param $audioOptions array of options for the audio file
319
     *
320
     * @return string       URL of the transcoded audio file or ""
321
     */
322
    public function getAudioUrl($filePath, $audioOptions): string
323
    {
324
        $result = '';
325
        $settings = Transcoder::$plugin->getSettings();
326
        $subfolder = '';
327
328
        // sub folder check
329
        if (\is_object($filePath) && ($filePath instanceof Asset) && $settings['createSubfolders']) {
330
            $subfolder = $filePath->folderPath;
331
        }
332
333
        $filePath = $this->getAssetPath($filePath);
334
335
        if (!empty($filePath)) {
336
            $destAudioPath = $settings['transcoderPaths']['audio'].$subfolder ?? $settings['transcoderPaths']['default'];
337
            $destAudioPath = Craft::getAlias($destAudioPath);
338
339
            $audioOptions = $this->coalesceOptions('defaultAudioOptions', $audioOptions);
340
341
            // Get the audio encoder presets to use
342
            $audioEncoders = $settings['audioEncoders'];
343
            $thisEncoder = $audioEncoders[$audioOptions['audioEncoder']];
344
345
            $audioOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
346
347
            // Build the basic command for ffmpeg
348
            $ffmpegCmd = $settings['ffmpegPath']
349
                .' -i '.escapeshellarg($filePath)
350
                .' -acodec '.$thisEncoder['audioCodec']
351
                .' '.$thisEncoder['audioCodecOptions']
352
                .' -bufsize 1000k'
353
                .' -threads '.$thisEncoder['threads'];
354
355
            // Set the bitrate if desired
356
            if (!empty($audioOptions['audioBitRate'])) {
357
                $ffmpegCmd .= ' -b:a '.$audioOptions['audioBitRate'];
358
            }
359
            // Set the sample rate if desired
360
            if (!empty($audioOptions['audioSampleRate'])) {
361
                $ffmpegCmd .= ' -ar '.$audioOptions['audioSampleRate'];
362
            }
363
            // Set the audio channels if desired
364
            if (!empty($audioOptions['audioChannels'])) {
365
                $ffmpegCmd .= ' -ac '.$audioOptions['audioChannels'];
366
            }
367
            $ffmpegCmd .= ' '.$thisEncoder['audioCodecOptions'];
368
369
            if (!empty($audioOptions['timeInSecs'])) {
370
                $ffmpegCmd .= ' -t '.$audioOptions['timeInSecs'];
371
            }
372
373
            // Create the directory if it isn't there already
374
            if (!is_dir($destAudioPath)) {
375
                try {
376
                    FileHelper::createDirectory($destAudioPath);
377
                } catch (Exception $e) {
378
                    Craft::error($e->getMessage(), __METHOD__);
379
                }
380
            }
381
382
            $destAudioFile = $this->getFilename($filePath, $audioOptions);
383
384
            // File to store the audio encoding progress in
385
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destAudioFile.'.progress';
386
387
            // Assemble the destination path and final ffmpeg command
388
            $destAudioPath .= $destAudioFile;
389
            
390
            if ($audioOptions['stripMetadata']) {
391
                $ffmpegCmd .= ' -map_metadata -1 ';
392
            }
393
            
394
            $ffmpegCmd .= ' -f '
395
                .$thisEncoder['fileFormat']
396
                .' -y '.escapeshellarg($destAudioPath);
397
            
398
            if (!$audioOptions['synchronous']) {
399
                $ffmpegCmd .=' 1> '.$progressFile.' 2>&1 & echo $!';
400
                // Make sure there isn't a lockfile for this audio file already
401
                $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destAudioFile.'.lock';
402
                $oldPid = @file_get_contents($lockFile);
403
                if ($oldPid !== false) {
404
                    exec("ps $oldPid", $ProcessState);
405
                    if (\count($ProcessState) >= 2) {
406
                        return $result;
407
                    }
408
                    // It's finished transcoding, so delete the lockfile and progress file
409
                    @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

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