Completed
Push — master ( ed08d3...1d1353 )
by Pascal
14s queued 13s
created

HLSExporter   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 212
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 92
dl 0
loc 212
rs 10
c 0
b 0
f 0
wmc 16

18 Methods

Rating   Name   Duplication   Size   Complexity  
A hp$0 ➔ addLegacyFilter() 0 5 1
A hp$0 ➔ applyFiltersCallback() 0 48 3
A hp$0 ➔ __construct() 0 5 1
A hp$0 ➔ addFilter() 0 14 3
A hp$0 ➔ scale() 0 3 1
A useSegmentFilenameGenerator() 0 5 1
A addHLSParametersToFormat() 0 13 1
applyFiltersCallback() 0 48 ?
A setKeyFrameInterval() 0 5 1
A getSegmentFilenameGenerator() 0 5 2
A getSegmentPatternAndFormatPlaylistPath() 0 19 1
A getPlaylistGenerator() 0 3 2
A withPlaylistGenerator() 0 5 1
A setSegmentLength() 0 5 1
B hp$0 ➔ save() 0 45 5
addFormat() 0 9 ?
A hp$0 ➔ addFormat() 0 9 2
save() 0 45 ?
1
<?php
2
3
namespace ProtoneMedia\LaravelFFMpeg\Exporters;
4
5
use Closure;
6
use FFMpeg\Filters\AdvancedMedia\ComplexFilters;
7
use FFMpeg\Format\FormatInterface;
8
use FFMpeg\Format\Video\DefaultVideo;
9
use FFMpeg\Format\VideoInterface;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Fluent;
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
     * @var \Closure
39
     */
40
    private $segmentFilenameGenerator = null;
41
42
    public function setSegmentLength(int $length): self
43
    {
44
        $this->segmentLength = $length;
45
46
        return $this;
47
    }
48
49
    public function setKeyFrameInterval(int $interval): self
50
    {
51
        $this->keyFrameInterval = $interval;
52
53
        return $this;
54
    }
55
56
    public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self
57
    {
58
        $this->playlistGenerator = $playlistGenerator;
59
60
        return $this;
61
    }
62
63
    private function getPlaylistGenerator(): PlaylistGenerator
64
    {
65
        return $this->playlistGenerator ?: new HLSPlaylistGenerator;
66
    }
67
68
    public function useSegmentFilenameGenerator(Closure $callback): self
69
    {
70
        $this->segmentFilenameGenerator = $callback;
71
72
        return $this;
73
    }
74
75
    private function getSegmentFilenameGenerator(): callable
76
    {
77
        return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) {
78
            $segments("{$name}_{$key}_{$format->getKiloBitrate()}_%05d.ts");
79
            $playlist("{$name}_{$key}_{$format->getKiloBitrate()}.m3u8");
80
        };
81
    }
82
83
    private function getSegmentPatternAndFormatPlaylistPath(string $baseName, VideoInterface $format, int $key): array
84
    {
85
        $segmentsPattern    = null;
86
        $formatPlaylistPath = null;
87
88
        call_user_func(
89
            $this->getSegmentFilenameGenerator(),
90
            $baseName,
91
            $format,
92
            $key,
93
            function ($path) use (&$segmentsPattern) {
94
                $segmentsPattern = $path;
95
            },
96
            function ($path) use (&$formatPlaylistPath) {
97
                $formatPlaylistPath = $path;
98
            }
99
        );
100
101
        return [$segmentsPattern, $formatPlaylistPath];
102
    }
103
104
    private function addHLSParametersToFormat(DefaultVideo $format, string $segmentsPattern, Disk $disk)
105
    {
106
        $format->setAdditionalParameters([
107
            '-sc_threshold',
108
            '0',
109
            '-g',
110
            $this->keyFrameInterval,
111
            '-hls_playlist_type',
112
            'vod',
113
            '-hls_time',
114
            $this->segmentLength,
115
            '-hls_segment_filename',
116
            $disk->makeMedia($segmentsPattern)->getLocalPath(),
117
        ]);
118
    }
