Passed
Push — master ( 04eafc...41f9d0 )
by Pascal
05:11 queued 02:24
created

PHPFFMpeg::addWatermark()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace ProtoneMedia\LaravelFFMpeg\Drivers;
4
5
use Closure;
6
use Exception;
7
use FFMpeg\Coordinate\TimeCode;
8
use FFMpeg\FFMpeg;
9
use FFMpeg\FFProbe\DataMapping\Stream;
10
use FFMpeg\Filters\Audio\SimpleFilter;
11
use FFMpeg\Filters\FilterInterface;
12
use FFMpeg\Media\AbstractMediaType;
13
use FFMpeg\Media\AdvancedMedia as BaseAdvancedMedia;
14
use FFMpeg\Media\Concat;
15
use FFMpeg\Media\Frame;
16
use FFMpeg\Media\Video;
17
use Illuminate\Support\Arr;
18
use Illuminate\Support\Collection;
19
use Illuminate\Support\Traits\ForwardsCalls;
20
use ProtoneMedia\LaravelFFMpeg\Exporters\MediaExporter;
21
use ProtoneMedia\LaravelFFMpeg\FFMpeg\AdvancedMedia;
22
use ProtoneMedia\LaravelFFMpeg\FFMpeg\AudioMedia;
23
use ProtoneMedia\LaravelFFMpeg\FFMpeg\FFProbe;
24
use ProtoneMedia\LaravelFFMpeg\FFMpeg\LegacyFilterMapping;
25
use ProtoneMedia\LaravelFFMpeg\FFMpeg\VideoMedia;
26
use ProtoneMedia\LaravelFFMpeg\Filesystem\Media;
27
use ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection;
28
use ProtoneMedia\LaravelFFMpeg\Filters\WatermarkFactory;
29
30
/**
31
 * @mixin \FFMpeg\Media\AbstractMediaType
32
 */
