FFMpeg::logChannel()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 9
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\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
            // in some formats like webm, duration is not available in streams, so we should get it from format
89
            if (!isset($meta['duration'])) {
90
                $meta['duration'] = $this->media->getFormat()->get('duration', 0);
91
            }
92
93
            $this->meta = FFMpegMeta::make(
94
                width: $meta['width'] ?? null,
95
                height: $meta['height'] ?? null,
96
                duration: $meta['duration']
97
            );
98
        }
99
100
        return $this->meta;
101
    }
102
103
    public function setMeta(FFMpegMeta $meta): self
104
    {
105
        $this->meta = $meta;
106
107
        return $this;
108
    }
109
110
    public function capture(int|float|null $fromSeconds, ImageStyle $style, string $saveTo, bool $withDominantColor = false): ?string
111
    {
112
        $dominantColor = null;
113
        $saveTo = get_larupload_save_path($this->disk, $saveTo);
114
115
116
        $style->mode->ffmpegResizeFilter()
117
            ? $this->resize($style)
118
            : $this->crop($style);
119
120
121
        $this->frame($fromSeconds, $saveTo);
122
123
        if ($withDominantColor) {
124
            $dominantColor = $this->dominantColor($saveTo['local']);
125
        }
126
127
        larupload_finalize_save($this->disk, $saveTo);
128
129
        return $dominantColor;
130
    }
131
132
    public function audio(AudioStyle $style, string $saveTo): void
133
    {
134
        $saveTo = get_larupload_save_path($this->disk, $saveTo, $style->extension());
135
136
        $this->media->save($style->format, $saveTo['local']);
137
138
        larupload_finalize_save($this->disk, $saveTo);
139
    }
140
141
    public function manipulate(VideoStyle $style, string $saveTo): void
142
    {
143
        $saveTo = get_larupload_save_path($this->disk, $saveTo, $style->extension());
144
145
        $style->mode->ffmpegResizeFilter()
146
            ? $this->resize($style)
147
            : $this->crop($style);
148
149
        $this->media->save($style->format, $saveTo['local']);
150
        larupload_finalize_save($this->disk, $saveTo);
151
    }
152
153
    public function stream(array $styles, string $basePath, string $fileName): bool
154
    {
155
        $hls = new HLS($this, $this->disk);
156
157
        return $hls->export($styles, $basePath, $fileName);
158
    }
159
160
    public function resize(VideoStyle|ImageStyle|StreamStyle $style): void
161
    {
162
        $dimension = $this->dimension($style);
163
164
        if ($style->padding) {
165
            $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

165
            $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

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

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

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