Passed
Push — develop ( ede75a...0e6ef8 )
by Andrew
10:42 queued 06:12
created

Transcode::getVideoFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
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
    /**
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
     * @param bool   $generate         whether the video should be encoded
0 ignored issues
show
Coding Style introduced by
Expected 58 spaces after parameter type; 3 found
Loading history...
Coding Style introduced by
Expected 1 spaces after parameter name; 9 found
Loading history...
90
     *
91
     * @return string       URL of the transcoded video or ""
92
     */
93
    public function getVideoUrl($filePath, $videoOptions, $generate = true): string
94
    {
95
	    	    
96
        $result = '';
97
        $settings = Transcoder::$plugin->getSettings();
98
		$subfolder = '';
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
99
		
100
		// sub folder check
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
101
		if(\is_object($filePath) && ($filePath instanceof Asset) && $settings['createSubfolders']) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 2
Loading history...
Coding Style introduced by
Expected "if (...) {\n"; found "if(...) {\n"
Loading history...
102
			$subfolder = $filePath->folderPath;
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 12 spaces, found 3
Loading history...
103
		}
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 2
Loading history...
104
105
		// file path
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
106
        $filePath = $this->getAssetPath($filePath);
107
				
108
        if (!empty($filePath)) {
109
            $destVideoPath = $settings['transcoderPaths']['video'] . $subfolder ?? $settings['transcoderPaths']['default'];
110
            $destVideoPath = Craft::getAlias($destVideoPath);           
111
            $videoOptions = $this->coalesceOptions('defaultVideoOptions', $videoOptions);
112
113
            // Get the video encoder presets to use
114
            $videoEncoders = $settings['videoEncoders'];
115
            $thisEncoder = $videoEncoders[$videoOptions['videoEncoder']];
116
117
            $videoOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
118
119
            // Build the basic command for ffmpeg
120
            $ffmpegCmd = $settings['ffmpegPath']
121
                .' -i '.escapeshellarg($filePath)
122
                .' -vcodec '.$thisEncoder['videoCodec']
123
                .' '.$thisEncoder['videoCodecOptions']
124
                .' -bufsize 1000k'
125
                .' -threads '.$thisEncoder['threads'];
126
127
            // Set the framerate if desired
128
            if (!empty($videoOptions['videoFrameRate'])) {
129
                $ffmpegCmd .= ' -r '.$videoOptions['videoFrameRate'];
130
            }
131
132
            // Set the bitrate if desired
133
            if (!empty($videoOptions['videoBitRate'])) {
134
                $ffmpegCmd .= ' -b:v '.$videoOptions['videoBitRate'].' -maxrate '.$videoOptions['videoBitRate'];
135
            }
136
137
            // Adjust the scaling if desired
138
            $ffmpegCmd = $this->addScalingFfmpegArgs(
139
                $videoOptions,
140
                $ffmpegCmd
141
            );
142
143
            // Handle any audio transcoding
144
            if (empty($videoOptions['audioBitRate'])
145
                && empty($videoOptions['audioSampleRate'])
146
                && empty($videoOptions['audioChannels'])
147
            ) {
148
                // Just copy the audio if no options are provided
149
                $ffmpegCmd .= ' -c:a copy';
150
            } else {
151
                // Do audio transcoding based on the settings
152
                $ffmpegCmd .= ' -acodec '.$thisEncoder['audioCodec'];
153
                if (!empty($videoOptions['audioBitRate'])) {
154
                    $ffmpegCmd .= ' -b:a '.$videoOptions['audioBitRate'];
155
                }
156
                if (!empty($videoOptions['audioSampleRate'])) {
157
                    $ffmpegCmd .= ' -ar '.$videoOptions['audioSampleRate'];
158
                }
159
                if (!empty($videoOptions['audioChannels'])) {
160
                    $ffmpegCmd .= ' -ac '.$videoOptions['audioChannels'];
161
                }
162
                $ffmpegCmd .= ' '.$thisEncoder['audioCodecOptions'];
163
            }
164
165
            // Create the directory if it isn't there already
166
            if (!is_dir($destVideoPath)) {
167
                try {
168
                    FileHelper::createDirectory($destVideoPath);
169
                } catch (Exception $e) {
170
                    Craft::error($e->getMessage(), __METHOD__);
171
                }
172
            }
173
174
            $destVideoFile = $this->getFilename($filePath, $videoOptions);
175
176
            // File to store the video encoding progress in
177
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.progress';
178
179
            // Assemble the destination path and final ffmpeg command
180
            $destVideoPath .= $destVideoFile;
181
            $ffmpegCmd .= ' -f '
182
                .$thisEncoder['fileFormat']
183
                .' -y '.escapeshellarg($destVideoPath)
184
                .' 1> '.$progressFile.' 2>&1 & echo $!';
185
186
            // Make sure there isn't a lockfile for this video already
187
            $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destVideoFile.'.lock';
188
            $oldPid = @file_get_contents($lockFile);
189
            if ($oldPid !== false) {
190
                exec("ps $oldPid", $ProcessState);
191
                if (\count($ProcessState) >= 2) {
192
                    return $result;
193
                }
194
                // It's finished transcoding, so delete the lockfile and progress file
195
                @unlink($lockFile);
196
                @unlink($progressFile);
197
            }
198
199
            // If the video file already exists and hasn't been modified, return it.  Otherwise, start it transcoding
200
            if (file_exists($destVideoPath) && (@filemtime($destVideoPath) >= @filemtime($filePath))) {
201
                $url = $settings['transcoderUrls']['video'] . $subfolder ?? $settings['transcoderUrls']['default'];
202
                $result = Craft::getAlias($url).$destVideoFile;
203
                
204
            // skip encoding
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 16 spaces, found 12
Loading history...
205
            } elseif (!$generate) {
206
	            $result = "";
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 16 spaces, found 13
Loading history...
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 = '';
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
236
		
237
		// sub folder check
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
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...
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 2
Loading history...
Coding Style introduced by
Expected "if (...) {\n"; found "if(...) {\n"
Loading history...
239
			$subfolder = $filePath->folderPath;
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 12 spaces, found 3
Loading history...
240
		}
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 2
Loading history...
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
                    
295
                } else {
296
                    Craft::info('Thumbnail does not exist, but not asked to generate it: '.$filePath, __METHOD__);
297
298
                    // The file doesn't exist, and we weren't asked to generate it
299
                    return false;
300
                }
