Passed
Push — master ( 461174...e40577 )
by Sébastien
02:22
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 29
    public function __construct(string $fileName, array $metadata, ?LoggerInterface $logger = null)
57
    {
58 29
        if (!file_exists($fileName)) {
59 1
            throw new IOException(sprintf(
60 1
                'File %s does not exists',
61 1
                $this->file
62
            ));
63
        }
64 28
        $this->metadata = $metadata;
65 28
        $this->file     = $fileName;
66 28
        $this->logger   = $logger ?? new NullLogger();
67 28
    }
68
69
    /**
70
     * @throws JsonParseException if json is invalid
71
     */
72 13
    public static function createFromFFProbeJson(string $fileName, string $ffprobeJson, ?LoggerInterface $logger = null): self
73
    {
74 13
        if (trim($ffprobeJson) === '') {
75 1
            throw new JsonParseException('Cannot parse empty json string');
76
        }
77 12
        $decoded = json_decode($ffprobeJson, true);
78 12
        if (!is_array($decoded)) {
79 2
            throw new JsonParseException('Cannot parse json');
80
        }
81
82 11
        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 8
    public function getVideoStreams(): VideoStreamCollectionInterface
111
    {
112 8
        if ($this->cachedVideoStreams === null) {
113
            try {
114 8
                $videoStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
115 7
                $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 7
        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 5
    public function getDuration(): float
211
    {
212 5
        return (float) ($this->metadata['format']['duration'] ?? 0.0);
213
    }
214
215
    /**
216
     * @return array<int, array>
217
     */
218 1
    public function getAudioStreamsMetadata(): array
219
    {
220 1
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
221
    }
222
223
    /**
224
     * @return array<int, array>
225
     */
226 1
    public function getVideoStreamsMetadata(): array
227
    {
228 1
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
229
    }
230
231
    /**
232
     * @throws InvalidArgumentException
233
     *
234
     * @param string $streamType 'audio', 'video', 'subtitle', 'data' / any of self::SUPPORTED_STREAM_TYPES
235
     *
236
     * @return array<int, array<string, mixed>>
237
     *
238
     * @throws InvalidStreamMetadataException
239
     */
240 20
    public function getStreamsMetadataByType(string $streamType): array
241
    {
242 20
        if (!in_array($streamType, self::SUPPORTED_STREAM_TYPES, true)) {
243 1
            $msg = sprintf(
244 1
                'Invalid usage, unsupported param $streamType given: %s',
245 1
                $streamType
246
            );
247 1
            $this->logger->log(LogLevel::ERROR, $msg);
248 1
            throw new InvalidArgumentException($msg);
249
        }
250
251 19
        return $this->getMetadataByStreamType()[$streamType];
252
    }
253
254
    /**
255
     * @throws InvalidStreamMetadataException
256
     */
257 19
    private function getMetadataByStreamType(): array
258
    {
259 19
        if ($this->metadataByStreamType === null) {
260
            try {
261
                $streams = [
262 19
                    self::STREAM_TYPE_VIDEO    => [],
263 19
                    self::STREAM_TYPE_AUDIO    => [],
264 19
                    self::STREAM_TYPE_DATA     => [],
265 19
                    self::STREAM_TYPE_SUBTITLE => [],
266
                ];
267 19
                if (!is_array($this->metadata['streams'] ?? null)) {
268 1
                    throw new InvalidStreamMetadataException(sprintf(
269 1
                        'Invalid or unsupported stream metadata returned by ffprobe: %s',
270 1
                        (string) json_encode($this->metadata)
271
                    ));
272
                }
273
274 18
                foreach ($this->metadata['streams'] as $stream) {
275 18
                    if (!is_array($stream)) {
276 2
                        throw new InvalidStreamMetadataException(sprintf(
277 2
                            'Stream metadata returned by ffprobe must be an array: %s',
278 2
                            (string) json_encode($stream)
279
                        ));
280
                    }
281
282 16
                    if (!isset($stream['codec_type'])) {
283 1
                        throw new InvalidStreamMetadataException(sprintf(
284 1
                            'Missing codec_type information in metadata returned by ffprobe: %s',
285 1
                            (string) json_encode($stream)
286
                        ));
287
                    }
288
289 15
                    $type = mb_strtolower($stream['codec_type']);
290
                    switch ($type) {
291 15
                        case self::STREAM_TYPE_VIDEO:
292 14
                            $streams[self::STREAM_TYPE_VIDEO][] = $stream;
293 14
                            break;
294 15
                        case self::STREAM_TYPE_AUDIO:
295 14
                            $streams[self::STREAM_TYPE_AUDIO][] = $stream;
296 14
                            break;
297 9
                        case self::STREAM_TYPE_DATA:
298
                            $streams[self::STREAM_TYPE_DATA][] = $stream;
299
                            break;
300 9
                        case self::STREAM_TYPE_SUBTITLE:
301 9
                            $streams[self::STREAM_TYPE_SUBTITLE][] = $stream;
302 9
                            break;
303
304
                        default:
305
                            $streams[$type][] = $stream;
306
                    }
307
                }
308
309 15
                $this->metadataByStreamType = $streams;
310 4
            } catch (InvalidStreamMetadataException $e) {
311 4
                $this->logger->log(LogLevel::ERROR, sprintf(
312 4
                    'Cannot read metadata for file: %s. Failed with message: %s',
313 4
                    $this->file,
314 4
                    $e->getMessage()
315
                ));
316 4
                throw $e;
317
            }
318
        }
319
320 15
        return $this->metadataByStreamType;
321
    }
322
}
323