Transcode   F
last analyzed

Complexity

Total Complexity 112

Size/Duplication

Total Lines 896
Duplicated Lines 0 %

Importance

Changes 33
Bugs 2 Features 0
Metric Value
eloc 423
c 33
b 2
f 0
dl 0
loc 896
rs 2
wmc 112

14 Methods

Rating   Name   Duplication   Size   Complexity  
F getAudioUrl() 0 131 20
A executeShellCommand() 0 19 4
C getFileInfo() 0 64 16
A coalesceOptions() 0 8 1
B getAssetPath() 0 47 10
A getGifFilename() 0 12 1
B getVideoThumbnailUrl() 0 80 10
A getVideoFilename() 0 12 1
A handleGetAssetThumbPath() 0 7 1
A getAudioFilename() 0 12 1
B addScalingFfmpegArgs() 0 41 9
F getVideoUrl() 0 127 19
B getFilename() 0 37 9
B getGifUrl() 0 86 10

How to fix   Complexity   

Complex Class

Complex classes like Transcode often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Transcode, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Transcoder plugin for Craft CMS
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 Craft;
14
use craft\base\Component;
15
use craft\elements\Asset;
0 ignored issues
show
Bug introduced by
The type craft\elements\Asset was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use craft\events\DefineAssetThumbUrlEvent;
17
use craft\fs\Local;
18
use craft\helpers\App;
19
use craft\helpers\FileHelper;
20
use craft\helpers\Json as JsonHelper;
21
use mikehaertl\shellcommand\Command as ShellCommand;
22
use nystudio107\transcoder\Transcoder;
23
use yii\base\Exception;
24
use yii\base\InvalidConfigException;
25
use yii\validators\UrlValidator;
26
use function count;
27
use function function_exists;
28
use function in_array;
29
use function is_bool;
30
31
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
32
 * @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...
33
 * @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...
34
 * @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...
