Test Failed
Push — master ( ba03f2...dee2b3 )
by Mostafa
58s queued 15s
created

FFMpeg::audio()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
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\AudioStyle;
18
use Mostafaznv\Larupload\DTOs\Style\ImageStyle;
19
use Mostafaznv\Larupload\DTOs\Style\StreamStyle;
20
use Mostafaznv\Larupload\DTOs\Style\VideoStyle;
21
use Mostafaznv\Larupload\Enums\LaruploadImageLibrary;
22
use Mostafaznv\Larupload\Storage\Image;
23
use Psr\Log\LoggerInterface;
24
25
26
class FFMpeg
27
{
28
    private readonly UploadedFile $file;
29
30
    private readonly string $disk;
31
32
    private readonly int $dominantColorQuality;
33
34
    private FFMpegMeta $meta;
35
36
    private Video|Audio $media;
37
38
39
    /**
40
     * Default scale size
41
     * we use this value if width and height both were undefined
42
     *
43
     * @var integer
44
     */
45
    private const DEFAULT_SCALE = 850;
46
47
48
    public function __construct(UploadedFile $file, string $disk, int $dominantColorQuality)
49
    {
50
        $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...
51
        $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...
52
        $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...
53
54
        $config = config('larupload.ffmpeg');
55
56
        $ffmpeg = FFMpegLib::create([
57
            'ffmpeg.binaries'  => $config['ffmpeg-binaries'],
58
            'ffprobe.binaries' => $config['ffprobe-binaries'],
59
            'timeout'          => $config['timeout'],
60
            'ffmpeg.threads'   => $config['threads'] ?? 12,
61
        ], $this->logChannel());
62
63
        $this->media = $ffmpeg->open($file->getRealPath());
64
    }
65
66
67
    public function getMedia(): Video|Audio
68
    {
69
        return $this->media;
70
    }
71
72
    public function getMeta(): FFMpegMeta
73
    {
74
        if (empty($this->meta)) {
75
            $meta = $this->media->getStreams()->first()->all();
76
77
            // support rotate tag in old ffmpeg versions
78
            if (isset($meta['tags']['rotate'])) {
79
                // @codeCoverageIgnoreStart
80
                $rotate = $meta['tags']['rotate'];
81
82
                if ($rotate == 90 or $rotate == 270) {
83
                    list($meta['height'], $meta['width']) = array($meta['width'], $meta['height']);
84
                }
85
                // @codeCoverageIgnoreEnd
86
            }
87
88
            $this->meta = FFMpegMeta::make(
89
                width: $meta['width'] ?? null,
90
                height: $meta['height'] ?? null,
91
                duration: $meta['duration']
92
            );
93
        }
94
95
        return $this->meta;
96
    }
97
98
    public function setMeta(FFMpegMeta $meta): self
99
    {
100
        $this->meta = $meta;
101
102
        return $this;
103
    }
104
105
    public function capture(int|float|null $fromSeconds, ImageStyle $style, string $saveTo, bool $withDominantColor = false): ?string
106
    {
107
        $dominantColor = null;
108
        $saveTo = get_larupload_save_path($this->disk, $saveTo);
109
110
111
        $style->mode->ffmpegResizeFilter()
112
            ? $this->resize($style)
113
            : $this->crop($style);
114
115
116
        $this->frame($fromSeconds, $saveTo);
117
118
        if ($withDominantColor) {
119
            $dominantColor = $this->dominantColor($saveTo['local']);
120
        }
121
122
        larupload_finalize_save($this->disk, $saveTo);
123
124
        return $dominantColor;
125
    }
126
127
    public function audio(AudioStyle $style, string $saveTo): void
128
    {
129
        $saveTo = get_larupload_save_path($this->disk, $saveTo, $style->extension());
130
131
        $this->media->save($style->format, $saveTo['local']);
132
133
        larupload_finalize_save($this->disk, $saveTo);
134
    }
135
136
    public function manipulate(VideoStyle $style, string $saveTo): void
137
    {
138
        $saveTo = get_larupload_save_path($this->disk, $saveTo);
139
140
        $style->mode->ffmpegResizeFilter()
141
            ? $this->resize($style)
142
            : $this->crop($style);
143
144
        $this->media->save($style->format, $saveTo['local']);
145
        larupload_finalize_save($this->disk, $saveTo);
146
    }
147
148
    public function stream(array $styles, string $basePath, string $fileName): bool
149
    {
150
        $hls = new HLS($this, $this->disk);
151
152
        return $hls->export($styles, $basePath, $fileName);
153
    }
154
155
    public function resize(VideoStyle|ImageStyle|StreamStyle $style): void
156
    {
157
        $dimension = $this->dimension($style);
158
159
        if ($style->padding) {
160
            $this->media->filters()->pad($dimension)->synchronize();
0 ignored issues
show
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

160
            $this->media->filters()->/** @scrutinizer ignore-call */ pad($dimension)->synchronize();
Loading history...
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

160
            $this->media->filters()->pad($dimension)->/** @scrutinizer ignore-call */ synchronize();
Loading history...
161
        }
162
        else {
163
            $mode = $style->mode->ffmpegResizeFilter() ?? ResizeFilter::RESIZEMODE_SCALE_HEIGHT;
164
165
            $this->media->filters()
166
                ->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

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

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