33
class PHPFFMpeg
34
{
35
    use ForwardsCalls;
36
37
    /**
38
     * @var \FFMpeg\FFMpeg
39
     */
40
    private $ffmpeg;
41
42
    /**
43
     * @var \ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection
44
     */
45
    private $mediaCollection;
46
47
    /**
48
     * @var boolean
49
     */
50
    private $forceAdvanced = false;
51
52
    /**
53
     * @var \Illuminate\Support\Collection
54
     */
55
    private $pendingComplexFilters;
56
57
    /**
58
     * @var \FFMpeg\Media\AbstractMediaType
59
     */
60
    private $media;
61
62
    public function __construct(FFMpeg $ffmpeg)
63
    {
64
        $this->ffmpeg                = $ffmpeg;
65
        $this->pendingComplexFilters = new Collection;
66
    }
67
68
    /**
69
     * Returns a fresh instance of itself with only the underlying FFMpeg instance.
70
     */
71
    public function fresh(): self
72
    {
73
        return new static($this->ffmpeg);
74
    }
75
76
    public function get(): AbstractMediaType
77
    {
78
        return $this->media;
79
    }
80
81
    private function isAdvancedMedia(): bool
82
    {
83
        return $this->get() instanceof BaseAdvancedMedia;
84
    }
85
86
    public function isFrame(): bool
87
    {
88
        return $this->get() instanceof Frame;
89
    }
90
91
    public function isConcat(): bool
92
    {
93
        return $this->get() instanceof Concat;
94
    }
95
96
    public function isVideo(): bool
97
    {
98
        return $this->get() instanceof Video;
99
    }
100
101
    public function getMediaCollection(): MediaCollection
102
    {
103
        return $this->mediaCollection;
104
    }
105
106
    /**
107
     * Opens the MediaCollection if it's not been instanciated yet.
108
     */
109
    public function open(MediaCollection $mediaCollection): self
110
    {
111
        if ($this->media) {
112
            return $this;
113
        }
114
115
        $this->mediaCollection = $mediaCollection;
116
117
        if ($mediaCollection->count() === 1 && !$this->forceAdvanced) {
118
            $media = Arr::first($mediaCollection->collection());
119
120
            $this->ffmpeg->setFFProbe(
121
                FFProbe::make($this->ffmpeg->getFFProbe())->setMedia($media)
122
            );
123
124
            $ffmpegMedia = $this->ffmpeg->open($media->getLocalPath());
125
126
            $this->media = $ffmpegMedia instanceof Video
127
                ? VideoMedia::make($ffmpegMedia)
128
                : AudioMedia::make($ffmpegMedia);
129
130
            $this->media->setHeaders(Arr::first($mediaCollection->getHeaders()) ?: []);
131
        } else {
132
            $ffmpegMedia = $this->ffmpeg->openAdvanced($mediaCollection->getLocalPaths());
133
134
            $this->media = AdvancedMedia::make($ffmpegMedia)
135
                ->setHeaders($mediaCollection->getHeaders());
136
        }
137
138
        return $this;
139
    }
140
141
    public function frame(TimeCode $timecode)
142
    {
143
        if (!$this->isVideo()) {
144
            throw new Exception('Opened media is not a video file.');
145
        }
146
147
        $this->media = $this->media->frame($timecode);
0 ignored issues
show
Bug introduced by
The method frame() does not exist on FFMpeg\Media\AbstractMediaType. It seems like you code against a sub-type of FFMpeg\Media\AbstractMediaType such as FFMpeg\Media\Video. ( Ignorable by Annotation )

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

147
        /** @scrutinizer ignore-call */ 
148
        $this->media = $this->media->frame($timecode);
Loading history...
148
149
        return $this;
150
    }
151
152
    public function concatWithoutTranscoding()
153
    {
154
        $localPaths = $this->mediaCollection->getLocalPaths();
155
156
        $this->media = $this->ffmpeg->open(Arr::first($localPaths))
157
            ->concat($localPaths);
158
159
        return $this;
160
    }
161
162
    /**
163
     * Force 'openAdvanced' when opening the MediaCollection
164
     */
165
    public function openAdvanced(MediaCollection $mediaCollection): self
166
    {
167
        $this->forceAdvanced = true;
168
169
        return $this->open($mediaCollection);
170
    }
171
172
    public function getStreams(): array
173
    {
174
        if (!$this->isAdvancedMedia()) {
175
            return iterator_to_array($this->media->getStreams());
0 ignored issues
show
Bug introduced by
The method getStreams() does not exist on FFMpeg\Media\AbstractMediaType. It seems like you code against a sub-type of FFMpeg\Media\AbstractMediaType such as FFMpeg\Media\AbstractStreamableMedia. ( Ignorable by Annotation )

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

175
            return iterator_to_array($this->media->/** @scrutinizer ignore-call */ getStreams());
Loading history...
176
        }
177
178
        return $this->mediaCollection->map(function (Media $media) {
0 ignored issues
show
Bug introduced by
The method map() does not exist on ProtoneMedia\LaravelFFMp...esystem\MediaCollection. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

178
        return $this->mediaCollection->/** @scrutinizer ignore-call */ map(function (Media $media) {
Loading history...
179
            return $this->fresh()->open(MediaCollection::make([$media]))->getStreams();
180
        })->collapse()->all();
181
    }
182
183
    public function getFilters(): array
184
    {
185
        return iterator_to_array($this->media->getFiltersCollection());
0 ignored issues
show
Bug introduced by
$this->media->getFiltersCollection() of type FFMpeg\Media\MediaTypeInterface is incompatible with the type Traversable expected by parameter $iterator of iterator_to_array(). ( Ignorable by Annotation )

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

185
        return iterator_to_array(/** @scrutinizer ignore-type */ $this->media->getFiltersCollection());
Loading history...
186
    }
187
188
    //
189
190
    public function getDurationInSeconds(): int
191
    {
192
        return round($this->getDurationInMiliseconds() / 1000);
193
    }
194
195
    /**
196
     * Gets the duration of the media from the first stream or from the format.
197
     */
198
    public function getDurationInMiliseconds(): int
199
    {
200
        $stream = Arr::first($this->getStreams());
201
202
        if ($stream->has('duration')) {
203
            return $stream->get('duration') * 1000;
204
        }
205
206
        $format = $this->getFormat();
0 ignored issues
show
Bug introduced by
The method getFormat() does not exist on ProtoneMedia\LaravelFFMpeg\Drivers\PHPFFMpeg. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

206
        /** @scrutinizer ignore-call */ 
207
        $format = $this->getFormat();
Loading history...
207
208
        if ($format->has('duration')) {
0 ignored issues
show
Bug introduced by
The method has() does not exist on ProtoneMedia\LaravelFFMpeg\Drivers\PHPFFMpeg. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

208
        if ($format->/** @scrutinizer ignore-call */ has('duration')) {
Loading history...
209
            return $format->get('duration') * 1000;
0 ignored issues
show
Unused Code introduced by
The call to ProtoneMedia\LaravelFFMp...rivers\PHPFFMpeg::get() has too many arguments starting with 'duration'. ( Ignorable by Annotation )

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

209
            return $format->/** @scrutinizer ignore-call */ get('duration') * 1000;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
210
        }
211
    }
212
213
    /**
214
     * Gets the first audio streams of the media.
215
     */
216
    public function getAudioStream(): ?Stream
217
    {
218
        return Arr::first($this->getStreams(), function (Stream $stream) {
219
            return $stream->isAudio();
220
        });
221
    }
222
223
    /**
224
     * Gets the first video streams of the media.
225
     */
226
    public function getVideoStream(): ?Stream
227
    {
228
        return Arr::first($this->getStreams(), function (Stream $stream) {
229
            return $stream->isVideo();
230
        });
231
    }
232
233
    //
234
235
    /**
236
     * Helper method to provide multiple ways to add a filter to the underlying
237
     * media object.
238
     *
239
     * @return self
240
     */
241
    public function addFilter(): self
242
    {
243
        $arguments = func_get_args();
244
245
        // to support '[in]filter[out]' complex filters
246
        if ($this->isAdvancedMedia() && count($arguments) === 3) {
247
            $this->media->filters()->custom(...$arguments);
248
249
            return $this;
250
        }
251
252
        // use a callback to add a filter
253
        if ($arguments[0] instanceof Closure) {
254
            call_user_func_array($arguments[0], [$this->media->filters()]);
255
256
            return $this;
257
        }
258
259
        // use an object to add a filter
260
        if ($arguments[0] instanceof FilterInterface) {
261
            call_user_func_array([$this->media, 'addFilter'], $arguments);
262
263
            return $this;
264
        }
265
266
        // use a single array with parameters to define a filter
267
        if (is_array($arguments[0])) {
268
            $this->media->addFilter(new SimpleFilter($arguments[0]));
0 ignored issues
show
Bug introduced by
The method addFilter() does not exist on FFMpeg\Media\AbstractMediaType. It seems like you code against a sub-type of said class. However, the method does not exist in FFMpeg\Media\AbstractStreamableMedia. Are you sure you never get one of those? ( Ignorable by Annotation )

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

268
            $this->media->/** @scrutinizer ignore-call */ 
269
                          addFilter(new SimpleFilter($arguments[0]));
Loading history...
269
270
            return $this;
271
        }
272
273
        // use all function arguments as a filter
274
        $this->media->addFilter(new SimpleFilter($arguments));
275
276
        return $this;
277
    }
278
279
    /**
280
     * Calls the callable with a WatermarkFactory instance and
281
     * adds the freshly generated WatermarkFilter.
282
     *
283
     * @param callable $withWatermarkFactory
284
     * @return self
285
     */
286
    public function addWatermark(callable $withWatermarkFactory): self
287
    {
288
        $watermarkFactory = new WatermarkFactory;
289
290
        $withWatermarkFactory($watermarkFactory);
291
292
        $watermarkFilter = $watermarkFactory->get();
293
294
        $this->addFilter($watermarkFilter);
295
296
        return $this;
297
    }
298
299
    /**
300
     * Maps the arguments into a 'LegacyFilterMapping' instance and
301
     * pushed it to the 'pendingComplexFilters' collection. These
302
     * filters will be applied later on by the MediaExporter.
303
     */
304
    public function addFilterAsComplexFilter($in, $out, ...$arguments): self
305
    {
306
        $this->pendingComplexFilters->push(new LegacyFilterMapping(
307
            $in,
308
            $out,
309
            ...$arguments
310
        ));
311
312
        return $this;
313
    }
314
315
    public function getPendingComplexFilters(): Collection
316
    {
317
        return $this->pendingComplexFilters;
318
    }
319
320
    /**
321
     * Returns the underlying media object itself.
322
     */
323
    public function __invoke(): AbstractMediaType
324
    {
325
        return $this->get();
326
    }
327
328
    /**
329
     * Forwards the call to the underling media object and returns the result
330
     * if it's something different than the media object itself.
331
     */
332
    public function __call($method, $arguments)
333
    {
334
        $result = $this->forwardCallTo($media = $this->get(), $method, $arguments);
335
336
        return ($result === $media) ? $this : $result;
337
    }
338
}
339