Passed
Push — master ( ca89e3...264744 )
by Sébastien
02:15
created

VideoInfo::getAudioStreams()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 4
nop 0
dl 0
loc 17
ccs 11
cts 11
cp 1
crap 3
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @see       https://github.com/soluble-io/soluble-mediatools for the canonical repository
7
 *
8
 * @copyright Copyright (c) 2018-2019 Sébastien Vanvelthem. (https://github.com/belgattitude)
9
 * @license   https://github.com/soluble-io/soluble-mediatools/blob/master/LICENSE.md MIT
10
 */
11
12
namespace Soluble\MediaTools\Video;
13
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\LogLevel;
16
use Psr\Log\NullLogger;
17
use Soluble\MediaTools\Common\Exception\IOException;
18
use Soluble\MediaTools\Common\Exception\JsonParseException;
19
use Soluble\MediaTools\Video\Exception\InvalidArgumentException;
20
use Soluble\MediaTools\Video\Exception\InvalidStreamMetadataException;
21
use Soluble\MediaTools\Video\Info\AudioStreamCollection;
22
use Soluble\MediaTools\Video\Info\AudioStreamCollectionInterface;
23
use Soluble\MediaTools\Video\Info\VideoStreamCollection;
24
use Soluble\MediaTools\Video\Info\VideoStreamCollectionInterface;
25
26
class VideoInfo implements VideoInfoInterface
27
{
28
    /** @var array<string, mixed> */
29
    private $metadata;
30
31
    /** @var string */
32
    private $file;
33
34
    /** @var LoggerInterface */
35
    private $logger;
36
37
    /** @var array|null */
38
    private $metadataByStreamType;
39
40
    /** @var VideoStreamCollectionInterface|null; */
41
    private $cachedVideoStreams;
42
43
    /** @var AudioStreamCollectionInterface|null; */
44
    private $cachedAudioStreams;
45
46
    /**
47
     * @param string               $fileName reference to filename
48
     * @param array                $metadata metadata as parsed from ffprobe --json
49
     * @param LoggerInterface|null $logger
50
     */
51 25
    public function __construct(string $fileName, array $metadata, ?LoggerInterface $logger = null)
52
    {
53 25
        if (!file_exists($fileName)) {
54 1
            throw new IOException(sprintf(
55 1
                'File %s does not exists',
56 1
                $this->file
57
            ));
58
        }
59 24
        $this->metadata = $metadata;
60 24
        $this->file     = $fileName;
61 24
        $this->logger   = $logger ?? new NullLogger();
62 24
    }
63
64
    /**
65
     * @throws JsonParseException if json is invalid
66
     */
67 9
    public static function createFromFFProbeJson(string $fileName, string $ffprobeJson, ?LoggerInterface $logger = null): self
68
    {
69 9
        if (trim($ffprobeJson) === '') {
70 1
            throw new JsonParseException('Cannot parse empty json string');
71
        }
72 8
        $decoded = json_decode($ffprobeJson, true);
73 8
        if ($decoded === null) {
74 1
            throw new JsonParseException('Cannot parse json');
75
        }
76
77 7
        return new self($fileName, $decoded, $logger);
78
    }
79
80 2
    public function getFile(): string
81
    {
82 2
        return $this->file;
83
    }
84
85
    /**
86
     * @throws IOException
87
     */
88 2
    public function getFileSize(): int
89
    {
90 2
        $size = @filesize($this->file);
91 2
        if ($size === false) {
92 1
            $msg = sprintf('Cannot get filesize of file %s', $this->file);
93 1
            $this->logger->log(LogLevel::ERROR, $msg);
94 1
            throw new IOException($msg);
95
        }
96
97 1
        return $size;
98
    }
99
100
    /**
101
     * Return VideoStreams as a collection.
102
     *
103
     * @throws InvalidStreamMetadataException
104
     */
105 5
    public function getVideoStreams(): VideoStreamCollectionInterface
106
    {
107 5
        if ($this->cachedVideoStreams === null) {
108
            try {
109 5
                $videoStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
110 5
                $this->cachedVideoStreams = new VideoStreamCollection($videoStreamsMetadata);
111
            } catch (InvalidStreamMetadataException $e) {
112
                $this->logger->log(LogLevel::ERROR, sprintf(
113
                    'Cannot get video streams info for file: %s, message is: %s',
114
                    $this->file,
115
                    $e->getMessage()
116
                ));
117
                throw $e;
118
            }
119
        }
120
121 5
        return $this->cachedVideoStreams;
122
    }
123
124
    /**
125
     * Return VideoStreams as a collection.
126
     *
127
     * @throws InvalidStreamMetadataException
128
     */
129 2
    public function getAudioStreams(): AudioStreamCollectionInterface
130
    {
131 2
        if ($this->cachedAudioStreams === null) {
132
            try {
133 2
                $audioStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
134 1
                $this->cachedAudioStreams = new AudioStreamCollection($audioStreamsMetadata);
135 1
            } catch (InvalidStreamMetadataException $e) {
136 1
                $this->logger->log(LogLevel::ERROR, sprintf(
137 1
                    'Cannot get audio streams info for file: %s, message is: %s',
138 1
                    $this->file,
139 1
                    $e->getMessage()
140
                ));
141 1
                throw $e;
142
            }
143
        }
144
145 1
        return $this->cachedAudioStreams;
146
    }
147
148
    /**
149
     * Format name as returned by ffprobe.
150
     */
151 2
    public function getFormatName(): string
152
    {
153 2
        return $this->metadata['format']['format_name'];
154
    }
155
156
    /**
157
     * @param string $streamType any of self::SUPPORTED_STREAM_TYPES
158
     */
159 2
    public function countStreams(?string $streamType = null): int
160
    {
161 2
        if ($streamType === null) {
162 2
            return count($this->metadata['streams'] ?? []);
163
        }
164
165 1
        return count($this->getStreamsMetadataByType($streamType));
166
    }
167
168
    /**
169
     * Return metadata as received by ffprobe.
170
     *
171
     * @return array<string, array>
172
     */
173 1
    public function getMetadata(): array
174
    {
175 1
        return $this->metadata;
176
    }
177
178
    /**
179
     * Return total duration.
180
     */
181 2
    public function getDuration(): float
182
    {
183 2
        return (float) ($this->metadata['format']['duration'] ?? 0.0);
184
    }
185
186
    /**
187
     * @deprecated
188
     *
189
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
190
     *
191
     * @return array<string, int> associative array with 'height' and 'width'
192
     */
193 2
    public function getDimensions(int $streamIndex = 0): array
194
    {
195
        return [
196 2
            'width'  => $this->getWidth($streamIndex),
0 ignored issues
show
Deprecated Code introduced by
The function Soluble\MediaTools\Video\VideoInfo::getWidth() has been deprecated. ( Ignorable by Annotation )

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

196
            'width'  => /** @scrutinizer ignore-deprecated */ $this->getWidth($streamIndex),
Loading history...
197 2
            'height' => $this->getHeight($streamIndex),
0 ignored issues
show
Deprecated Code introduced by
The function Soluble\MediaTools\Video\VideoInfo::getHeight() has been deprecated. ( Ignorable by Annotation )

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

197
            'height' => /** @scrutinizer ignore-deprecated */ $this->getHeight($streamIndex),
Loading history...
198
        ];
199
    }
200
201
    /**
202
     * @deprecated
203
     *
204
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
205
     */
206 3
    public function getWidth(int $streamIndex = 0): int
207
    {
208 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
209
210 3
        return (int) ($videoStream['width'] ?? 0);
211
    }
212
213
    /**
214
     * @deprecated
215
     *
216
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
217
     */
218 3
    public function getHeight(int $streamIndex = 0): int
219
    {
220 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
221
222 3
        return (int) ($videoStream['height'] ?? 0);
223
    }
224
225
    /**
226
     * @deprecated
227
     *
228
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
229
     */
230 3
    public function getNbFrames(int $streamIndex = 0): int
231
    {
232 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
233
234 3
        return (int) ($videoStream['nb_frames'] ?? 0);
235
    }
236
237
    /**
238
     * @deprecated
239
     *
240
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
241
     */
242 1
    public function getVideoBitrate(int $streamIndex = 0): int
243
    {
244 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
245
246 1
        return (int) ($videoStream['bit_rate'] ?? 0);
247
    }
248
249
    /**
250
     * @deprecated
251
     *
252
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
253
     */
254 1
    public function getAudioBitrate(int $streamIndex = 0): int
255
    {
256 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
257
258 1
        return (int) ($audioStream['bit_rate'] ?? 0);
259
    }
260
261
    /**
262
     * @deprecated
263
     *
264
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
265
     */
266 1
    public function getAudioCodecName(int $streamIndex = 0): ?string
267
    {
268 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
269
270 1
        return $audioStream['codec_name'] ?? null;
271
    }
272
273
    /**
274
     * @deprecated
275
     *
276
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
277
     */
278 1
    public function getVideoCodecName(int $streamIndex = 0): ?string
279
    {
280 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
281
282 1
        return $videoStream['codec_name'] ?? null;
283
    }
284
285
    /**
286
     * @return array<int, array>
287
     */
288 3
    public function getAudioStreamsMetadata(): array
289
    {
290 3
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
291
    }
292
293
    /**
294
     * @return array<int, array>
295
     */
296 8
    public function getVideoStreamsMetadata(): array
297
    {
298 8
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
299
    }
300
301
    /**
302
     * @throws InvalidArgumentException
303
     *
304
     * @param string $streamType  any of self::SUPPORTED_STREAM_TYPES
305
     * @param int    $streamIndex selected a specific stream by index, default: 0 = the first available
306
     *
307
     * @return array<string|int, array>
308
     *
309
     * @throws InvalidStreamMetadataException
310
     */
311 19
    public function getStreamsMetadataByType(string $streamType, int $streamIndex = 0): array
312
    {
313 19
        if (!in_array($streamType, self::SUPPORTED_STREAM_TYPES, true)) {
314 1
            $msg = sprintf(
315 1
                'Invalid usage, unsupported param $streamType given: %s',
316 1
                $streamType
317
            );
318 1
            $this->logger->log(LogLevel::ERROR, $msg);
319 1
            throw new InvalidArgumentException($msg);
320
        }
321
322 18
        return $this->getMetadataByStreamType()[$streamType];
323
    }
324
325
    /**
326
     * @throws InvalidStreamMetadataException
327
     */
328 18
    private function getMetadataByStreamType(): array
329
    {
330 18
        if ($this->metadataByStreamType === null) {
331
            try {
332
                $streams = [
333 18
                    self::STREAM_TYPE_VIDEO => [],
334 18
                    self::STREAM_TYPE_AUDIO => [],
335 18
                    self::STREAM_TYPE_DATA  => [],
336
                ];
337 18
                if (!is_array($this->metadata['streams'])) {
338
                    throw new InvalidStreamMetadataException(sprintf(
339
                        'Invalid or unsupported stream metadata returned by ffprobe: %s',
340
                        (string) json_encode($this->metadata)
341
                    ));
342
                }
343
344 18
                foreach ($this->metadata['streams'] as $stream) {
345 18
                    if (!is_array($stream)) {
346 1
                        throw new InvalidStreamMetadataException(sprintf(
347 1
                            'Stream metadata returned by ffprobe must be an array: %s',
348 1
                            (string) json_encode($stream)
349
                        ));
350
                    }
351
352 17
                    if (!isset($stream['codec_type'])) {
353
                        throw new InvalidStreamMetadataException(sprintf(
354
                            'Missing codec_type information in metadata returned by ffprobe: %s',
355
                            (string) json_encode($stream)
356
                        ));
357
                    }
358
359 17
                    $type = mb_strtolower($stream['codec_type']);
360
                    switch ($type) {
361 17
                        case self::STREAM_TYPE_VIDEO:
362 17
                            $streams[self::STREAM_TYPE_VIDEO][] = $stream;
363 17
                            break;
364 17
                        case self::STREAM_TYPE_AUDIO:
365 17
                            $streams[self::STREAM_TYPE_AUDIO][] = $stream;
366 17
                            break;
367
                        case self::STREAM_TYPE_DATA:
368
                            $streams[self::STREAM_TYPE_DATA][] = $stream;
369
                            break;
370
                        default:
371
                            $streams[$type][] = $stream;
372
                    }
373
                }
374
375 17
                $this->metadataByStreamType = $streams;
376 1
            } catch (InvalidStreamMetadataException $e) {
377 1
                $this->logger->log(LogLevel::ERROR, sprintf(
378 1
                    'Cannot read metadata for file: %s. Failed with message: %s',
379 1
                    $this->file,
380 1
                    $e->getMessage()
381
                ));
382 1
                throw $e;
383
            }
384
        }
385
386 17
        return $this->metadataByStreamType;
387
    }
388
}
389