Passed
Push — master ( dfdcd4...72bb00 )
by Pascal
03:25
created

generateTemporarySegmentPlaylistFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace ProtoneMedia\LaravelFFMpeg\Exporters;
4
5
use Closure;
6
use FFMpeg\Format\FormatInterface;
7
use FFMpeg\Format\Video\DefaultVideo;
8
use FFMpeg\Format\VideoInterface;
9
use Illuminate\Support\Collection;
10
use ProtoneMedia\LaravelFFMpeg\Filesystem\Disk;
11
use ProtoneMedia\LaravelFFMpeg\Filesystem\Media;
12
use ProtoneMedia\LaravelFFMpeg\MediaOpener;
13
14
class HLSExporter extends MediaExporter
15
{
16
    use EncryptsHLSSegments;
17
18
    const HLS_KEY_INFO_FILENAME = 'hls_encryption.keyinfo';
19
20
    /**
21
     * @var integer
22
     */
23
    private $segmentLength = 10;
24
25
    /**
26
    * @var integer
27
    */
28
    private $keyFrameInterval = 48;
29
30
    /**
31
     * @var \Illuminate\Support\Collection
32
     */
33
    private $pendingFormats;
34
35
    /**
36
     * @var \ProtoneMedia\LaravelFFMpeg\Exporters\PlaylistGenerator
37
     */
38
    private $playlistGenerator;
39
40
    /**
41
     * @var \Closure
42
     */
43
    private $segmentFilenameGenerator = null;
44
45
    /**
46
     * Setter for the segment length
47
     *
48
     * @param integer $length
49
     * @return self
50
     */
51
    public function setSegmentLength(int $length): self
52
    {
53
        $this->segmentLength = $length;
54
55
        return $this;
56
    }
57
58
    /**
59
     * Setter for the Key Frame interval
60
     *
61
     * @param integer $interval
62
     * @return self
63
     */
64
    public function setKeyFrameInterval(int $interval): self
65
    {
66
        $this->keyFrameInterval = $interval;
67
68
        return $this;
69
    }
70
71
    /**
72
     * Method to set a different playlist generator than
73
     * the default HLSPlaylistGenerator.
74
     *
75
     * @param \ProtoneMedia\LaravelFFMpeg\Exporters\PlaylistGenerator $playlistGenerator
76
     * @return self
77
     */
78
    public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self
79
    {
80
        $this->playlistGenerator = $playlistGenerator;
81
82
        return $this;
83
    }
84
85
    private function getPlaylistGenerator(): PlaylistGenerator
86
    {
87
        return $this->playlistGenerator ?: new HLSPlaylistGenerator;
88
    }
89
90
    /**
91
     * Setter for a callback that generates a segment filename.
92
     *
93
     * @param Closure $callback
94
     * @return self
95
     */
96
    public function useSegmentFilenameGenerator(Closure $callback): self
97
    {
98
        $this->segmentFilenameGenerator = $callback;
99
100
        return $this;
101
    }
102
103
    /**
104
     * Returns a default generator if none is set.
105
     *
106
     * @return callable
107
     */
108
    private function getSegmentFilenameGenerator(): callable
109
    {
110
        return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) {
111
            $segments("{$name}_{$key}_{$format->getKiloBitrate()}_%05d.ts");
112
            $playlist("{$name}_{$key}_{$format->getKiloBitrate()}.m3u8");
113
        };
114
    }
115
116
    /**
117
     * Calls the generator with the path (without extension), format and key.
118
     *
119
     * @param string $baseName
120
     * @param \FFMpeg\Format\VideoInterface $format
121
     * @param integer $key
122
     * @return array
123
     */
124
    private function getSegmentPatternAndFormatPlaylistPath(string $baseName, VideoInterface $format, int $key): array