119
120
    private function applyFiltersCallback(callable $filtersCallback, $key): bool
121
    {
122
        $called = new Fluent(['called' => false]);
123
124
        $mediaMock = new class($this->driver, $key, $called) {
125
            private $driver;
126
            private $key;
127
            private $called;
128
129
            public function __construct($driver, $key, $called)
130
            {
131
                $this->driver = $driver;
132
                $this->key    = $key;
133
                $this->called = $called;
134
            }
135
136
            public function addLegacyFilter(...$arguments)
137
            {
138
                $this->driver->addFilterAsComplexFilter('[0]', "[v{$this->key}]", ...$arguments);
139
140
                $this->called['called'] = true;
141
            }
142
143
            public function scale($width, $height)
144
            {
145
                $this->addFilter("scale={$width}:{$height}");
146
            }
147
148
            public function addFilter(...$arguments)
149
            {
150
                $in  = '[0]';
151
                $out = "[v{$this->key}]";
152
153
                if (count($arguments) === 1 && !is_callable($arguments[0])) {
154
                    $this->driver->addFilter($in, $arguments[0], $out);
155
                } else {
156
                    $this->driver->addFilter(function (ComplexFilters $filters) use ($arguments, $in,$out) {
157
                        $arguments[0]($filters, $in, $out);
158
                    });
159
                }
160
161
                $this->called['called'] = true;
162
            }
163
        };
164
165
        $filtersCallback($mediaMock);
166
167
        return $called['called'];
168
    }
169
170
    public function save(string $path = null): MediaOpener
171
    {
172
        $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

172
        $media = $this->getDisk()->makeMedia(/** @scrutinizer ignore-type */ $path);
Loading history...
173
174
        $baseName = $media->getDirectory() . $media->getFilenameWithoutExtension();
175
176
        return $this->pendingFormats->map(function ($formatAndCallback, $key) use ($baseName) {
177
            $disk = $this->getDisk()->clone();
178
179
            [$format, $filtersCallback] = $formatAndCallback;
180
181
            [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath(
182
                $baseName,
183
                $format,
184
                $key
185
            );
186
187
            $this->addHLSParametersToFormat($format, $segmentsPattern, $disk);
188
189
            $keysWithFilters = [];
190
191
            if ($filtersCallback) {
192
                if ($this->applyFiltersCallback($filtersCallback, $key)) {
193
                    $keysWithFilters[$key] = "[v{$key}]";
194
                }
195
            }
196
197
            $outs = array_key_exists($key, $keysWithFilters)
198
                ? array_filter([$keysWithFilters[$key], $this->getAudioStream() ? "0:a" : null])
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

198
                ? array_filter([$keysWithFilters[$key], $this->/** @scrutinizer ignore-call */ getAudioStream() ? "0:a" : null])
Loading history...
199
                : ['0'];
200
201
            $this->addFormatOutputMapping($format, $disk->makeMedia($formatPlaylistPath), $outs);
202
203
            return $this->getDisk()->makeMedia($formatPlaylistPath);
204
        })->pipe(function ($playlistMedia) use ($path) {
205
            $result = parent::save();
206
207
            $playlist = $this->getPlaylistGenerator()->get(
208
                $playlistMedia->all(),
209
                $this->driver->fresh()
210
            );
211
212
            $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

212
            $this->getDisk()->/** @scrutinizer ignore-call */ put($path, $playlist);
Loading history...
213
214
            return $result;
215
        });
216
    }
217
218
    public function addFormat(FormatInterface $format, callable $filtersCallback = null): self
219
    {
220
        if (!$this->pendingFormats) {
221
            $this->pendingFormats = new Collection;
222
        }
223
224
        $this->pendingFormats->push([$format, $filtersCallback]);
225
226
        return $this;
227
    }
228
}
229