Passed
Pull Request — master (#262)
by Pascal
02:34
created

HLSExporter   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 120
dl 0
loc 297
rs 9.6
c 0
b 0
f 0
wmc 35

18 Methods

Rating   Name   Duplication   Size   Complexity  
A rotateEncryptionKey() 0 33 5
A useSegmentFilenameGenerator() 0 5 1
B addHLSParametersToFormat() 0 37 7
A applyFiltersCallback() 0 15 3
A setKeyFrameInterval() 0 5 1
A getSegmentFilenameGenerator() 0 5 2
A getCommand() 0 5 1
A onEncryptionKey() 0 5 1
A getSegmentPatternAndFormatPlaylistPath() 0 19 1
A generateEncryptionKey() 0 3 1
A getPlaylistGenerator() 0 3 2
A addFormat() 0 9 2
A withRotatingEncryptionKey() 0 7 1
A save() 0 13 1
A withPlaylistGenerator() 0 5 1
A prepareSaving() 0 26 2
A withEncryptionKey() 0 5 2
A setSegmentLength() 0 5 1
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 Illuminate\Support\Str;
11
use ProtoneMedia\LaravelFFMpeg\FFMpeg\StdListener;
12
use ProtoneMedia\LaravelFFMpeg\Filesystem\Disk;
13
use ProtoneMedia\LaravelFFMpeg\MediaOpener;
14
15
class HLSExporter extends MediaExporter
16
{
17
    /**
18
     * @var integer
19
     */
20
    private $segmentLength = 10;
21
22
    /**
23
    * @var integer
24
    */
25
    private $keyFrameInterval = 48;
26
27
    /**
28
     * @var \Illuminate\Support\Collection
29
     */
30
    private $pendingFormats;
31
32
    /**
33
     * @var \ProtoneMedia\LaravelFFMpeg\Exporters\PlaylistGenerator
34
     */
35
    private $playlistGenerator;
36
37
    /**
38
     * The encryption key.
39
     *
40
     * @var string
41
     */
42
    private $encryptionKey;
43
44
    /**
45
     * @var \Closure
46
     */
47
    private $segmentFilenameGenerator = null;
48
49
    private $newEncryptionKeyCallback = null;
50
51
    private $encryptionKeyDisk = null;
52
    private $encryptionKeyName = null;
53
    private $encryptionIV      = null;
54
55
    private $rotatingEncryptiongKey = false;
56
57
    public function setSegmentLength(int $length): self
58
    {
59
        $this->segmentLength = $length;
60
61
        return $this;
62
    }
63
64
    public function setKeyFrameInterval(int $interval): self
65
    {
66
        $this->keyFrameInterval = $interval;
67
68
        return $this;
69
    }
70
71
    public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self
72
    {
73
        $this->playlistGenerator = $playlistGenerator;
74
75
        return $this;
76
    }
77
78
    private function getPlaylistGenerator(): PlaylistGenerator
79
    {
80
        return $this->playlistGenerator ?: new HLSPlaylistGenerator;
81
    }
82
83
    public function useSegmentFilenameGenerator(Closure $callback): self
84
    {
85
        $this->segmentFilenameGenerator = $callback;
86
87
        return $this;
88
    }
89
90
    public function onEncryptionKey(Closure $callback): self
91
    {
92
        $this->newEncryptionKeyCallback = $callback;
93
94
        return $this;
95
    }
96
97
    private function getSegmentFilenameGenerator(): callable
98
    {
99
        return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) {
100
            $segments("{$name}_{$key}_{$format->getKiloBitrate()}_%05d.ts");
101
            $playlist("{$name}_{$key}_{$format->getKiloBitrate()}.m3u8");
102
        };
103
    }
104
105
    /**
106
     * Creates a new encryption key.
107
     *
108
     * @return string
109
     */
110
    public static function generateEncryptionKey(): string
111
    {
112
        return random_bytes(16);
113
    }
114
115
    /**
116
     * Sets the encryption key with the given value or generates a new one.
117
     *
118
     * @param string $key
119
     * @return self
120
     */
121
    public function withEncryptionKey($key = null): self
122
    {
123
        $this->encryptionKey = $key ?: static::generateEncryptionKey();
124
125
        return $this;
126
    }
127
128
    public function withRotatingEncryptionKey(): self
129
    {
130
        $this->withEncryptionKey();
131
132
        $this->rotatingEncryptiongKey = true;
133
134
        return $this;
135
    }
136
137
    private function getSegmentPatternAndFormatPlaylistPath(string $baseName, VideoInterface $format, int $key): array
138
    {
139
        $segmentsPattern    = null;
140
        $formatPlaylistPath = null;
141
142
        call_user_func(
143
            $this->getSegmentFilenameGenerator(),
144
            $baseName,
145
            $format,
146
            $key,
147
            function ($path) use (&$segmentsPattern) {
148
                $segmentsPattern = $path;
149
            },
150
            function ($path) use (&$formatPlaylistPath) {
151
                $formatPlaylistPath = $path;
152
            }
153
        );
154
155
        return [$segmentsPattern, $formatPlaylistPath];
156
    }
157
158
    private function rotateEncryptionKey()
159
    {
160
        $this->withEncryptionKey();
161
162
        if (!$this->encryptionKeyName) {
163
            $this->encryptionKeyName = Str::random(8);
164
        }
165
166
        if (!$this->encryptionIV) {
167
            $this->encryptionIV = bin2hex(static::generateEncryptionKey());
168
        }
169
170
        if (!$this->encryptionKeyDisk) {
171
            $this->encryptionKeyDisk = Disk::makeTemporaryDisk();
172
        }
173
174
        $name = $this->encryptionKeyName . "_" . Str::random(8);
175
176
        file_put_contents(
177
            $keyPath = $this->encryptionKeyDisk->makeMedia("{$name}.key")->getLocalPath(),
178
            $this->encryptionKey
179
        );
180
181
        file_put_contents(
182
            $keyInfoPath = $this->encryptionKeyDisk->makeMedia("{$this->encryptionKeyName}.keyinfo")->getLocalPath(),
183
            $keyPath . PHP_EOL . $keyPath . PHP_EOL . $this->encryptionIV
184
        );
185
186
        if ($this->newEncryptionKeyCallback) {
187
            call_user_func($this->newEncryptionKeyCallback, "{$name}.key", $this->encryptionKey);
188
        }
189
190
        return $keyInfoPath;
191
    }
192
193
    private function addHLSParametersToFormat(DefaultVideo $format, string $segmentsPattern, Disk $disk)
194
    {
195
        $hlsParameters = [
196
            '-sc_threshold',
197
            '0',
198
            '-g',
199
            $this->keyFrameInterval,
200
            '-hls_playlist_type',
201
            'vod',
202
            '-hls_time',
203
            $this->segmentLength,
204
            '-hls_segment_filename',
205
            $disk->makeMedia($segmentsPattern)->getLocalPath(),
206
        ];
207
208
        if ($this->encryptionKey) {
209
            $hlsParameters[] = '-hls_key_info_file';
210
            $hlsParameters[] = $this->rotateEncryptionKey();
211
212
            if ($this->rotatingEncryptiongKey) {
213
                $hlsParameters[] = '-hls_flags';
214
                $hlsParameters[] = 'periodic_rekey';
215
            }
216
        }
217
218
        $format->setAdditionalParameters(array_merge(
219
            $format->getAdditionalParameters() ?: [],
220
            $hlsParameters
221
        ));
222
223
        if ($this->rotatingEncryptiongKey) {
224
            $this->addListener(new StdListener)->onEvent('listen', function ($line) {
0 ignored issues
show
Bug introduced by
The method addListener() 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

224
            $this->/** @scrutinizer ignore-call */ 
225
                   addListener(new StdListener)->onEvent('listen', function ($line) {
Loading history...
Bug introduced by
The method onEvent() 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

224
            $this->addListener(new StdListener)->/** @scrutinizer ignore-call */ onEvent('listen', function ($line) {
Loading history...
225
                if (!(Str::contains($line, "Opening 'crypto:/") && Str::contains($line, ".ts' for writing"))) {
226
                    return;
227
                }
228
229
                $this->rotateEncryptionKey();
230
            });
231
        }
232
    }
233
234
    private function applyFiltersCallback(callable $filtersCallback, int $formatKey): array
235
    {
236
        $filtersCallback(
237
            $hlsVideoFilters = new HLSVideoFilters($this->driver, $formatKey)
238
        );
239
240
        $filterCount = $hlsVideoFilters->count();
241
242
        $outs = [$filterCount ? HLSVideoFilters::glue($formatKey, $filterCount) : '0:v'];
243
244
        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

244
        if ($this->/** @scrutinizer ignore-call */ getAudioStream()) {
Loading history...
245
            $outs[] = '0:a';
246
        }
247
248
        return $outs;
249
    }
250
251
    private function prepareSaving(string $path = null): Collection
252
    {
253
        $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

253
        $media = $this->getDisk()->makeMedia(/** @scrutinizer ignore-type */ $path);
Loading history...
254
255
        $baseName = $media->getDirectory() . $media->getFilenameWithoutExtension();
256
257
        return $this->pendingFormats->map(function ($formatAndCallback, $key) use ($baseName) {
258
            $disk = $this->getDisk()->clone();
259
260
            [$format, $filtersCallback] = $formatAndCallback;
261
262
            [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath(
263
                $baseName,
264
                $format,
265
                $key
266
            );
267
268
            $this->addHLSParametersToFormat($format, $segmentsPattern, $disk);
269
270
            if ($filtersCallback) {
271
                $outs = $this->applyFiltersCallback($filtersCallback, $key);
272
            }
273
274
            $this->addFormatOutputMapping($format, $disk->makeMedia($formatPlaylistPath), $outs ?? ['0']);
275
276
            return $this->getDisk()->makeMedia($formatPlaylistPath);
277
        });
278
    }
279
280
    public function getCommand(string $path = null)
281
    {
282
        $this->prepareSaving($path);
283
284
        return parent::getCommand(null);
285
    }
286
287
    public function save(string $path = null): MediaOpener
288
    {
289
        return $this->prepareSaving($path)->pipe(function ($playlistMedia) use ($path) {
290
            $result = parent::save();
291
292
            $playlist = $this->getPlaylistGenerator()->get(
293
                $playlistMedia->all(),
294
                $this->driver->fresh()
295
            );
296
297
            $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

297
            $this->getDisk()->/** @scrutinizer ignore-call */ put($path, $playlist);
Loading history...
298
299
            return $result;
300
        });
301
    }
302
303
    public function addFormat(FormatInterface $format, callable $filtersCallback = null): self
304
    {
305
        if (!$this->pendingFormats) {
306
            $this->pendingFormats = new Collection;
307
        }
308
309
        $this->pendingFormats->push([$format, $filtersCallback]);
310
311
        return $this;
312
    }
313
}
314