125
    {
126
        $segmentsPattern    = null;
127
        $formatPlaylistPath = null;
128
129
        call_user_func(
130
            $this->getSegmentFilenameGenerator(),
131
            $baseName,
132
            $format,
133
            $key,
134
            function ($path) use (&$segmentsPattern) {
135
                $segmentsPattern = $path;
136
            },
137
            function ($path) use (&$formatPlaylistPath) {
138
                $formatPlaylistPath = $path;
139
            }
140
        );
141
142
        return [$segmentsPattern, $formatPlaylistPath];
143
    }
144
145
    /**
146
     * Merges the HLS parameters to the given format.
147
     *
148
     * @param \FFMpeg\Format\Video\DefaultVideo $format
149
     * @param string $segmentsPattern
150
     * @param \ProtoneMedia\LaravelFFMpeg\Filesystem\Disk $disk
151
     * @param integer $key
152
     * @return array
153
     */
154
    private function addHLSParametersToFormat(DefaultVideo $format, string $segmentsPattern, Disk $disk, int $key): array
155
    {
156
        $format->setAdditionalParameters(array_merge(
157
            $format->getAdditionalParameters() ?: [],
158
            $hlsParameters = [
159
                '-sc_threshold',
160
                '0',
161
                '-g',
162
                $this->keyFrameInterval,
163
                '-hls_playlist_type',
164
                'vod',
165
                '-hls_time',
166
                $this->segmentLength,
167
                '-hls_segment_filename',
168
                $disk->makeMedia($segmentsPattern)->getLocalPath(),
169
                '-master_pl_name',
170
                $this->generateTemporarySegmentPlaylistFilename($key),
171
            ],
172
            $this->getEncrypedHLSParameters()
173
        ));
174
175
        return $hlsParameters;
176
    }
177
178
    /**
179
     * Gives the callback an HLSVideoFilters object that provides addFilter(),
180
     * addLegacyFilter(), addWatermark() and resize() helper methods. It
181
     * returns a mapping for the video and (optional) audio stream.
182
     *
183
     * @param callable $filtersCallback
184
     * @param integer $formatKey
185
     * @return array
186
     */
187
    private function applyFiltersCallback(callable $filtersCallback, int $formatKey): array
188
    {
189
        $filtersCallback(
190
            $hlsVideoFilters = new HLSVideoFilters($this->driver, $formatKey)
191
        );
192
193
        $filterCount = $hlsVideoFilters->count();
194
195
        $outs = [$filterCount ? HLSVideoFilters::glue($formatKey, $filterCount) : '0:v'];
196
197
        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

197
        if ($this->/** @scrutinizer ignore-call */ getAudioStream()) {
Loading history...
198
            $outs[] = '0:a';
199
        }
200
201
        return $outs;
202
    }
203
204
    /**
205
     * Returns the filename of a segment playlist by its key. We let FFmpeg generate a playlist
206
     * for each added format so we don't have to detect the bitrate and codec ourselves.
207
     * We use this as a reference so when can generate our own main playlist.
208
     *
209
     * @param int $key
210
     * @return string
211
     */
212
    public static function generateTemporarySegmentPlaylistFilename(int $key): string
213
    {
214
        return "temporary_segment_playlist_{$key}.m3u8";
215
    }
216
217
    /**
218
     * Loops through each added format and then deletes the temporary
219
     * segment playlist, which we generate manually using the
220
     * HLSPlaylistGenerator.
221
     *
222
     * @param \ProtoneMedia\LaravelFFMpeg\Filesystem\Media $media
223
     * @return self
224
     */
225
    private function cleanupSegmentPlaylistGuides(Media $media): self
226
    {
227
        $disk      = $media->getDisk();
228
        $directory = $media->getDirectory();
229
230
        $this->pendingFormats->map(function ($formatAndCallback, $key) use ($disk, $directory) {
231
            $disk->delete($directory . static::generateTemporarySegmentPlaylistFilename($key));
0 ignored issues
show
Bug introduced by
The method delete() 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

231
            $disk->/** @scrutinizer ignore-call */ 
232
                   delete($directory . static::generateTemporarySegmentPlaylistFilename($key));
Loading history...
232
        });
233
234
        return $this;
235
    }
