Passed
Push — master ( 3d8be6...81b9b0 )
by Sébastien
02:32
created

VideoInfo::getVideoCodecName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 Psr\SimpleCache\CacheInterface;
18
use Soluble\MediaTools\Common\Cache\NullCache;
19
use Soluble\MediaTools\Common\Exception\IOException;
20
use Soluble\MediaTools\Common\Exception\JsonParseException;
21
use Soluble\MediaTools\Video\Exception\InvalidArgumentException;
22
use Soluble\MediaTools\Video\Exception\InvalidStreamMetadataException;
23
use Soluble\MediaTools\Video\Info\AudioStreamCollection;
24
use Soluble\MediaTools\Video\Info\AudioStreamCollectionInterface;
25
use Soluble\MediaTools\Video\Info\SubtitleStreamCollection;
26
use Soluble\MediaTools\Video\Info\SubtitleStreamCollectionInterface;
27
use Soluble\MediaTools\Video\Info\VideoStreamCollection;
28
use Soluble\MediaTools\Video\Info\VideoStreamCollectionInterface;
29
30
class VideoInfo implements VideoInfoInterface
31
{
32
    /** @var array<string, mixed> */
33
    private $metadata;
34
35
    /** @var string */
36
    private $file;
37
38
    /** @var LoggerInterface */
39
    private $logger;
40
41
    /** @var CacheInterface */
42
    private $cache;
43
44
    /** @var array|null */
45
    private $metadataByStreamType;
46
47
    /** @var VideoStreamCollectionInterface|null; */
48
    private $cachedVideoStreams;
49
50
    /** @var AudioStreamCollectionInterface|null; */
51
    private $cachedAudioStreams;
52
53
    /** @var SubtitleStreamCollectionInterface|null; */
54
    private $cachedSubtitleStreams;
55
56
    /**
57
     * @param string               $fileName reference to filename
58
     * @param array                $metadata metadata as parsed from ffprobe --json
59
     * @param LoggerInterface|null $logger
60
     */
61 31
    public function __construct(string $fileName, array $metadata, ?LoggerInterface $logger = null, ?CacheInterface $cache = null)
62
    {
63 31
        if (!file_exists($fileName)) {
64 1
            throw new IOException(sprintf(
65 1
                'File %s does not exists',
66 1
                $this->file
67
            ));
68
        }
69 30
        $this->metadata = $metadata;
70 30
        $this->file     = $fileName;
71 30
        $this->logger   = $logger ?? new NullLogger();
72 30
        $this->cache    = $cache ?? new NullCache();
73 30
    }
74
75
    /**
76
     * @throws JsonParseException if json is invalid
77
     */
78 10
    public static function createFromFFProbeJson(string $fileName, string $ffprobeJson, ?LoggerInterface $logger = null): self
79
    {
80 10
        if (trim($ffprobeJson) === '') {
81 1
            throw new JsonParseException('Cannot parse empty json string');
82
        }
83 9
        $decoded = json_decode($ffprobeJson, true);
84 9
        if ($decoded === null) {
85 1
            throw new JsonParseException('Cannot parse json');
86
        }
87
88 8
        return new self($fileName, $decoded, $logger);
89
    }
90
91 2
    public function getFile(): string
92
    {
93 2
        return $this->file;
94
    }
95
96
    /**
97
     * @throws IOException
98
     */
99 2
    public function getFileSize(): int
100
    {
101 2
        $size = @filesize($this->file);
102 2
        if ($size === false) {
103 1
            $msg = sprintf('Cannot get filesize of file %s', $this->file);
104 1
            $this->logger->log(LogLevel::ERROR, $msg);
105 1
            throw new IOException($msg);
106
        }
107
108 1
        return $size;
109
    }
110
111
    /**
112
     * Return VideoStreams as a collection.
113
     *
114
     * @throws InvalidStreamMetadataException
115
     */
116 7
    public function getVideoStreams(): VideoStreamCollectionInterface
117
    {
118 7
        if ($this->cachedVideoStreams === null) {
119
            try {
120 7
                $videoStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
121 6
                $this->cachedVideoStreams = new VideoStreamCollection($videoStreamsMetadata);
122 1
            } catch (InvalidStreamMetadataException $e) {
123 1
                $this->logger->log(LogLevel::ERROR, sprintf(
124 1
                    'Cannot get video streams info for file: %s, message is: %s',
125 1
                    $this->file,
126 1
                    $e->getMessage()
127
                ));
128 1
                throw $e;
129
            }
130
        }
131
132 6
        return $this->cachedVideoStreams;
133
    }
134
135
    /**
136
     * Return SubtitleStreams as a collection.
137
     *
138
     * @throws InvalidStreamMetadataException
139
     */
140 2
    public function getSubtitleStreams(): SubtitleStreamCollectionInterface
141
    {
142 2
        if ($this->cachedSubtitleStreams === null) {
143
            try {
144 2
                $streamsMetadata             = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_SUBTITLE));
145 2
                $this->cachedSubtitleStreams = new SubtitleStreamCollection($streamsMetadata);
146
            } catch (InvalidStreamMetadataException $e) {
147
                $this->logger->log(LogLevel::ERROR, sprintf(
148
                    'Cannot get subtitle streams info for file: %s, message is: %s',
149
                    $this->file,
150
                    $e->getMessage()
151
                ));
152
                throw $e;
153
            }
154
        }
