Passed
Push — master ( 4eae52...59c295 )
by Sébastien
03:55
created

VideoInfo::getAudioStreams()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.4609

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 17
ccs 5
cts 11
cp 0.4545
rs 9.9
c 0
b 0
f 0
cc 3
nc 4
nop 0
crap 4.4609
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 23
    public function __construct(string $fileName, array $metadata, ?LoggerInterface $logger = null)
52
    {
53 23
        if (!file_exists($fileName)) {
54 1
            throw new IOException(sprintf(
55 1
                'File %s does not exists',
56 1
                $this->file
57
            ));
58
        }
59 22
        $this->metadata = $metadata;
60 22
        $this->file     = $fileName;
61 22
        $this->logger   = $logger ?? new NullLogger();
62 22
    }
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
            throw new IOException(sprintf(
93 1
                'Cannot get filesize of file %s',
94 1
                $this->file
95
            ));
96
        }
97
98 1
        return $size;
99
    }
100
101
    /**
102
     * Return VideoStreams as a collection.
103
     *
104
     * @throws InvalidStreamMetadataException
105
     */
106 6
    public function getVideoStreams(): VideoStreamCollectionInterface
107
    {
108 6
        if ($this->cachedVideoStreams === null) {
109
            try {
110 6
                $videoStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
111 5
                $this->cachedVideoStreams = new VideoStreamCollection($videoStreamsMetadata);
112 1
            } catch (InvalidStreamMetadataException $e) {
113 1
                $this->logger->log(LogLevel::ERROR, sprintf(
114 1
                    'Cannot get video streams info for file: %s, message is: %s',
115 1
                    $this->file,
116 1
                    $e->getMessage()
117
                ));
118 1
                throw $e;
119
            }
120
        }
121
122 5
        return $this->cachedVideoStreams;
123
    }
124
125
    /**
126
     * Return VideoStreams as a collection.
127
     *
128
     * @throws InvalidStreamMetadataException
129
     */
130 1
    public function getAudioStreams(): AudioStreamCollectionInterface
131
    {
132 1
        if ($this->cachedAudioStreams === null) {
133
            try {
134 1
                $audioStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
135 1
                $this->cachedAudioStreams = new AudioStreamCollection($audioStreamsMetadata);
136
            } catch (InvalidStreamMetadataException $e) {
137
                $this->logger->log(LogLevel::ERROR, sprintf(
138
                    'Cannot get audio streams info for file: %s, message is: %s',
139
                    $this->file,
140
                    $e->getMessage()
141
                ));
142
                throw $e;
143
            }
144
        }
145
146 1
        return $this->cachedAudioStreams;
147
    }
148
149
    /**
150
     * Format name as returned by ffprobe.
151
     */
152 2
    public function getFormatName(): string
153
    {
154 2
        return $this->metadata['format']['format_name'];
155
    }
156
157
    /**
158
     * @param string $streamType any of self::SUPPORTED_STREAM_TYPES
159
     */
160 2
    public function countStreams(?string $streamType = null): int
161
    {
162 2
        if ($streamType === null) {
163 2
            return count($this->metadata['streams'] ?? []);
164
        }
165
166 1
        return count($this->getStreamsMetadataByType($streamType));
167
    }
168
169 1
    public function getMetadata(): array
170
    {
171 1
        return $this->metadata;
172
    }
173
174 2
    public function getDuration(): float
175
    {
176 2
        return (float) ($this->metadata['format']['duration'] ?? 0.0);
177
    }
178
179
    /**
180
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
181
     *
182
     * @return array<string, int> associative array with 'height' and 'width'
183
     */
184 2
    public function getDimensions(int $streamIndex = 0): array
185
    {
186
        return [
187 2
            'width'  => $this->getWidth($streamIndex),
188 2
            'height' => $this->getHeight($streamIndex),
189
        ];
190
    }
191
192
    /**
193
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
194
     */
195 3
    public function getWidth(int $streamIndex = 0): int
196
    {
197 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
198
199 3
        return (int) ($videoStream['width'] ?? 0);
200
    }
201
202
    /**
203
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
204
     */
205 3
    public function getHeight(int $streamIndex = 0): int
206
    {
207 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
208
209 3
        return (int) ($videoStream['height'] ?? 0);
210
    }
211
212
    /**
213
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
214
     */
215 3
    public function getNbFrames(int $streamIndex = 0): int
216
    {
217 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
218
219 3
        return (int) ($videoStream['nb_frames'] ?? 0);
220
    }
221
222
    /**
223
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
224
     */
225 1
    public function getVideoBitrate(int $streamIndex = 0): int
