Test Failed
Branch master (df1c42)
by Mostafa
15:23
created

FFMpeg::manipulate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 10
rs 10
1
<?php
2
3
namespace Mostafaznv\Larupload\Storage\FFMpeg;
4
5
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
6
use FFMpeg\Coordinate\Dimension;
7
use FFMpeg\Coordinate\TimeCode;
8
use FFMpeg\Exception\RuntimeException;
9
use FFMpeg\FFMpeg as FFMpegLib;
10
use FFMpeg\Filters\Video\ResizeFilter;
11
use FFMpeg\Format\Video\X264;
12
use FFMpeg\Media\Audio;
13
use FFMpeg\Media\Video;
14
use Illuminate\Http\UploadedFile;
15
use Illuminate\Support\Facades\Log;
16
use Mostafaznv\Larupload\DTOs\FFMpeg\FFMpegMeta;
17
use Mostafaznv\Larupload\DTOs\Style\ImageStyle;
18
use Mostafaznv\Larupload\DTOs\Style\StreamStyle;
19
use Mostafaznv\Larupload\DTOs\Style\VideoStyle;
20
use Mostafaznv\Larupload\Enums\LaruploadImageLibrary;
21
use Mostafaznv\Larupload\Storage\Image;
22
use Psr\Log\LoggerInterface;
23
24
class FFMpeg
25
{
26
    private readonly UploadedFile $file;
27
28
    private readonly string $disk;
29
30
    private readonly int $dominantColorQuality;
31
32
    private FFMpegMeta $meta;
33
34
    private Video|Audio $media;
35
36
37
    /**
38
     * Default scale size
39
     * we use this value if width and height both were undefined
40
     *
41
     * @var integer
42
     */
43
    private const DEFAULT_SCALE = 850;
44
45
46
    public function __construct(UploadedFile $file, string $disk, int $dominantColorQuality)
47
    {
48
        $this->file = $file;
0 ignored issues
show
Bug introduced by
The property file is declared read-only in Mostafaznv\Larupload\Storage\FFMpeg\FFMpeg.
Loading history...
49
        $this->disk = $disk;
0 ignored issues
show
Bug introduced by
The property disk is declared read-only in Mostafaznv\Larupload\Storage\FFMpeg\FFMpeg.
Loading history...
50
        $this->dominantColorQuality = $dominantColorQuality;
0 ignored issues
show
Bug introduced by
The property dominantColorQuality is declared read-only in Mostafaznv\Larupload\Storage\FFMpeg\FFMpeg.
Loading history...
51
52
        $config = config('larupload.ffmpeg');
53
54
        $ffmpeg = FFMpegLib::create([
55
            'ffmpeg.binaries'  => $config['ffmpeg-binaries'],
56
            'ffprobe.binaries' => $config['ffprobe-binaries'],
57
            'timeout'          => $config['timeout'],
58
            'ffmpeg.threads'   => $config['threads'] ?? 12,
59
        ], $this->logChannel());
60
61
        $this->media = $ffmpeg->open($file->getRealPath());
62
    }
63
64
65
    public function getMedia(): Video|Audio
66
    {
67
        return $this->media;
68
    }
69
70
    public function getMeta(): FFMpegMeta
71
    {
72
        if (empty($this->meta)) {
73
            $meta = $this->media->getStreams()->first()->all();
74
75
            // support rotate tag in old ffmpeg versions
76
            if (isset($meta['tags']['rotate'])) {
77
                // @codeCoverageIgnoreStart
78
                $rotate = $meta['tags']['rotate'];
79
80
                if ($rotate == 90 or $rotate == 270) {
81
                    list($meta['height'], $meta['width']) = array($meta['width'], $meta['height']);
82
                }
83
                // @codeCoverageIgnoreEnd
84
            }
85
86
            $this->meta = FFMpegMeta::make(
87
                width: $meta['width'] ?? null,
88
                height: $meta['height'] ?? null,
89
                duration: $meta['duration']
90
            );
91
        }
92
93
        return $this->meta;
94
    }
95
96
    public function setMeta(FFMpegMeta $meta): self
97
    {
98
        $this->meta = $meta;
99
100
        return $this;
101
    }
102
103
    public function capture(int|float|null $fromSeconds, ImageStyle $style, string $saveTo, bool $withDominantColor = false): ?string
104
    {
105
        $dominantColor = null;
106
        $saveTo = get_larupload_save_path($this->disk, $saveTo);
107
108
109
        $style->mode->ffmpegResizeFilter()
110
            ? $this->resize($style)
111
            : $this->crop($style);
112
113
114
        $this->frame($fromSeconds, $saveTo);
115
116
        if ($withDominantColor) {
117
            $dominantColor = $this->dominantColor($saveTo['local']);
118
        }
119
120
        larupload_finalize_save($this->disk, $saveTo);
121
122
        return $dominantColor;
123
    }
124
125
    public function manipulate(VideoStyle $style, string $saveTo): void
126
    {
127
        $saveTo = get_larupload_save_path($this->disk, $saveTo);
128
129
        $style->mode->ffmpegResizeFilter()
130
            ? $this->resize($style)
131
            : $this->crop($style);
132
133
        $this->media->save($style->format, $saveTo['local']);
134
        larupload_finalize_save($this->disk, $saveTo);
135
    }
136
137
    public function stream(array $styles, string $basePath, string $fileName): bool
138
    {
139
        $hls = new HLS($this, $this->disk);
140
141
        return $hls->export($styles, $basePath, $fileName);
142
    }
143
144
    public function resize(VideoStyle|ImageStyle|StreamStyle $style): void
145
    {
146
        $dimension = $this->dimension($style);
147
148
        if ($style->padding) {
149
            $this->media->filters()->pad($dimension)->synchronize();
0 ignored issues
show
introduced by
The method synchronize() does not exist on FFMpeg\Filters\Audio\AudioFilters. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

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

149
            $this->media->filters()->pad($dimension)->/** @scrutinizer ignore-call */ synchronize();
Loading history...
introduced by
The method pad() does not exist on FFMpeg\Filters\Audio\AudioFilters. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

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

149
            $this->media->filters()->/** @scrutinizer ignore-call */ pad($dimension)->synchronize();
Loading history...
150
        }
151
        else {
152
            $mode = $style->mode->ffmpegResizeFilter() ?? ResizeFilter::RESIZEMODE_SCALE_HEIGHT;
153
154
            $this->media->filters()
155
                ->resize($dimension, $mode)
0 ignored issues
show
introduced by
The method resize() does not exist on FFMpeg\Filters\Audio\AudioFilters. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

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

155
                ->/** @scrutinizer ignore-call */ resize($dimension, $mode)
Loading history...
156
                ->synchronize();
157
        }
158
    }
159
160
    public function crop(VideoStyle|ImageStyle|StreamStyle $style): void
161
    {
162
        $meta = $this->getMeta();
163
        $width = $style->width ?? null;
164
        $height = $style->height ?? null;
165
        $scale = $width ?: ($height ?: self::DEFAULT_SCALE);
166
        $scaleType = $meta->width >= $meta->height ? "-1:$scale" : "$scale:-1";
167
168
        if ($width and $height) {
169
            $this->media->filters()->custom("scale=$scaleType,crop=$width:$height,setsar=1");
170
        }
171
        else {
172
            /**
173
             * With new validation rules in Video/Image/Stream style, this code will never happen
174
             */
175
            // @codeCoverageIgnoreStart
176
            $this->media->filters()->custom("scale=$scaleType,crop=$scale:$scale,setsar=1");
177
            // @codeCoverageIgnoreEnd
178
        }
179
    }
180
181
    private function frame(int|float|null $fromSeconds, array $saveTo): void
182
    {
183
        if (is_null($fromSeconds)) {
184
            $fromSeconds = $this->getMeta()->duration / 2;
185
        }
186
187
        $saveToPath = $saveTo['local'];
188
        $commands = [
189
            '-y', '-ss', (string)TimeCode::fromSeconds($fromSeconds),
190
            '-i', $this->media->getPathfile(),
191
            '-vframes', '1',
192
            '-f', 'image2',
193
        ];
194
195
196
        foreach ($this->media->getFiltersCollection() as $filter) {
197
            $commands = array_merge($commands, $filter->apply($this->media, new X264));
198
        }
199
200
        $commands = array_merge($commands, [$saveToPath]);
201
202
        try {
203
            $this->media->getFFMpegDriver()->command($commands);
204
        }
205
        catch (ExecutionFailureException $e) {
206
            if (file_exists($saveToPath) && is_writable($saveToPath)) {
207
                // @codeCoverageIgnoreStart
208
                @unlink($saveToPath);
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

208
                /** @scrutinizer ignore-unhandled */ @unlink($saveToPath);

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...
209
                // @codeCoverageIgnoreEnd
210
            }
211
212
            throw new RuntimeException('Unable to save frame', $e->getCode(), $e);
213
        }
214
    }
215
216
    public function clone(bool $withMeta = false): FFMpeg
217
    {
218
        $ffmpeg = new self($this->file, $this->disk, $this->dominantColorQuality);
219
220
        if ($withMeta) {
221
            $ffmpeg->setMeta($this->getMeta());
222
        }
223
224
        return $ffmpeg;
225
    }
226
227
    private function logChannel(): ?LoggerInterface
228
    {
229
        $channel = config('larupload.ffmpeg.log-channel');
230
231
        if ($channel === false) {
232
            return null;
233
        }
234
235
        return Log::channel($channel ?: config('logging.default'));
236
    }
237
238
    private function dimension(VideoStyle|ImageStyle|StreamStyle $style): Dimension
239
    {
240
        $width = $style->width ?: (!$style->height ? self::DEFAULT_SCALE : 1);
241
        $height = $style->height ?: (!$style->width ? self::DEFAULT_SCALE : 1);
242
243
        return new Dimension($width, $height);
244
    }
245
246
    private function dominantColor($path): ?string
247
    {
248
        $file = new UploadedFile($path, basename($path));
249
        $image = new Image($file, $this->disk, LaruploadImageLibrary::GD, $this->dominantColorQuality);
250
251
        return $image->getDominantColor();
252
    }
253
}
254