155
156 2
        return $this->cachedSubtitleStreams;
157
    }
158
159
    /**
160
     * Return VideoStreams as a collection.
161
     *
162
     * @throws InvalidStreamMetadataException
163
     */
164 3
    public function getAudioStreams(): AudioStreamCollectionInterface
165
    {
166 3
        if ($this->cachedAudioStreams === null) {
167
            try {
168 3
                $audioStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
169 2
                $this->cachedAudioStreams = new AudioStreamCollection($audioStreamsMetadata);
170 1
            } catch (InvalidStreamMetadataException $e) {
171 1
                $this->logger->log(LogLevel::ERROR, sprintf(
172 1
                    'Cannot get audio streams info for file: %s, message is: %s',
173 1
                    $this->file,
174 1
                    $e->getMessage()
175
                ));
176 1
                throw $e;
177
            }
178
        }
179
180 2
        return $this->cachedAudioStreams;
181
    }
182
183
    /**
184
     * Format name as returned by ffprobe.
185
     */
186 3
    public function getFormatName(): string
187
    {
188 3
        return $this->metadata['format']['format_name'];
189
    }
190
191
    /**
192
     * @param string $streamType any of self::SUPPORTED_STREAM_TYPES
193
     */
194 4
    public function countStreams(?string $streamType = null): int
195
    {
196 4
        if ($streamType === null) {
197 4
            return count($this->metadata['streams'] ?? []);
198
        }
199
200 1
        return count($this->getStreamsMetadataByType($streamType));
201
    }
202
203
    /**
204
     * Return metadata as received by ffprobe.
205
     *
206
     * @return array<string, array>
207
     */
208 1
    public function getMetadata(): array
209
    {
210 1
        return $this->metadata;
211
    }
212
213
    /**
214
     * Return total duration.
215
     */
216 2
    public function getDuration(): float
217
    {
218 2
        return (float) ($this->metadata['format']['duration'] ?? 0.0);
219
    }
220
221
    /**
222
     * @deprecated
223
     *
224
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
225
     *
226
     * @return array<string, int> associative array with 'height' and 'width'
227
     */
228 1
    public function getDimensions(int $streamIndex = 0): array
229
    {
230
        return [
231 1
            '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

231
            'width'  => /** @scrutinizer ignore-deprecated */ $this->getWidth($streamIndex),
Loading history...
232 1
            '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

232
            'height' => /** @scrutinizer ignore-deprecated */ $this->getHeight($streamIndex),
Loading history...
233
        ];
234
    }
235
236
    /**
237
     * @deprecated
238
     *
239
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
240
     */
241 2
    public function getWidth(int $streamIndex = 0): int
242
    {
243 2
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
244
245 2
        return (int) ($videoStream['width'] ?? 0);
246
    }
247
248
    /**
249
     * @deprecated
250
     *
251
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
252
     */
253 2
    public function getHeight(int $streamIndex = 0): int
254
    {
255 2
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
256
257 2
        return (int) ($videoStream['height'] ?? 0);
258
    }
259
260
    /**
261
     * @deprecated
262
     *
263
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
264
     */
265 2
    public function getNbFrames(int $streamIndex = 0): int
266
    {
267 2
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
268
269 2
        return (int) ($videoStream['nb_frames'] ?? 0);
270
    }
271
272
    /**
273
     * @deprecated
274
     *
275
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
276
     */
277 1
    public function getVideoBitrate(int $streamIndex = 0): int
278
    {
279 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
280
281 1
        return (int) ($videoStream['bit_rate'] ?? 0);
282
    }
283
284
    /**
285
     * @deprecated
286
     *
287
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
288
     */
289 1
    public function getAudioBitrate(int $streamIndex = 0): int
290
    {
291 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
292
293 1
        return (int) ($audioStream['bit_rate'] ?? 0);
294
    }
