Passed
Pull Request — master (#243)
by Pascal
02:39
created

HLSExporter.php$0 ➔ scale()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
1
<?php
2
3
namespace ProtoneMedia\LaravelFFMpeg\Exporters;
4
5
use Closure;
6
use FFMpeg\Coordinate\Dimension;
7
use FFMpeg\Filters\AdvancedMedia\ComplexFilters;
8
use FFMpeg\Filters\Video\ResizeFilter;
9
use FFMpeg\Format\FormatInterface;
10
use FFMpeg\Format\Video\DefaultVideo;
11
use FFMpeg\Format\VideoInterface;
12
use Illuminate\Support\Collection;
13
use Illuminate\Support\Fluent;
14
use Illuminate\Support\Str;
15
use ProtoneMedia\LaravelFFMpeg\Filesystem\Disk;
16
use ProtoneMedia\LaravelFFMpeg\Filters\WatermarkFactory;
17
use ProtoneMedia\LaravelFFMpeg\MediaOpener;
18
19
class HLSExporter extends MediaExporter
20
{
21
    const FORMAT_FILTER_MAPPING_GLUE = "_hls_ffms_";
22
23
    /**
24
     * @var integer
25
     */
26
    private $segmentLength = 10;
27
28
    /**
29
    * @var integer
30
    */
31
    private $keyFrameInterval = 48;
32
33
    /**
34
     * @var \Illuminate\Support\Collection
35
     */
36
    private $pendingFormats;
37
38
    /**
39
     * @var \ProtoneMedia\LaravelFFMpeg\Exporters\PlaylistGenerator
40
     */
41
    private $playlistGenerator;
42
43
    /**
44
     * @var \Closure
45
     */
46
    private $segmentFilenameGenerator = null;
47
48
    public function setSegmentLength(int $length): self
49
    {
50
        $this->segmentLength = $length;
51
52
        return $this;
53
    }
54
55
    public function setKeyFrameInterval(int $interval): self
56
    {
57
        $this->keyFrameInterval = $interval;
58
59
        return $this;
60
    }
61
62
    public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self
63
    {
64
        $this->playlistGenerator = $playlistGenerator;
65
66
        return $this;
67
    }
68
69
    private function getPlaylistGenerator(): PlaylistGenerator
70
    {
71
        return $this->playlistGenerator ?: new HLSPlaylistGenerator;
72
    }
73
74
    public function useSegmentFilenameGenerator(Closure $callback): self
75
    {
76
        $this->segmentFilenameGenerator = $callback;
77
78
        return $this;
79
    }
80
81
    private function getSegmentFilenameGenerator(): callable
82
    {
83
        return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) {
84
            $segments("{$name}_{$key}_{$format->getKiloBitrate()}_%05d.ts");
85
            $playlist("{$name}_{$key}_{$format->getKiloBitrate()}.m3u8");
86
        };
87
    }
88
89
    private function getSegmentPatternAndFormatPlaylistPath(string $baseName, VideoInterface $format, int $key): array
90
    {
91
        $segmentsPattern    = null;
92
        $formatPlaylistPath = null;
93
94
        call_user_func(
95
            $this->getSegmentFilenameGenerator(),
96
            $baseName,
97
            $format,
98
            $key,
99
            function ($path) use (&$segmentsPattern) {
100
                $segmentsPattern = $path;
101
            },
102
            function ($path) use (&$formatPlaylistPath) {
103
                $formatPlaylistPath = $path;
104
            }
105
        );
106
107
        return [$segmentsPattern, $formatPlaylistPath];
108
    }
109
110
    private function addHLSParametersToFormat(DefaultVideo $format, string $segmentsPattern, Disk $disk)
111
    {
112
        $format->setAdditionalParameters([
113
            '-sc_threshold',
114
            '0',
115
            '-g',
116
            $this->keyFrameInterval,
117
            '-hls_playlist_type',
118
            'vod',
119
            '-hls_time',
120
            $this->segmentLength,
121
            '-hls_segment_filename',
122
            $disk->makeMedia($segmentsPattern)->getLocalPath(),
123
        ]);
124
    }
125
126
    private function applyFiltersCallback(callable $filtersCallback, $formatKey): array
127
    {
128
        $formatFilters = $this->formatFilters;
129
130
        $mediaMock = new class($this->driver, $formatKey, $formatFilters) {
131
            private $driver;
132
            private $formatKey;
133
            private $formatFilters;
134
135
            public function __construct($driver, $formatKey, $formatFilters)
136
            {
137
                $this->driver        = $driver;
138
                $this->formatKey     = $formatKey;
139
                $this->formatFilters = $formatFilters;
140
            }
141
142
            private function called(): self
143
            {
144
                if (!$this->formatFilters->offsetExists($this->formatKey)) {
145
                    $this->formatFilters[$this->formatKey] = 1;
146
                } else {
147
                    $this->formatFilters[$this->formatKey] = $this->formatFilters[$this->formatKey] + 1;
148
                }
149
150
                return $this;
151
            }
152
153
            private function input(): string
154
            {
155
                $filters = $this->formatFilters->get($this->formatKey, 0);
156
157
                if ($filters < 1) {
158
                    return '[0]';
159
                }
160
161
                return HLSExporter::glue($this->formatKey, $filters);
162
            }
163
164
            private function output(): string
165
            {
166
                $filters = $this->formatFilters->get($this->formatKey, 0) + 1;
167
168
                return HLSExporter::glue($this->formatKey, $filters);
169
            }
170
171
            public function addLegacyFilter(...$arguments): self
172
            {
173
                $this->driver->addFilterAsComplexFilter($this->input(), $this->output(), ...$arguments);
174
175
                return $this->called();
176
            }
177
178
            public function resize($width, $height, $mode = null): self
179
            {
180
                $dimension = new Dimension($width, $height);
181
182
                $filter = new ResizeFilter($dimension, $mode);
183
184
                return $this->addLegacyFilter($filter);
185
            }
186
187
            public function addWatermark(callable $withWatermarkFactory): self
188
            {
189
                $withWatermarkFactory($watermarkFactory = new WatermarkFactory);
190
191
                return $this->addLegacyFilter($watermarkFactory->get());
192
            }
193
194
            public function scale($width, $height): self
195
            {
196
                return $this->addFilter("scale={$width}:{$height}");
197
            }
198
199
            public function addFilter(...$arguments): self
200
            {
201
                if (count($arguments) === 1 && !is_callable($arguments[0])) {
202
                    $this->driver->addFilter($this->input(), $arguments[0], $this->output());
203
                } else {
204
                    $this->driver->addFilter(function (ComplexFilters $filters) use ($arguments) {
205
                        $arguments[0]($filters, $this->input(), $this->output());
206
                    });
207
                }
208
209
                return $this->called();
210
            }
211
        };
212
213
        $filtersCallback($mediaMock);
214
215
        $filters = $formatFilters->get($formatKey, 0);
216
217
        $videoOut = $filters ? static::glue($formatKey, $filters) : '0:v';
218
219
        $outs = [$videoOut];
220
221
        if ($this->getAudioStream()) {
0 ignored issues
show
Bug introduced by
The method getAudioStream() does not exist on ProtoneMedia\LaravelFFMpeg\Exporters\HLSExporter. 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

221
        if ($this->/** @scrutinizer ignore-call */ getAudioStream()) {
Loading history...
222
            $outs[] = '0:a';
223
        }
224
225
        return $outs;
226
    }
227
228
    public static function glue($format, $filter): string
229
    {
230
        return "[v{$format}" . static::FORMAT_FILTER_MAPPING_GLUE . "{$filter}]";
231
    }
232
233
    public static function beforeGlue($input): string
234
    {
235
        return Str::before($input, static::FORMAT_FILTER_MAPPING_GLUE);
236
    }
237
238
    public function save(string $path = null): MediaOpener
239
    {
240
        $media = $this->getDisk()->makeMedia($path);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $path of ProtoneMedia\LaravelFFMp...ystem\Disk::makeMedia() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

240
        $media = $this->getDisk()->makeMedia(/** @scrutinizer ignore-type */ $path);
Loading history...
241
242
        $baseName = $media->getDirectory() . $media->getFilenameWithoutExtension();
243
244
        $this->formatFilters = new Fluent;
0 ignored issues
show
Bug Best Practice introduced by
The property formatFilters does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
245
246
        return $this->pendingFormats->map(function ($formatAndCallback, $key) use ($baseName) {
247
            $disk = $this->getDisk()->clone();
248
249
            [$format, $filtersCallback] = $formatAndCallback;
250
251
            [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath(
252
                $baseName,
253
                $format,
254
                $key
255
            );
256
257
            $this->addHLSParametersToFormat($format, $segmentsPattern, $disk);
258
259
            if ($filtersCallback) {
260
                $outs = $this->applyFiltersCallback($filtersCallback, $key);
261
            }
262
263
            $this->addFormatOutputMapping($format, $disk->makeMedia($formatPlaylistPath), $outs ?? ['0']);
264
265
            return $this->getDisk()->makeMedia($formatPlaylistPath);
266
        })->pipe(function ($playlistMedia) use ($path) {
267
            $result = parent::save();
268
269
            $playlist = $this->getPlaylistGenerator()->get(
270
                $playlistMedia->all(),
271
                $this->driver->fresh()
272
            );
273
274
            $this->getDisk()->put($path, $playlist);
0 ignored issues
show
Bug introduced by
The method put() does not exist on ProtoneMedia\LaravelFFMpeg\Filesystem\Disk. 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

274
            $this->getDisk()->/** @scrutinizer ignore-call */ put($path, $playlist);
Loading history...
275
276
            return $result;
277
        });
278
    }
279
280
    public function addFormat(FormatInterface $format, callable $filtersCallback = null): self
281
    {
282
        if (!$this->pendingFormats) {
283
            $this->pendingFormats = new Collection;
284
        }
285
286
        $this->pendingFormats->push([$format, $filtersCallback]);
287
288
        return $this;
289
    }
290
}
291