35
 */
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...
36
class Transcode extends Component
37
{
38
    // Constants
39
    // =========================================================================
40
41
    // Suffixes to add to the generated filename params
42
    protected const SUFFIX_MAP = [
43
        'videoFrameRate' => 'fps',
44
        'videoBitRate' => 'bps',
45
        'audioBitRate' => 'bps',
46
        'audioChannels' => 'c',
47
        'height' => 'h',
48
        'width' => 'w',
49
        'timeInSecs' => 's',
50
    ];
51
52
    // Params that should be excluded from being part of the generated filename
53
    protected const EXCLUDE_PARAMS = [
54
        'videoEncoder',
55
        'audioEncoder',
56
        'fileSuffix',
57
        'sharpen',
58
        'synchronous',
59
        'stripMetadata',
60
        'videoCodecOptions',
61
    ];
62
63
    // Mappings for getFileInfo() summary values
64
    protected const INFO_SUMMARY = [
65
        'format' => [
66
            'filename' => 'filename',
67
            'duration' => 'duration',
68
            'size' => 'size',
69
        ],
70
        'audio' => [
71
            'codec_name' => 'audioEncoder',
72
            'bit_rate' => 'audioBitRate',
73
            'sample_rate' => 'audioSampleRate',
74
            'channels' => 'audioChannels',
75
        ],
76
        'video' => [
77
            'codec_name' => 'videoEncoder',
78
            'bit_rate' => 'videoBitRate',
79
            'avg_frame_rate' => 'videoFrameRate',
80
            'height' => 'height',
81
            'width' => 'width',
82
        ],
83
    ];
84
85
    // Public Methods
86
    // =========================================================================
87
88
    /**
89
     * Returns a URL to the transcoded video or "" if it doesn't exist (at
90
     * which
91
     * time it will create it).
92
     *
93
     * @param string|Asset $filePath string  path to the original video -OR- an
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
94
     *                           Asset
0 ignored issues
show
Coding Style introduced by
Parameter comment not aligned correctly; expected 31 spaces but found 27
Loading history...
95
     * @param array $videoOptions array   of options for the video
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
96
     * @param bool $generate whether the video should be encoded
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
97
     *
98
     * @return string       URL of the transcoded video or ""
99
     * @throws InvalidConfigException
100
     */
101
    public function getVideoUrl(string|Asset $filePath, array $videoOptions, bool $generate = true): string
102
    {
103
        $result = '';
104
        $settings = Transcoder::$plugin->getSettings();
0 ignored issues
show
Bug introduced by
The method getSettings() does not exist on null. ( Ignorable by Annotation )

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

104
        /** @scrutinizer ignore-call */ 
105
        $settings = Transcoder::$plugin->getSettings();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
105
        $subfolder = '';
106
107
        // sub folder check
108
        if (($filePath instanceof Asset) && $settings['createSubfolders']) {
109
            $subfolder = $filePath->folderPath;
110
        }
111
112
        // file path
113
        $filePath = $this->getAssetPath($filePath);
114
115
        if (!empty($filePath)) {
116
            $destVideoPath = $settings['transcoderPaths']['video'] ?? $settings['transcoderPaths']['default'];
117
            $destVideoPath .= $subfolder;
118
            $destVideoPath = App::parseEnv($destVideoPath);
119
            $videoOptions = $this->coalesceOptions('defaultVideoOptions', $videoOptions);
120
121
            // Get the video encoder presets to use
122
            $videoEncoders = $settings['videoEncoders'];
123
            $thisEncoder = $videoEncoders[$videoOptions['videoEncoder']];
124
125
            $videoOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
126
127
            // Build the basic command for ffmpeg
128
            $ffmpegCmd = $settings['ffmpegPath']
129
                . ' -i ' . escapeshellarg($filePath)
130
                . ' -vcodec ' . $thisEncoder['videoCodec']
131
                . ' ' . $thisEncoder['videoCodecOptions']
132
                . ' -bufsize 1000k'
133
                . ' -threads ' . $thisEncoder['threads'];
134
135
            // Set the framerate if desired
136
            if (!empty($videoOptions['videoFrameRate'])) {
137
                $ffmpegCmd .= ' -r ' . $videoOptions['videoFrameRate'];
138
            }
139
140
            // Set the bitrate if desired
141
            if (!empty($videoOptions['videoBitRate'])) {
142
                $ffmpegCmd .= ' -b:v ' . $videoOptions['videoBitRate'] . ' -maxrate ' . $videoOptions['videoBitRate'];
143
            }
144
145
            // Adjust the scaling if desired
146
            $ffmpegCmd = $this->addScalingFfmpegArgs(
147
                $videoOptions,
148
                $ffmpegCmd
149
            );
150
151
            // Handle any audio transcoding
152
            if (empty($videoOptions['audioBitRate'])
153
                && empty($videoOptions['audioSampleRate'])
154
                && empty($videoOptions['audioChannels'])
155
            ) {
156
                // Just copy the audio if no options are provided
157
                $ffmpegCmd .= ' -c:a copy';
158
            } else {
159
                // Do audio transcoding based on the settings
160
                $ffmpegCmd .= ' -acodec ' . $thisEncoder['audioCodec'];
161
                if (!empty($videoOptions['audioBitRate'])) {
162
                    $ffmpegCmd .= ' -b:a ' . $videoOptions['audioBitRate'];
163
                }
164
                if (!empty($videoOptions['audioSampleRate'])) {
165
                    $ffmpegCmd .= ' -ar ' . $videoOptions['audioSampleRate'];
166
                }
167
                if (!empty($videoOptions['audioChannels'])) {
168
                    $ffmpegCmd .= ' -ac ' . $videoOptions['audioChannels'];
169
                }
170
                $ffmpegCmd .= ' ' . $thisEncoder['audioCodecOptions'];
171
            }
172
173
            // Create the directory if it isn't there already
174
            if (!is_dir($destVideoPath)) {
0 ignored issues
show
Bug introduced by
It seems like $destVideoPath can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

174
            if (!is_dir(/** @scrutinizer ignore-type */ $destVideoPath)) {
Loading history...
175
                try {
176
                    FileHelper::createDirectory($destVideoPath);
177
                } catch (Exception $e) {
178
                    Craft::error($e->getMessage(), __METHOD__);
179
                }
180
            }
181
182
            $destVideoFile = $this->getFilename($filePath, $videoOptions);
183
184
            // File to store the video encoding progress in
185
            $progressFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $destVideoFile . '.progress';
186
187
            // Assemble the destination path and final ffmpeg command
188
            $destVideoPath .= $destVideoFile;
189
            $ffmpegCmd .= ' -f '
190
                . $thisEncoder['fileFormat']
191
                . ' -y ' . escapeshellarg($destVideoPath)
192
                . ' 1> ' . $progressFile . ' 2>&1 & echo $!';
193
194
            // Make sure there isn't a lockfile for this video already
195
            $lockFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $destVideoFile . '.lock';
196
            $oldPid = @file_get_contents($lockFile);
197
            if ($oldPid !== false) {
198
                // See if the process is running, and empty result means the process is still running
199
                // ref: https://stackoverflow.com/questions/3043978/how-to-check-if-a-process-id-pid-exists
200
                exec("kill -0 $oldPid 2>&1", $ProcessState);
201
                if (count($ProcessState) === 0) {
202
                    return $result;
203
                }
204
                // It's finished transcoding, so delete the lockfile and progress file
205
                @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

205
                /** @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...
206
                @unlink($progressFile);
207
            }
208
209
            // If the video file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
210
            if (file_exists($destVideoPath) && (@filemtime($destVideoPath) >= @filemtime($filePath))) {
211
                $url = $settings['transcoderUrls']['video'] ?? $settings['transcoderUrls']['default'];
212
                $url .= $subfolder;
213
                $result = App::parseEnv($url) . $destVideoFile;
214
            // skip encoding
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 16 spaces, found 12
Loading history...
215
            } elseif (!$generate) {
216
                $result = '';
217
            } else {
218
                // Kick off the transcoding
219
                $pid = $this->executeShellCommand($ffmpegCmd);
220
                Craft::info($ffmpegCmd . "\nffmpeg PID: " . $pid, __METHOD__);
221
222
                // Create a lockfile in tmp
223
                file_put_contents($lockFile, $pid);
224
            }
225
        }
226
227
        return $result;
228
    }
229
230
    /**
231
     * Returns a URL to a video thumbnail
232
     *
233
     * @param Asset|string $filePath path to the original video or an Asset
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter name; 1 found
Loading history...
234
     * @param array $thumbnailOptions of options for the thumbnail
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
235
     * @param bool $generate whether the thumbnail should be
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Expected 9 spaces after parameter name; 1 found
Loading history...
236
     *                                 generated if it doesn't exists
0 ignored issues
show
Coding Style introduced by
Parameter comment not aligned correctly; expected 23 spaces but found 33
Loading history...
237
     * @param bool $asPath Whether we should return a path or not
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Expected 11 spaces after parameter name; 1 found
Loading history...
238
     *
239
     * @return string|false|null URL or path of the video thumbnail
240
     * @throws InvalidConfigException
241
     */
242
    public function getVideoThumbnailUrl(Asset|string $filePath, array $thumbnailOptions, bool $generate = true, bool $asPath = false): string|false|null
243
    {
244
        $result = null;
245
        $settings = Transcoder::$plugin->getSettings();
246
        $subfolder = '';
247
248
        // sub folder check
249
        if (($filePath instanceof Asset) && $settings['createSubfolders']) {
250
            $subfolder = $filePath->folderPath;
251
        }
252
253
        $filePath = $this->getAssetPath($filePath);
254
255
        if (!empty($filePath)) {
256
            $destThumbnailPath = $settings['transcoderPaths']['thumbnail'] ?? $settings['transcoderPaths']['default'];
257
            $destThumbnailPath .= $subfolder;
258
            $destThumbnailPath = App::parseEnv($destThumbnailPath);
259
260
            $thumbnailOptions = $this->coalesceOptions('defaultThumbnailOptions', $thumbnailOptions);
261
262
            // Build the basic command for ffmpeg
263
            $ffmpegCmd = $settings['ffmpegPath']
264
                . ' -i ' . escapeshellarg($filePath)
265
                . ' -vcodec mjpeg'
266
                . ' -vframes 1';
267
268
            // Adjust the scaling if desired
269
            $ffmpegCmd = $this->addScalingFfmpegArgs(
270
                $thumbnailOptions,
271
                $ffmpegCmd
272
            );
273
274
            // Set the timecode to get the thumbnail from if desired
275
            if (!empty($thumbnailOptions['timeInSecs'])) {
276
                $timeCode = gmdate('H:i:s', $thumbnailOptions['timeInSecs']);
277
                $ffmpegCmd .= ' -ss ' . $timeCode . '.00';
278
            }
279
280
            // Create the directory if it isn't there already
281
            if (!is_dir($destThumbnailPath)) {
0 ignored issues
show
Bug introduced by
It seems like $destThumbnailPath can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

281
            if (!is_dir(/** @scrutinizer ignore-type */ $destThumbnailPath)) {
Loading history...
282
                try {
283
                    FileHelper::createDirectory($destThumbnailPath);
284
                } catch (Exception $e) {
285
                    Craft::error($e->getMessage(), __METHOD__);
286
                }
287
            }
288
289
            $destThumbnailFile = $this->getFilename($filePath, $thumbnailOptions);
290
291
            // Assemble the destination path and final ffmpeg command
292
            $destThumbnailPath .= $destThumbnailFile;
293
            $ffmpegCmd .= ' -f image2 -y ' . escapeshellarg($destThumbnailPath) . ' >/dev/null 2>/dev/null &';
294
295
            // If the thumbnail file already exists, return it.  Otherwise, generate it and return it
296
            if (!file_exists($destThumbnailPath)) {
297
                if ($generate) {
298
                    /** @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...
299
                    $shellOutput = $this->executeShellCommand($ffmpegCmd);
300
                    Craft::info($ffmpegCmd, __METHOD__);
301
302
                // if ffmpeg fails which we can't check because the process is ran in the background
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 20 spaces, found 16
Loading history...
303
                    // don't return the future path of the image or else we can't check this in the front end
304
                } else {
305
                    Craft::info('Thumbnail does not exist, but not asked to generate it: ' . $filePath, __METHOD__);
306
307
                    // The file doesn't exist, and we weren't asked to generate it
308
                }
309
                return false;
310
            }
311
            // Return either a path or a URL
312
            if ($asPath) {
313
                $result = $destThumbnailPath;
314
            } else {
315
                $url = $settings['transcoderUrls']['thumbnail'] ?? $settings['transcoderUrls']['default'];
316
                $url .= $subfolder;
317
                $result = App::parseEnv($url) . $destThumbnailFile;
318
            }
319
        }
320
321
        return $result;
322
    }
323
324
    /**
325
     * Returns a URL to the transcoded audio file or "" if it doesn't exist
326
     * (at which time it will create it).
327
     *
328
     * @param Asset|string $filePath path to the original audio file -OR- an Asset
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
329
     * @param array $audioOptions array of options for the audio file
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
330
     *
331
     * @return string       URL of the transcoded audio file or ""
332
     * @throws InvalidConfigException
333
     */
334
    public function getAudioUrl(Asset|string $filePath, array $audioOptions): string
335
    {
336
        $result = '';
337
        $settings = Transcoder::$plugin->getSettings();
338
        $subfolder = '';
339
340
        // sub folder check
341
        if (($filePath instanceof Asset) && $settings['createSubfolders']) {
342
            $subfolder = $filePath->folderPath;
343
        }
344
345
        $filePath = $this->getAssetPath($filePath);
346
347
        if (!empty($filePath)) {
348
            $destAudioPath = $settings['transcoderPaths']['audio'] ?? $settings['transcoderPaths']['default'];
349
            $destAudioPath .= $subfolder;
350
            $destAudioPath = App::parseEnv($destAudioPath);
351
352
            $audioOptions = $this->coalesceOptions('defaultAudioOptions', $audioOptions);
353
354
            // Get the audio encoder presets to use
355
            $audioEncoders = $settings['audioEncoders'];
356
            $thisEncoder = $audioEncoders[$audioOptions['audioEncoder']];
357
358
            $audioOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
359
360
            // Build the basic command for ffmpeg
361
            $ffmpegCmd = $settings['ffmpegPath']
362
                . ' -i ' . escapeshellarg($filePath)
363
                . ' -acodec ' . $thisEncoder['audioCodec']
364
                . ' ' . $thisEncoder['audioCodecOptions']
365
                . ' -bufsize 1000k'
366
                . ' -vn'
367
                . ' -threads ' . $thisEncoder['threads'];
368
369
            // Set the bitrate if desired
370
            if (!empty($audioOptions['audioBitRate'])) {
371
                $ffmpegCmd .= ' -b:a ' . $audioOptions['audioBitRate'];
372
            }
373
            // Set the sample rate if desired
374
            if (!empty($audioOptions['audioSampleRate'])) {
375
                $ffmpegCmd .= ' -ar ' . $audioOptions['audioSampleRate'];
376
            }
377
            // Set the audio channels if desired
378
            if (!empty($audioOptions['audioChannels'])) {
379
                $ffmpegCmd .= ' -ac ' . $audioOptions['audioChannels'];
380
            }
381
            $ffmpegCmd .= ' ' . $thisEncoder['audioCodecOptions'];
382
383
            if (!empty($audioOptions['seekInSecs'])) {
384
                $ffmpegCmd .= ' -ss ' . $audioOptions['seekInSecs'];
385
            }
386
387
            if (!empty($audioOptions['timeInSecs'])) {
388
                $ffmpegCmd .= ' -t ' . $audioOptions['timeInSecs'];
389
            }
390
391
            // Create the directory if it isn't there already
392
            if (!is_dir($destAudioPath)) {
0 ignored issues
show
Bug introduced by
It seems like $destAudioPath can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

392
            if (!is_dir(/** @scrutinizer ignore-type */ $destAudioPath)) {
Loading history...
393
                try {
394
                    FileHelper::createDirectory($destAudioPath);
395
                } catch (Exception $e) {
396
                    Craft::error($e->getMessage(), __METHOD__);
397
                }
398
            }
399
400
            $destAudioFile = $this->getFilename($filePath, $audioOptions);
401
402
            // File to store the audio encoding progress in
403
            $progressFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $destAudioFile . '.progress';
404
405
            // Assemble the destination path and final ffmpeg command
406
            $destAudioPath .= $destAudioFile;
407
            // Handle the `stripMetadata` setting
408
            $stripMetadata = false;
409
            if (!empty($audioOptions['stripMetadata'])) {
410
                $stripMetadata = $audioOptions['stripMetadata'];
411
            }
412
            if ($stripMetadata) {
413
                $ffmpegCmd .= ' -map_metadata -1 ';
414
            }
415
            // Add the file format
416
            $ffmpegCmd .= ' -f '
417
                . $thisEncoder['fileFormat']
418
                . ' -y ' . escapeshellarg($destAudioPath);
419
            // Handle the `synchronous` setting
420
            $synchronous = false;
421
            if (!empty($audioOptions['synchronous'])) {
422
                $synchronous = $audioOptions['synchronous'];
423
            }
424
            if (!$synchronous) {
425
                $ffmpegCmd .= ' 1> ' . $progressFile . ' 2>&1 & echo $!';
426
                // Make sure there isn't a lockfile for this audio file already
427
                $lockFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $destAudioFile . '.lock';
428
                $oldPid = @file_get_contents($lockFile);
429
                if ($oldPid !== false) {
430
                    // See if the process is running, and empty result means the process is still running
431
                    // ref: https://stackoverflow.com/questions/3043978/how-to-check-if-a-process-id-pid-exists
432
                    exec("kill -0 $oldPid 2>&1", $ProcessState);
433
                    if (count($ProcessState) === 0) {
434
                        return $result;
435
                    }
436
                    // It's finished transcoding, so delete the lockfile and progress file
437
                    @unlink($lockFile);
438
                    @unlink($progressFile);
439
                }
440
            }
441
442
            // If the audio file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
443
            if (file_exists($destAudioPath) && (@filemtime($destAudioPath) >= @filemtime($filePath))) {
444
                $url = $settings['transcoderUrls']['audio'] ?? $settings['transcoderUrls']['default'];
445
                $url .= $subfolder;
446
                $result = App::parseEnv($url) . $destAudioFile;
447
            } else {
448
                // Kick off the transcoding
449
                $pid = $this->executeShellCommand($ffmpegCmd);
450
451
                if ($synchronous) {
452
                    Craft::info($ffmpegCmd, __METHOD__);
453
                    $url = $settings['transcoderUrls']['audio'] ?? $settings['transcoderUrls']['default'];
454
                    $url .= $subfolder;
455
                    $result = App::parseEnv($url) . $destAudioFile;
456
                } else {
457
                    Craft::info($ffmpegCmd . "\nffmpeg PID: " . $pid, __METHOD__);
458
                    // Create a lockfile in tmp
459
                    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...
460
                }
461
            }
462
        }
463
464
        return $result;
465
    }
466
467
    /**
468
     * Extract information from a video/audio file
469
     *
470
     * @param Asset|string $filePath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
471
     * @param bool $summary
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
472
     *
473
     * @return null|array
474
     * @throws InvalidConfigException
475
     */
476
    public function getFileInfo(Asset|string $filePath, bool $summary = false): ?array
477
    {
478
        $result = null;
479
        $settings = Transcoder::$plugin->getSettings();
480
        $filePath = $this->getAssetPath($filePath);
481
482
        if (!empty($filePath)) {
483
            // Build the basic command for ffprobe
484
            $ffprobeOptions = $settings['ffprobeOptions'];
485
            $ffprobeCmd = $settings['ffprobePath']
486
                . ' ' . $ffprobeOptions
487
                . ' ' . escapeshellarg($filePath);
488
489
            $shellOutput = $this->executeShellCommand($ffprobeCmd);
490
            Craft::info($ffprobeCmd, __METHOD__);
491
            $result = JsonHelper::decodeIfJson($shellOutput, true);
492
            Craft::info(print_r($result, true), __METHOD__);
0 ignored issues
show
Bug introduced by
It seems like print_r($result, true) can also be of type true; however, parameter $message of yii\BaseYii::info() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

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

492
            Craft::info(/** @scrutinizer ignore-type */ print_r($result, true), __METHOD__);
Loading history...
493
            // Handle the case it not being JSON
494
            if (!is_array($result)) {
495
                $result = [];
496
            }
497
            // Trim down the arrays to just a summary
498
            if ($summary && !empty($result)) {
499
                $summaryResult = [];
500
                foreach ($result as $topLevelKey => $topLevelValue) {
501
                    switch ($topLevelKey) {
502
                        // Format info
503
                        case 'format':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
504
                            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...
505
                                if (!empty($topLevelValue[$settingKey])) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
506
                                    $summaryResult[$settingValue] = $topLevelValue[$settingKey];
507
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
508
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
509
                            break;
510
                        // Stream info
511
                        case 'streams':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
512
                            foreach ($topLevelValue as $stream) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
513
                                $infoSummaryType = $stream['codec_type'];
514
                                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...
515
                                    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...
516
                                        if (!empty($stream[$settingKey])) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 36 spaces, found 40
Loading history...
517
                                            $summaryResult[$settingValue] = $stream[$settingKey];
518
                                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 36 spaces, found 40
Loading history...
519
                                    }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 32 spaces, found 36
Loading history...
520
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
521
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
522
                            break;
523
                        // Unknown info
524
                        default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
525
                            break;
526
                    }
527
                }
528
                // Handle cases where the framerate is returned as XX/YY
529
                if (!empty($summaryResult['videoFrameRate'])
530
                    && (str_contains($summaryResult['videoFrameRate'], '/'))
531
                ) {
532
                    $parts = explode('/', $summaryResult['videoFrameRate']);
533
                    $summaryResult['videoFrameRate'] = (float)$parts[0] / (float)$parts[1];
534
                }
535
                $result = $summaryResult;
536
            }
537
        }
538
539
        return $result;
540
    }
541
542
    /**
543
     * Get the name of a video file from a path and options
544
     *
545
     * @param Asset|string $filePath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
546
     * @param array $videoOptions
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
547
     *
548
     * @return string
549
     * @throws InvalidConfigException
550
     */
551
    public function getVideoFilename(Asset|string $filePath, array $videoOptions): string
552
    {
553
        $settings = Transcoder::$plugin->getSettings();
554
        $videoOptions = $this->coalesceOptions('defaultVideoOptions', $videoOptions);
555
556
        // Get the video encoder presets to use
557
        $videoEncoders = $settings['videoEncoders'];
558
        $thisEncoder = $videoEncoders[$videoOptions['videoEncoder']];
559
560
        $videoOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
561
562
        return $this->getFilename($filePath, $videoOptions);
563
    }
564
565
    /**
566
     * Get the name of an audio file from a path and options
567
     *
568
     * @param Asset|string $filePath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
569
     * @param array $audioOptions
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
570
     *
571
     * @return string
572
     * @throws InvalidConfigException
573
     */
574
    public function getAudioFilename(Asset|string $filePath, array $audioOptions): string
575
    {
576
        $settings = Transcoder::$plugin->getSettings();
577
        $audioOptions = $this->coalesceOptions('defaultAudioOptions', $audioOptions);
578
579
        // Get the video encoder presets to use
580
        $audioEncoders = $settings['audioEncoders'];
581
        $thisEncoder = $audioEncoders[$audioOptions['audioEncoder']];
582
583
        $audioOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
584
585
        return $this->getFilename($filePath, $audioOptions);
586
    }
587
588
    /**
589
     * Get the name of a gif video file from a path and options
590
     *
591
     * @param Asset|string $filePath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
592
     * @param array $gifOptions
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
593
     *
594
     * @return string
595
     * @throws InvalidConfigException
596
     */
597
    public function getGifFilename(Asset|string $filePath, array $gifOptions): string
598
    {
599
        $settings = Transcoder::$plugin->getSettings();
600
        $gifOptions = $this->coalesceOptions('defaultGifOptions', $gifOptions);
601
602
        // Get the video encoder presets to use
603
        $videoEncoders = $settings['videoEncoders'];
604
        $thisEncoder = $videoEncoders[$gifOptions['videoEncoder']];
605
606
        $gifOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
607
608
        return $this->getFilename($filePath, $gifOptions);
609
    }
610
611
    /**
612
     * Handle generated a thumbnail for the Control Panel
613
     *
614
     * @param DefineAssetThumbUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
615
     *
616
     * @return null|false|string
617
     * @throws InvalidConfigException
618
     */
619
    public function handleGetAssetThumbPath(DefineAssetThumbUrlEvent $event): null|false|string
620
    {
621
        $options = [
622
            'width' => $event->width,
623
            'height' => $event->height,
624
        ];
625
        return $this->getVideoThumbnailUrl($event->asset, $options);
626
    }
627
628
    // Protected Methods
629
    // =========================================================================
630
631
    /**
632
     * Returns a URL to an encoded GIF file (mp4)
633
     *
634
     * @param Asset|string $filePath path to the original video or an Asset
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
635
     * @param array $gifOptions of options for the GIF file
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
636
     *
637
     * @return string|false|null URL or path of the GIF file
638
     * @throws InvalidConfigException
639
     */
0 ignored issues
show
Coding Style introduced by
There must be no blank lines after the function comment
Loading history...
640
641
    public function getGifUrl(Asset|string $filePath, array $gifOptions): string|false|null
642
    {
643
        $result = '';
644
        $settings = Transcoder::$plugin->getSettings();
645
        $subfolder = '';
646
647
        // sub folder check
648
        if (($filePath instanceof Asset) && $settings['createSubfolders']) {
649
            $subfolder = $filePath->folderPath;
650
        }
651
652
        $filePath = $this->getAssetPath($filePath);
653
654
        if (!empty($filePath)) {
655
            // Dest path
656
            $destVideoPath = $settings['transcoderPaths']['gif'] ?? $settings['transcoderPaths']['default'];
657
            $destVideoPath .= $subfolder;
658
            $destVideoPath = App::parseEnv($destVideoPath);
659
660
            // Options
661
            $gifOptions = $this->coalesceOptions('defaultGifOptions', $gifOptions);
662
663
            // Get the video encoder presets to use
664
            $videoEncoders = $settings['videoEncoders'];
665
            $thisEncoder = $videoEncoders[$gifOptions['videoEncoder']];
666
            $gifOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
667
668
            // Build the basic command for ffmpeg
669
            $ffmpegCmd = $settings['ffmpegPath']
670
                . ' -f gif'
671
                . ' -i ' . escapeshellarg($filePath)
672
                . ' -vcodec ' . $thisEncoder['videoCodec']
673
                . ' ' . $thisEncoder['videoCodecOptions'];
674
675
676
            // Create the directory if it isn't there already
677
            if (!is_dir($destVideoPath)) {
0 ignored issues
show
Bug introduced by
It seems like $destVideoPath can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

677
            if (!is_dir(/** @scrutinizer ignore-type */ $destVideoPath)) {
Loading history...
678
                try {
679
                    FileHelper::createDirectory($destVideoPath);
680
                } catch (Exception $e) {
681
                    Craft::error($e->getMessage(), __METHOD__);
682
                }
683
            }
684
685
            $destVideoFile = $this->getFilename($filePath, $gifOptions);
686
687
            // File to store the video encoding progress in
688
            $progressFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $destVideoFile . '.progress';
689
690
            // Assemble the destination path and final ffmpeg command
691
            $destVideoPath .= $destVideoFile;
692
            $ffmpegCmd .= ' '
693
                . ' -y ' . escapeshellarg($destVideoPath)
694
                . ' 1> ' . $progressFile . ' 2>&1 & echo $!';
695
696
            // Make sure there isn't a lockfile for this video already
697
            $lockFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $destVideoFile . '.lock';
698
            $oldPid = @file_get_contents($lockFile);
699
            if ($oldPid !== false) {
700
                // See if the process is running, and empty result means the process is still running
701
                // ref: https://stackoverflow.com/questions/3043978/how-to-check-if-a-process-id-pid-exists
702
                exec("kill -0 $oldPid 2>&1", $ProcessState);
703
                if (count($ProcessState) === 0) {
704
                    return $result;
705
                }
706
                // It's finished transcoding, so delete the lockfile and progress file
707
                @unlink($lockFile);
708
                @unlink($progressFile);
709
            }
710
711
            // If the video file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
712
            if (file_exists($destVideoPath) && (@filemtime($destVideoPath) >= @filemtime($filePath))) {
713
                $url = $settings['transcoderUrls']['gif'] ?? $settings['transcoderUrls']['default'];
714
                $url .= $subfolder;
715
                $result = App::parseEnv($url) . $destVideoFile;
716
            } else {
717
                // Kick off the transcoding
718
                $pid = $this->executeShellCommand($ffmpegCmd);
719
                Craft::info($ffmpegCmd . "\nffmpeg PID: " . $pid, __METHOD__);
720
721
                // Create a lockfile in tmp
722
                file_put_contents($lockFile, $pid);
723
            }
724
        }
725
726
        return $result;
727
    }
728
729
    /**
730