Passed
Push — master ( 3b2ed5...ac7e73 )
by Sébastien
02:17
created

VideoInfo   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Test Coverage

Coverage 90.38%

Importance

Changes 0
Metric Value
wmc 36
eloc 91
dl 0
loc 290
c 0
b 0
f 0
ccs 94
cts 104
cp 0.9038
rs 9.52

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getDimensions() 0 5 1
A getMetadata() 0 3 1
A getWidth() 0 5 1
A getDuration() 0 3 1
A getNbFrames() 0 5 1
A getStreamsMetadataByType() 0 10 2
A getVideoBitrate() 0 5 1
A countStreams() 0 7 2
A getFormatName() 0 3 1
A getFile() 0 3 1
A getAudioBitrate() 0 5 1
A __construct() 0 10 2
A getHeight() 0 5 1
A getFileSize() 0 11 2
A getVideoStreams() 0 8 2
A createFromFFProbeJson() 0 11 3
B getMetadataByStreamType() 0 51 9
A getAudioStreamsMetadata() 0 3 1
A getVideoStreamsMetadata() 0 3 1
A getAudioCodecName() 0 5 1
A getVideoCodecName() 0 5 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 Soluble\MediaTools\Common\Exception\IOException;
15
use Soluble\MediaTools\Common\Exception\JsonParseException;
16
use Soluble\MediaTools\Video\Exception\InvalidArgumentException;
17
use Soluble\MediaTools\Video\Exception\InvalidStreamMetadataException;
18
use Soluble\MediaTools\Video\Info\VideoStreamCollection;
19
20
class VideoInfo implements VideoInfoInterface
21
{
22
    /** @var array<string, mixed> */
23
    private $metadata;
24
25
    /** @var string */
26
    private $file;
27
28
    /** @var array|null */
29
    private $metadataByStreamType;
30
31
    /** @var VideoStreamCollection|null; */
32
    private $cachedVideoStreams;
33
34
    /**
35
     * @param string $fileName reference to filename
36
     * @param array  $metadata metadata as parsed from ffprobe --json
37
     *
38
     * @throws IOException
39
     */
40 18
    public function __construct(string $fileName, array $metadata)
41
    {
42 18
        if (!file_exists($fileName)) {
43 1
            throw new IOException(sprintf(
44 1
                'File %s does not exists',
45 1
                $this->file
46
            ));
47
        }
48 17
        $this->metadata = $metadata;
49 17
        $this->file     = $fileName;
50 17
    }
51
52
    /**
53
     * @throws JsonParseException if json is invalid
54
     */
55 5
    public static function createFromFFProbeJson(string $fileName, string $ffprobeJson): self
56
    {
57 5
        if (trim($ffprobeJson) === '') {
58 1
            throw new JsonParseException('Cannot parse empty json string');
59
        }
60 4
        $decoded = json_decode($ffprobeJson, true);
61 4
        if ($decoded === null) {
62 1
            throw new JsonParseException('Cannot parse json');
63
        }
64
65 3
        return new self($fileName, $decoded);
66
    }
67
68 2
    public function getFile(): string
69
    {
70 2
        return $this->file;
71
    }
72
73
    /**
74
     * @throws IOException
75
     */
76 2
    public function getFileSize(): int
77
    {
78 2
        $size = @filesize($this->file);
79 2
        if ($size === false) {
80 1
            throw new IOException(sprintf(
81 1
                'Cannot get filesize of file %s',
82 1
                $this->file
83
            ));
84
        }
85
86 1
        return $size;
87
    }
88
89
    /**
90
     * Return VideoStreams as a collection
91
     * @throws InvalidStreamMetadataException
92
     */
93 3
    public function getVideoStreams(): VideoStreamCollection
94
    {
95 3
        if ($this->cachedVideoStreams === null) {
96 3
            $videoStreamsMetadata     = array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
97 2
            $this->cachedVideoStreams = new VideoStreamCollection($videoStreamsMetadata);
98
        }
99
100 2
        return $this->cachedVideoStreams;
101
    }
102
103
    /**
104
     * Format name as returned by ffprobe.
105
     */
106 2
    public function getFormatName(): string
107
    {
108 2
        return $this->metadata['format']['format_name'];
109
    }
110
111
    /**
112
     * @param string $streamType any of self::SUPPORTED_STREAM_TYPES
113
     */
114 2
    public function countStreams(?string $streamType = null): int
115
    {
116 2
        if ($streamType === null) {
117 2
            return count($this->metadata['streams'] ?? []);
118
        }
119
120 1
        return count($this->getStreamsMetadataByType($streamType));
121
    }
122
123 1
    public function getMetadata(): array
124
    {
125 1
        return $this->metadata;
126
    }
127
128 2
    public function getDuration(): float
129
    {
130 2
        return (float) ($this->metadata['format']['duration'] ?? 0.0);
131
    }
132
133
    /**
134
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
135
     *
136
     * @return array<string, int> associative array with 'height' and 'width'
137
     */
138 2
    public function getDimensions(int $streamIndex = 0): array
139
    {
140
        return [
141 2
            'width'  => $this->getWidth($streamIndex),
142 2
            'height' => $this->getHeight($streamIndex),
143
        ];
144
    }
145
146
    /**
147
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
148
     */
149 3
    public function getWidth(int $streamIndex = 0): int
150
    {
151 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
152
153 3
        return (int) ($videoStream['width'] ?? 0);
154
    }
155
156
    /**
157
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
158
     */
159 3
    public function getHeight(int $streamIndex = 0): int
160
    {
161 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
162
163 3
        return (int) ($videoStream['height'] ?? 0);
164
    }
165
166
    /**
167
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
168
     */
169 3
    public function getNbFrames(int $streamIndex = 0): int
170
    {
171 3
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
172
173 3
        return (int) ($videoStream['nb_frames'] ?? 0);
174
    }
175
176
    /**
177
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
178
     */
179 1
    public function getVideoBitrate(int $streamIndex = 0): int
180
    {
181 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
182
183 1
        return (int) ($videoStream['bit_rate'] ?? 0);
184
    }
185
186
    /**
187
     * Convenience method to get audio stream bitrate.
188
     *
189
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
190
     */
191 1
    public function getAudioBitrate(int $streamIndex = 0): int
192
    {
193 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
194
195 1
        return (int) ($audioStream['bit_rate'] ?? 0);
196
    }
197
198
    /**
199
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
200
     */
201 1
    public function getAudioCodecName(int $streamIndex = 0): ?string
202
    {
203 1
        $audioStream = $this->getAudioStreamsMetadata()[$streamIndex] ?? [];
204
205 1
        return $audioStream['codec_name'] ?? null;
206
    }
207
208
    /**
209
     * @param int $streamIndex selected a specific stream by index, default: 0 = the first available
210
     */
211 1
    public function getVideoCodecName(int $streamIndex = 0): ?string
212
    {
213 1
        $videoStream = $this->getVideoStreamsMetadata()[$streamIndex] ?? [];
214
215 1
        return $videoStream['codec_name'] ?? null;
216
    }
217
218
    /**
219
     * @return array<int, array>
220
     */
221 2
    public function getAudioStreamsMetadata(): array
222
    {
223 2
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_AUDIO));
224
    }