295
296
    /**
297
     * @deprecated
298
     *
299
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
300
     */
301 1
    public function getAudioCodecName(int $streamIndex = 0): ?string
302
    {
303 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
304
305 1
        return $audioStream['codec_name'] ?? null;
306
    }
307
308
    /**
309
     * @deprecated
310
     *
311
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
312
     */
313 1
    public function getVideoCodecName(int $streamIndex = 0): ?string
314
    {
315 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
316
317 1
        return $videoStream['codec_name'] ?? null;
318
    }
319
320
    /**
321
     * @return array<int, array>
322
     */
323 3
    public function getAudioStreamsMetadata(): array
324
    {
325 3
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
326
    }
327
328
    /**
329
     * @return array<int, array>
330
     */
331 7
    public function getVideoStreamsMetadata(): array
332
    {
333 7
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
334
    }
335
336
    /**
337
     * @throws InvalidArgumentException
338
     *
339
     * @param string $streamType 'audio', 'video', 'subtitle', 'data' / any of self::SUPPORTED_STREAM_TYPES
340
     *
341
     * @return array<int, array<string, mixed>>
342
     *
343
     * @throws InvalidStreamMetadataException
344
     */
345 25
    public function getStreamsMetadataByType(string $streamType): array
346
    {
347 25
        if (!in_array($streamType, self::SUPPORTED_STREAM_TYPES, true)) {
348 1
            $msg = sprintf(
349 1
                'Invalid usage, unsupported param $streamType given: %s',
350 1
                $streamType
351
            );
352 1
            $this->logger->log(LogLevel::ERROR, $msg);
353 1
            throw new InvalidArgumentException($msg);
354
        }
355
356 24
        return $this->getMetadataByStreamType()[$streamType];
357
    }
358
359
    /**
360
     * @throws InvalidStreamMetadataException
361
     */
362 24
    private function getMetadataByStreamType(): array
363
    {
364 24
        if ($this->metadataByStreamType === null) {
365
            try {
366
                $streams = [
367 24
                    self::STREAM_TYPE_VIDEO    => [],
368 24
                    self::STREAM_TYPE_AUDIO    => [],
369 24
                    self::STREAM_TYPE_DATA     => [],
370 24
                    self::STREAM_TYPE_SUBTITLE => [],
371
                ];
372 24
                if (!is_array($this->metadata['streams'] ?? null)) {
373 1
                    throw new InvalidStreamMetadataException(sprintf(
374 1
                        'Invalid or unsupported stream metadata returned by ffprobe: %s',
375 1
                        (string) json_encode($this->metadata)
376
                    ));
377
                }
378
379 23
                foreach ($this->metadata['streams'] as $stream) {
380 23
                    if (!is_array($stream)) {
381 2
                        throw new InvalidStreamMetadataException(sprintf(
382 2
                            'Stream metadata returned by ffprobe must be an array: %s',
383 2
                            (string) json_encode($stream)
384
                        ));
385
                    }
386
387 21
                    if (!isset($stream['codec_type'])) {
388 1
                        throw new InvalidStreamMetadataException(sprintf(
389 1
                            'Missing codec_type information in metadata returned by ffprobe: %s',
390 1
                            (string) json_encode($stream)
391
                        ));
392
                    }
393
394 20
                    $type = mb_strtolower($stream['codec_type']);
395
                    switch ($type) {
396 20
                        case self::STREAM_TYPE_VIDEO:
397 19
                            $streams[self::STREAM_TYPE_VIDEO][] = $stream;
398 19
                            break;
399 20
                        case self::STREAM_TYPE_AUDIO:
400 19
                            $streams[self::STREAM_TYPE_AUDIO][] = $stream;
401 19
                            break;
402 14
                        case self::STREAM_TYPE_DATA:
403
                            $streams[self::STREAM_TYPE_DATA][] = $stream;
404
                            break;
405 14
                        case self::STREAM_TYPE_SUBTITLE:
406 14
                            $streams[self::STREAM_TYPE_SUBTITLE][] = $stream;
407 14
                            break;
408
409
                        default:
410
                            $streams[$type][] = $stream;
411
                    }
412
                }
413
414 20
                $this->metadataByStreamType = $streams;
415 4
            } catch (InvalidStreamMetadataException $e) {
416 4
                $this->logger->log(LogLevel::ERROR, sprintf(
417 4
                    'Cannot read metadata for file: %s. Failed with message: %s',
418 4
                    $this->file,
419 4
                    $e->getMessage()
420
                ));
421 4
                throw $e;
422
            }
423
        }
424
425 20
        return $this->metadataByStreamType;
426
    }
427
}
428