236
237
    /**
238
     * Adds a mapping for each added format and automatically handles the mapping
239
     * for filters. Adds a handler to rotate the encryption key (optional).
240
     * Returns a media collection of all segment playlists.
241
     *
242
     * @param string $path
243
     * @throws \ProtoneMedia\LaravelFFMpeg\Exporters\NoFormatException
244
     * @return \Illuminate\Support\Collection
245
     */
246
    private function prepareSaving(string $path = null): Collection
247
    {
248
        if (!$this->pendingFormats) {
249
            throw new NoFormatException;
250
        }
251
252
        $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

252
        $media = $this->getDisk()->makeMedia(/** @scrutinizer ignore-type */ $path);
Loading history...
253
254
        $baseName = $media->getDirectory() . $media->getFilenameWithoutExtension();
255
256
        return $this->pendingFormats->map(function (array $formatAndCallback, $key) use ($baseName, $media) {
0 ignored issues
show
Unused Code introduced by
The import $media is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
257
            [$format, $filtersCallback] = $formatAndCallback;
258
259
            [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath(
260
                $baseName,
261
                $format,
262
                $key
263
            );
264
265
            $disk = $this->getDisk()->clone();
266
267
            $this->addHLSParametersToFormat($format, $segmentsPattern, $disk, $key);
268
269
            if ($filtersCallback) {
270
                $outs = $this->applyFiltersCallback($filtersCallback, $key);
271
            }
272
273
            $this->addFormatOutputMapping($format, $disk->makeMedia($formatPlaylistPath), $outs ?? ['0']);
274
275
            return $this->getDisk()->makeMedia($formatPlaylistPath);
276
        })->tap(function () {
277
            $this->addHandlerToRotateEncryptionKey();
278
        });
279
    }
280
281
    /**
282
     * Prepares the saves command but returns the command instead.
283
     *
284
     * @param string $path
285
     * @return mixed
286
     */
287
    public function getCommand(string $path = null)
288
    {
289
        $this->prepareSaving($path);
290
291
        return parent::getCommand(null);
292
    }
293
294
    /**
295
     * Runs the export, generates the main playlist, and cleans up the
296
     * segment playlist guides and temporary HLS encryption keys.
297
     *
298
     * @param string $path
299
     * @return \ProtoneMedia\LaravelFFMpeg\MediaOpener
300
     */
301
    public function save(string $mainPlaylistPath = null): MediaOpener
302
    {
303
        return $this->prepareSaving($mainPlaylistPath)->pipe(function ($segmentPlaylists) use ($mainPlaylistPath) {
304
            $result = parent::save();
305
306
            $playlist = $this->getPlaylistGenerator()->get(
307
                $segmentPlaylists->all(),
308
                $this->driver->fresh()
309
            );
310
311
            $this->getDisk()->put($mainPlaylistPath, $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

311
            $this->getDisk()->/** @scrutinizer ignore-call */ put($mainPlaylistPath, $playlist);
Loading history...
312
313
            $this->replaceAbsolutePathsHLSEncryption($segmentPlaylists)
314
                ->cleanupSegmentPlaylistGuides($segmentPlaylists->first())
315
                ->cleanupHLSEncryption();
316
317
            return $result;
318
        });
319
    }
320
321
    /**
322
     * Initializes the $pendingFormats property when needed and adds the format
323
     * with the optional callback to add filters.
324
     *
325
     * @param \FFMpeg\Format\FormatInterface $format
326
     * @param callable $filtersCallback
327
     * @return self
328
     */
329
    public function addFormat(FormatInterface $format, callable $filtersCallback = null): self
330
    {
331
        if (!$this->pendingFormats) {
332
            $this->pendingFormats = new Collection;
333
        }
334
335
        $this->pendingFormats->push([$format, $filtersCallback]);
336
337
        return $this;
338
    }
339
}
340