301
            }
302
            // Return either a path or a URL
303
            if ($asPath) {
304
                $result = $destThumbnailPath;
305
            } else {
306
                $url = $settings['transcoderUrls']['thumbnail'] . $subfolder ?? $settings['transcoderUrls']['default'];
307
                $result = Craft::getAlias($url).$destThumbnailFile;
308
            }
309
        }
310
311
        return $result;
312
    }
313
314
    /**
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...
315
     * Returns a URL to the transcoded audio file or "" if it doesn't exist
316
     * (at which time it will create it).
317
     *
318
     * @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...
319
     * @param $audioOptions array of options for the audio file
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
320
     *
321
     * @return string       URL of the transcoded audio file or ""
322
     */
323
    public function getAudioUrl($filePath, $audioOptions): string
324
    {
325
        $result = '';
326
        $settings = Transcoder::$plugin->getSettings();
327
		$subfolder = '';
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
328
		
329
		// sub folder check
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 8 spaces, found 2
Loading history...
330
		if(\is_object($filePath) && ($filePath instanceof Asset) && $settings['createSubfolders']) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 2
Loading history...
Coding Style introduced by
Expected "if (...) {\n"; found "if(...) {\n"
Loading history...
331
			$subfolder = $filePath->folderPath;
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected at least 12 spaces, found 3
Loading history...
332
		}
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 2
Loading history...
333
		
334
        $filePath = $this->getAssetPath($filePath);
335
336
        if (!empty($filePath)) {
337
            $destAudioPath = $settings['transcoderPaths']['audio'] . $subfolder ?? $settings['transcoderPaths']['default'];
338
            $destAudioPath = Craft::getAlias($destAudioPath);
339
340
            $audioOptions = $this->coalesceOptions('defaultAudioOptions', $audioOptions);
341
342
            // Get the audio encoder presets to use
343
            $audioEncoders = $settings['audioEncoders'];
344
            $thisEncoder = $audioEncoders[$audioOptions['audioEncoder']];
345
346
            $audioOptions['fileSuffix'] = $thisEncoder['fileSuffix'];
347
348
            // Build the basic command for ffmpeg
349
            $ffmpegCmd = $settings['ffmpegPath']
350
                .' -i '.escapeshellarg($filePath)
351
                .' -acodec '.$thisEncoder['audioCodec']
352
                .' '.$thisEncoder['audioCodecOptions']
353
                .' -bufsize 1000k'
354
                .' -threads '.$thisEncoder['threads'];
355
356
            // Set the bitrate if desired
357
            if (!empty($audioOptions['audioBitRate'])) {
358
                $ffmpegCmd .= ' -b:a '.$audioOptions['audioBitRate'];
359
            }
360
            // Set the sample rate if desired
361
            if (!empty($audioOptions['audioSampleRate'])) {
362
                $ffmpegCmd .= ' -ar '.$audioOptions['audioSampleRate'];
363
            }
364
            // Set the audio channels if desired
365
            if (!empty($audioOptions['audioChannels'])) {
366
                $ffmpegCmd .= ' -ac '.$audioOptions['audioChannels'];
367
            }
368
            $ffmpegCmd .= ' '.$thisEncoder['audioCodecOptions'];
369
370
371
            // Create the directory if it isn't there already
372
            if (!is_dir($destAudioPath)) {
373
                try {
374
                    FileHelper::createDirectory($destAudioPath);
375
                } catch (Exception $e) {
376
                    Craft::error($e->getMessage(), __METHOD__);
377
                }
378
            }
379
380
            $destAudioFile = $this->getFilename($filePath, $audioOptions);
381
382
            // File to store the audio encoding progress in
383
            $progressFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destAudioFile.'.progress';
384
385
            // Assemble the destination path and final ffmpeg command
386
            $destAudioPath .= $destAudioFile;
387
            $ffmpegCmd .= ' -f '
388
                .$thisEncoder['fileFormat']
389
                .' -y '.escapeshellarg($destAudioPath)
390
                .' 1> '.$progressFile.' 2>&1 & echo $!';
391
392
            // Make sure there isn't a lockfile for this audio file already
393
            $lockFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.$destAudioFile.'.lock';
394
            $oldPid = @file_get_contents($lockFile);
395
            if ($oldPid !== false) {
396
                exec("ps $oldPid", $ProcessState);
397
                if (\count($ProcessState) >= 2) {
398
                    return $result;
399
                }
400
                // It's finished transcoding, so delete the lockfile and progress file
401
                @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

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