Passed
Push — master ( 81b9b0...8c7e20 )
by Sébastien
02:17
created

VideoInfo::getDuration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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