225
226
    /**
227
     * @return array<int, array>
228
     */
229 7
    public function getVideoStreamsMetadata(): array
230
    {
231 7
        return array_values($this->getStreamsMetadataByType(self::STREAM_TYPE_VIDEO));
232
    }
233
234
    /**
235
     * @throws InvalidArgumentException
236
     *
237
     * @param string $streamType  any of self::SUPPORTED_STREAM_TYPES
238
     * @param int    $streamIndex selected a specific stream by index, default: 0 = the first available
239
     *
240
     * @return array<string|int, array>
241
     *
242
     * @throws InvalidStreamMetadataException
243
     */
244 12
    public function getStreamsMetadataByType(string $streamType, int $streamIndex = 0): array
245
    {
246 12
        if (!in_array($streamType, self::SUPPORTED_STREAM_TYPES, true)) {
247 1
            throw new InvalidArgumentException(sprintf(
248 1
                'Invalid usage, unsupported param $streamType given: %s',
249 1
                $streamType
250
            ));
251
        }
252
253 11
        return $this->getMetadataByStreamType()[$streamType];
254
    }
255
256
    /**
257
     * @throws InvalidStreamMetadataException
258
     */
259 11
    private function getMetadataByStreamType(): array
260
    {
261 11
        if ($this->metadataByStreamType === null) {
262
            $streams = [
263 11
                self::STREAM_TYPE_VIDEO => [],
264 11
                self::STREAM_TYPE_AUDIO => [],
265 11
                self::STREAM_TYPE_DATA  => [],
266
            ];
267 11
            if (!is_array($this->metadata['streams'])) {
268
                throw new InvalidStreamMetadataException(sprintf(
269
                    'Invalid or unsupported stream metadata returned by ffprobe: %s',
270
                    (string) json_encode($this->metadata)
271
                ));
272
            }
273
274 11
            foreach ($this->metadata['streams'] as $stream) {
275
276 11
                if (!is_array($stream)) {
277 1
                    throw new InvalidStreamMetadataException(sprintf(
278 1
                        'Stream metadata returned by ffprobe must be an array: %s',
279 1
                        (string) json_encode($stream)
280
                    ));
281
                }
282
283
284 10
                if (!isset($stream['codec_type'])) {
285
                    throw new InvalidStreamMetadataException(sprintf(
286
                        'Missing codec_type information in metadata returned by ffprobe: %s',
287
                        (string) json_encode($stream)
288
                    ));
289
                }
290
291 10
                $type = mb_strtolower($stream['codec_type']);
292
                switch ($type) {
293 10
                    case self::STREAM_TYPE_VIDEO:
294 10
                        $streams[self::STREAM_TYPE_VIDEO][] = $stream;
295 10
                        break;
296 10
                    case self::STREAM_TYPE_AUDIO:
297 10
                        $streams[self::STREAM_TYPE_AUDIO][] = $stream;
298 10
                        break;
299
                    case self::STREAM_TYPE_DATA:
300
                        $streams[self::STREAM_TYPE_DATA][] = $stream;
301
                        break;
302
                    default:
303
                        $streams[$type][] = $stream;
304
                }
305
            }
306 10
            $this->metadataByStreamType = $streams;
307
        }
308
309 10
        return $this->metadataByStreamType;
310
    }
311
}
312