Passed
Push — develop ( 1e580d...c4488a )
by Andrew
01:31
created

Transcode::getGifUrl()   B

Complexity

Conditions 7
Paths 11

Size

Total Lines 71
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 37
nc 11
nop 2
dl 0
loc 71
rs 8.3946
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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