226
    {
227 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
228
229 1
        return (int) ($videoStream['bit_rate'] ?? 0);
230
    }
231
232
    /**
233
     * Convenience method to get audio stream bitrate.
234
     *
235
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
236
     */
237 1
    public function getAudioBitrate(int $streamIndex = 0): int
238
    {
239 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
240
241 1
        return (int) ($audioStream['bit_rate'] ?? 0);
242
    }
243
244
    /**
245
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
246
     */
247 1
    public function getAudioCodecName(int $streamIndex = 0): ?string
248
    {
249 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
250
251 1
        return $audioStream['codec_name'] ?? null;
252
    }
253
254
    /**
255
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
256
     */
257 1
    public function getVideoCodecName(int $streamIndex = 0): ?string
258
    {
259 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
260
261 1
        return $videoStream['codec_name'] ?? null;
262
    }
263
264
    /**
265
     * @return array<int, array>
266
     */
267 2
    public function getAudioStreamsMetadata(): array
268
    {
269 2
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
270
    }
271
272
    /**
273
     * @return array<int, array>
274
     */
275 8
    public function getVideoStreamsMetadata(): array
276
    {
277 8
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
278
    }
279
280
    /**
281
     * @throws InvalidArgumentException
282
     *
283
     * @param string $streamType  any of self::SUPPORTED_STREAM_TYPES
284
     * @param int    $streamIndex selected a specific stream by index, default: 0 = the first available
285
     *
286
     * @return array<string|int, array>
287
     *
288
     * @throws InvalidStreamMetadataException
289
     */
290 17
    public function getStreamsMetadataByType(string $streamType, int $streamIndex = 0): array
291
    {
292 17
        if (!in_array($streamType, self::SUPPORTED_STREAM_TYPES, true)) {
293 1
            $msg = sprintf(
294 1
                'Invalid usage, unsupported param $streamType given: %s',
295 1
                $streamType
296
            );
297 1
            $this->logger->log(LogLevel::ERROR, $msg);
298 1
            throw new InvalidArgumentException($msg);
299
        }
300
301 16
        return $this->getMetadataByStreamType()[$streamType];
302
    }
303
304
    /**
305
     * @throws InvalidStreamMetadataException
306
     */
307 16
    private function getMetadataByStreamType(): array
308
    {
309 16
        if ($this->metadataByStreamType === null) {
310
            try {
311
                $streams = [
312 16
                    self::STREAM_TYPE_VIDEO => [],
313 16
                    self::STREAM_TYPE_AUDIO => [],
314 16
                    self::STREAM_TYPE_DATA  => [],
315
                ];
316 16
                if (!is_array($this->metadata['streams'])) {
317
                    throw new InvalidStreamMetadataException(sprintf(
318
                        'Invalid or unsupported stream metadata returned by ffprobe: %s',
319
                        (string) json_encode($this->metadata)
320
                    ));
321
                }
322
323 16
                foreach ($this->metadata['streams'] as $stream) {
324 16
                    if (!is_array($stream)) {
325 1
                        throw new InvalidStreamMetadataException(sprintf(
326 1
                            'Stream metadata returned by ffprobe must be an array: %s',
327 1
                            (string) json_encode($stream)
328
                        ));
329
                    }
330
331 15
                    if (!isset($stream['codec_type'])) {
332
                        throw new InvalidStreamMetadataException(sprintf(
333
                            'Missing codec_type information in metadata returned by ffprobe: %s',
334
                            (string) json_encode($stream)
335
                        ));
336
                    }
337
338 15
                    $type = mb_strtolower($stream['codec_type']);
339
                    switch ($type) {
340 15
                        case self::STREAM_TYPE_VIDEO:
341 15
                            $streams[self::STREAM_TYPE_VIDEO][] = $stream;
342 15
                            break;
343 15
                        case self::STREAM_TYPE_AUDIO:
344 15
                            $streams[self::STREAM_TYPE_AUDIO][] = $stream;
345 15
                            break;
346
                        case self::STREAM_TYPE_DATA:
347
                            $streams[self::STREAM_TYPE_DATA][] = $stream;
348
                            break;
349
                        default:
350
                            $streams[$type][] = $stream;
351
                    }
352
                }
353
354 15
                $this->metadataByStreamType = $streams;
355 1
            } catch (InvalidStreamMetadataException $e) {
356 1
                $this->logger->log(LogLevel::ERROR, sprintf(
357 1
                    'Cannot read metadata for file: %s. Failed with message: %s',
358 1
                    $this->file,
359 1
                    $e->getMessage()
360
                ));
361 1
                throw $e;
362
            }
363
        }
364
365 15
        return $this->metadataByStreamType;
366
    }
367
}
368