Passed
Pull Request — master (#262)
by Pascal
06:26 queued 10s
created

HLSExporter::setKeyFrameInterval()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
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\Encryption\Encrypter;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Str;
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
    private $encryptionKey;
38
39
    /**
40
     * @var \Closure
41
     */
42
    private $segmentFilenameGenerator = null;
43
44
    public function setSegmentLength(int $length): self
45
    {
46
        $this->segmentLength = $length;
47
48
        return $this;
49
    }
50
51
    public function setKeyFrameInterval(int $interval): self
52
    {
53
        $this->keyFrameInterval = $interval;
54
55
        return $this;
56
    }
57
58
    public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self
59
    {
60
        $this->playlistGenerator = $playlistGenerator;
61
62
        return $this;
63
    }
64
65
    private function getPlaylistGenerator(): PlaylistGenerator
66
    {
67
        return $this->playlistGenerator ?: new HLSPlaylistGenerator;
68
    }
69
70
    public function useSegmentFilenameGenerator(Closure $callback): self
71
    {
72
        $this->segmentFilenameGenerator = $callback;
73
74
        return $this;
75
    }
76
77
    private function getSegmentFilenameGenerator(): callable
78
    {
79
        return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) {
80
            $segments("{$name}_{$key}_{$format->getKiloBitrate()}_%05d.ts");
81
            $playlist("{$name}_{$key}_{$format->getKiloBitrate()}.m3u8");
82
        };
83
    }
84
85
    public static function generateEncryptionKey(): string
86
    {
87
        return Encrypter::generateKey('AES-128-CBC');
88
    }
89
90
    public function withEncryptionKey($key = null): self
91
    {
92
        $this->encryptionKey = $key ?: static::generateEncryptionKey();
93
94
        return $this;
95
    }
96
97
    private function getSegmentPatternAndFormatPlaylistPath(string $baseName, VideoInterface $format, int $key): array
98
    {
99
        $segmentsPattern    = null;
100
        $formatPlaylistPath = null;
101
102
        call_user_func(
103
            $this->getSegmentFilenameGenerator(),
104
            $baseName,
105
            $format,
106
            $key,
107
            function ($path) use (&$segmentsPattern) {
108
                $segmentsPattern = $path;
109
            },
110
            function ($path) use (&$formatPlaylistPath) {
111
                $formatPlaylistPath = $path;
112
            }
113
        );
114
115
        return [$segmentsPattern, $formatPlaylistPath];
116
    }
117
118
    private function addHLSParametersToFormat(DefaultVideo $format, string $segmentsPattern, Disk $disk)
119
    {
120
        $hlsParameters = [
121
            '-sc_threshold',
122
            '0',
123
            '-g',
124
            $this->keyFrameInterval,
125
            '-hls_playlist_type',
126
            'vod',
127
            '-hls_time',
128
            $this->segmentLength,
129
            '-hls_segment_filename',
130
            $disk->makeMedia($segmentsPattern)->getLocalPath(),
131
        ];
132
133
        if ($this->encryptionKey) {
134
            $name = Str::random(8);
135
136
            $disk = Disk::makeTemporaryDisk();
137
138
            file_put_contents(
139
                $keyPath = $disk->makeMedia("{$name}.key")->getLocalPath(),
140
                $this->encryptionKey
141
            );
142
143
            file_put_contents(
144
                $keyInfoPath = $disk->makeMedia("{$name}.keyinfo")->getLocalPath(),
145
                $keyPath . PHP_EOL . $keyPath . PHP_EOL . bin2hex(static::generateEncryptionKey())
146
            );
147
148
            $hlsParameters[] = '-hls_key_info_file';
149
            $hlsParameters[] = $keyInfoPath;
150
        }
151
152
        $format->setAdditionalParameters(array_merge(
153
            $format->getAdditionalParameters() ?: [],
154
            $hlsParameters
155
        ));
156
    }
157
158
    private function applyFiltersCallback(callable $filtersCallback, int $formatKey): array
159
    {
160
        $filtersCallback(
161
            $hlsVideoFilters = new HLSVideoFilters($this->driver, $formatKey)
162
        );
163
164
        $filterCount = $hlsVideoFilters->count();
165
166
        $outs = [$filterCount ? HLSVideoFilters::glue($formatKey, $filterCount) : '0:v'];
167
168
        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

168
        if ($this->/** @scrutinizer ignore-call */ getAudioStream()) {
Loading history...
169
            $outs[] = '0:a';
170
        }
171
172
        return $outs;
173
    }
174
175
    private function prepareSaving(string $path = null): Collection
176
    {
177
        $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

177
        $media = $this->getDisk()->makeMedia(/** @scrutinizer ignore-type */ $path);
Loading history...
178
179
        $baseName = $media->getDirectory() . $media->getFilenameWithoutExtension();
180
181
        return $this->pendingFormats->map(function ($formatAndCallback, $key) use ($baseName) {
182
            $disk = $this->getDisk()->clone();
183
184
            [$format, $filtersCallback] = $formatAndCallback;
185
186
            [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath(
187
                $baseName,
188
                $format,
189
                $key
190
            );
191
192
            $this->addHLSParametersToFormat($format, $segmentsPattern, $disk);
193
194
            if ($filtersCallback) {
195
                $outs = $this->applyFiltersCallback($filtersCallback, $key);
196
            }
197
198
            $this->addFormatOutputMapping($format, $disk->makeMedia($formatPlaylistPath), $outs ?? ['0']);
199
200
            return $this->getDisk()->makeMedia($formatPlaylistPath);
201
        });
202
    }
203
204
    public function getCommand(string $path = null)
205
    {
206
        $this->prepareSaving($path);
207
208
        return parent::getCommand(null);
209
    }
210
211
    public function save(string $path = null): MediaOpener
212
    {
213
        return $this->prepareSaving($path)->pipe(function ($playlistMedia) use ($path) {
214
            $result = parent::save();
215
216
            $playlist = $this->getPlaylistGenerator()->get(
217
                $playlistMedia->all(),
218
                $this->driver->fresh()
219
            );
220
221
            $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

221
            $this->getDisk()->/** @scrutinizer ignore-call */ put($path, $playlist);
Loading history...
222
223
            return $result;
224
        });
225
    }
226
227
    public function addFormat(FormatInterface $format, callable $filtersCallback = null): self
228
    {
229
        if (!$this->pendingFormats) {
230
            $this->pendingFormats = new Collection;
231
        }
232
233
        $this->pendingFormats->push([$format, $filtersCallback]);
234
235
        return $this;
236
    }
237
}
238