Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.65% covered (warning)
58.65%
122 / 208
42.86% covered (danger)
42.86%
9 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
OggHandler
58.65% covered (warning)
58.65%
122 / 208
42.86% covered (danger)
42.86%
9 / 21
508.03
0.00% covered (danger)
0.00%
0 / 1
 getMetadata
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
4
 formatMetadata
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCommonMetaArray
95.35% covered (success)
95.35%
41 / 43
0.00% covered (danger)
0.00%
0 / 1
14
 getImageSize
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
9.08
 unpackMetadata
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 getMetadataType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWebType
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getStreamTypes
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getOffset
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLength
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getContentHeaders
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 findStream
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 findVideoStream
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 findAudioStream
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFramerate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 hasVideo
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasAudio
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAudioChannels
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getShortDesc
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getLongDesc
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
110
 getBitRate
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace MediaWiki\TimedMediaHandler\Handlers\OggHandler;
4
5use File;
6use File_Ogg;
7use IContextSource;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\TimedMediaHandler\TimedMediaHandler;
10
11/**
12 * ogg handler
13 */
14class OggHandler extends TimedMediaHandler {
15    private const METADATA_VERSION = 2;
16
17    /**
18     * @param File $image
19     * @param string $path
20     * @return string
21     */
22    public function getMetadata( $image, $path ) {
23        $metadata = [ 'version' => self::METADATA_VERSION ];
24
25        try {
26            $f = new File_Ogg( $path );
27            $streams = [];
28            foreach ( $f->listStreams() as $streamIDs ) {
29                foreach ( $streamIDs as $streamID ) {
30                    $stream = $f->getStream( $streamID );
31                    '@phan-var \File_Ogg_Media $stream';
32                    $streams[$streamID] = [
33                        'serial' => $stream->getSerial(),
34                        'group' => $stream->getGroup(),
35                        'type' => $stream->getType(),
36                        'vendor' => $stream->getVendor(),
37                        'length' => $stream->getLength(),
38                        'size' => $stream->getSize(),
39                        'header' => $stream->getHeader(),
40                        'comments' => $stream->getComments()
41                    ];
42                }
43            }
44            $metadata['streams'] = $streams;
45            $metadata['length'] = $f->getLength();
46            // Get the offset of the file (in cases where the file is a segment copy)
47            $metadata['offset'] = $f->getStartOffset();
48        } catch ( OggException $e ) {
49            // File not found, invalid stream, etc.
50            $metadata['error'] = [
51                'message' => $e->getMessage(),
52                'code' => $e->getCode()
53            ];
54        }
55        return serialize( $metadata );
56    }
57
58    /**
59     * Display metadata box on file description page.
60     *
61     * This is pretty basic, it puts data from all the streams together,
62     * and only outputs a couple of the most commonly used ogg "comments",
63     * with comments from all the streams combined
64     *
65     * @param File $file
66     * @param false|IContextSource $context Context to use (optional)
67     * @return array|false
68     */
69    public function formatMetadata( $file, $context = false ) {
70        $meta = $this->getCommonMetaArray( $file );
71        if ( count( $meta ) === 0 ) {
72            return false;
73        }
74        return $this->formatMetadataHelper( $meta, $context );
75    }
76
77    /**
78     * Get some basic metadata properties that are common across file types.
79     *
80     * @param File $file
81     * @return array Array of metadata. See MW's FormatMetadata class for format.
82     */
83    public function getCommonMetaArray( File $file ) {
84        $metadata = $file->getMetadataArray();
85        if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['streams'] ) ) {
86            return [];
87        }
88
89        // See http://www.xiph.org/vorbis/doc/v-comment.html
90        // http://age.hobba.nl/audio/mirroredpages/ogg-tagging.html
91        $metadataMap = [
92            'title' => 'ObjectName',
93            'artist' => 'Artist',
94            'performer' => 'Artist',
95            'description' => 'ImageDescription',
96            'license' => 'UsageTerms',
97            'copyright' => 'Copyright',
98            'organization' => 'dc-publisher',
99            'date' => 'DateTimeDigitized',
100            'location' => 'LocationDest',
101            'contact' => 'Contact',
102            'encoded_using' => 'Software',
103            'encoder' => 'Software',
104            // OpenSubtitles.org hash. Identifies source video.
105            'source_ohash' => 'OriginalDocumentID',
106            'comment' => 'UserComment',
107            'language' => 'LanguageCode',
108        ];
109
110        $props = [];
111
112        foreach ( $metadata['streams'] as $stream ) {
113            if ( isset( $stream['vendor'] ) ) {
114                if ( !isset( $props['Software'] ) ) {
115                    $props['Software'] = [];
116                }
117                $props['Software'][] = trim( $stream['vendor'] );
118            }
119            if ( !isset( $stream['comments'] ) ) {
120                continue;
121            }
122            foreach ( $stream['comments'] as $name => $rawValue ) {
123                // $value will be an array if the file has
124                // a multiple tags with the same name. Otherwise it
125                // is a string.
126                foreach ( (array)$rawValue as $value ) {
127                    $trimmedValue = trim( $value );
128                    if ( $trimmedValue === '' ) {
129                        continue;
130                    }
131                    $lowerName = strtolower( $name );
132                    if ( isset( $metadataMap[$lowerName] ) ) {
133                        $convertedName = $metadataMap[$lowerName];
134                        if ( !isset( $props[$convertedName] ) ) {
135                            $props[$convertedName] = [];
136                        }
137                        $props[$convertedName][] = $trimmedValue;
138                    }
139                }
140            }
141
142        }
143        // properties might be duplicated across streams
144        foreach ( $props as &$type ) {
145            $type = array_unique( $type );
146            $type = array_values( $type );
147        }
148
149        return $props;
150    }
151
152    /**
153     * Get the "media size"
154     *
155     * @param File $file
156     * @param string $path
157     * @param false|string|array $metadata
158     * @return array|false
159     */
160    public function getImageSize( $file, $path, $metadata = false ) {
161        // Just return the size of the first video stream
162        if ( $metadata === false ) {
163            $metadata = $file->getMetadata();
164        }
165
166        if ( is_string( $metadata ) ) {
167            $metadata = $this->unpackMetadata( $metadata );
168        }
169
170        if ( isset( $metadata['error'] ) || !isset( $metadata['streams'] ) ) {
171            return false;
172        }
173        $config = MediaWikiServices::getInstance()->getMainConfig();
174        $mediaVideoTypes = $config->get( 'MediaVideoTypes' );
175        foreach ( $metadata['streams'] as $stream ) {
176            if ( in_array( $stream['type'], $mediaVideoTypes, true ) ) {
177                $pictureWidth = $stream['header']['PICW'];
178                $parNumerator = $stream['header']['PARN'];
179                $parDenominator = $stream['header']['PARD'];
180                if ( $parNumerator && $parDenominator ) {
181                    // Compensate for non-square pixel aspect ratios
182                    $pictureWidth = $pictureWidth * $parNumerator / $parDenominator;
183                }
184                return [
185                    (int)$pictureWidth,
186                    (int)$stream['header']['PICH']
187                ];
188            }
189        }
190        return [ false, false ];
191    }
192
193    /**
194     * @param string|array $metadata
195     * @param bool $unserialize
196     * @return false|mixed
197     */
198    public function unpackMetadata( $metadata, $unserialize = true ) {
199        if ( $unserialize ) {
200            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
201            $metadata = @unserialize( $metadata );
202        }
203
204        if ( isset( $metadata['version'] ) && $metadata['version'] === self::METADATA_VERSION ) {
205            return $metadata;
206        }
207
208        return false;
209    }
210
211    /**
212     * @param File $image
213     * @return string
214     */
215    public function getMetadataType( $image ) {
216        return 'ogg';
217    }
218
219    /**
220     * @param File $file
221     * @return string
222     */
223    public function getWebType( $file ) {
224        $baseType = $this->isAudio( $file ) ? 'audio' : 'video';
225        $baseType .= '/ogg';
226        $streamTypes = $this->getStreamTypes( $file );
227        if ( !$streamTypes ) {
228            return $baseType;
229        }
230        $codecs = strtolower( implode( ", ", $streamTypes ) );
231        return $baseType . '; codecs="' . $codecs . '"';
232    }
233
234    /**
235     * @param File $file
236     * @return string[]|false
237     */
238    public function getStreamTypes( $file ) {
239        $streamTypes = [];
240        $metadata = $file->getMetadataArray();
241        foreach ( $metadata['streams'] ?? [] as $stream ) {
242            $streamTypes[] = $stream['type'];
243        }
244        return array_unique( $streamTypes );
245    }
246
247    /**
248     * @param File $file
249     * @return float
250     */
251    public function getOffset( $file ) {
252        $metadata = $file->getMetadataArray();
253        return (float)( $metadata['offset'] ?? 0.0 );
254    }
255
256    /**
257     * @param File $file
258     * @return float
259     */
260    public function getLength( $file ) {
261        $metadata = $file->getMetadataArray();
262        return (float)( $metadata['length'] ?? 0.0 );
263    }
264
265    /**
266     * Get useful response headers for GET/HEAD requests for a file with the given metadata
267     * @param array $metadata Contains this handler's unserialized getMetadata() for a file
268     * @return array
269     * @since 1.30
270     */
271    public function getContentHeaders( $metadata ) {
272        $result = [];
273
274        if ( $metadata && !isset( $metadata['error'] ) && isset( $metadata['length'] ) ) {
275            $result = [ 'X-Content-Duration' => (float)$metadata['length'] ];
276        }
277
278        return $result;
279    }
280
281    private function findStream( File $file, array $types ): ?array {
282        $metadata = $file->getMetadataArray();
283        foreach ( $metadata['streams'] ?? [] as $stream ) {
284            if ( in_array( $stream['type'] ?? [], $types ) ) {
285                return $stream;
286            }
287        }
288        return null;
289    }
290
291    private function findVideoStream( File $file ): ?array {
292        $config = MediaWikiServices::getInstance()->getMainConfig();
293        $mediaVideoTypes = $config->get( 'MediaVideoTypes' );
294        return $this->findStream( $file, $mediaVideoTypes );
295    }
296
297    private function findAudioStream( File $file ): ?array {
298        $config = MediaWikiServices::getInstance()->getMainConfig();
299        $mediaAudioTypes = $config->get( 'MediaAudioTypes' );
300        return $this->findStream( $file, $mediaAudioTypes );
301    }
302
303    /**
304     * @param File $file
305     * @return float
306     */
307    public function getFramerate( $file ) {
308        $stream = $this->findVideoStream( $file );
309        if ( $stream ) {
310            return $stream['header']['FRN'] / $stream['header']['FRD'];
311        }
312        return 0.0;
313    }
314
315    /**
316     * @param File $file
317     * @return bool
318     */
319    public function hasVideo( $file ) {
320        $stream = $this->findVideoStream( $file );
321        return $stream !== null;
322    }
323
324    /**
325     * @param File $file
326     * @return bool
327     */
328    public function hasAudio( $file ) {
329        $stream = $this->findAudioStream( $file );
330        return $stream !== null;
331    }
332
333    /**
334     * @param File $file
335     * @return int
336     */
337    public function getAudioChannels( $file ) {
338        $stream = $this->findAudioStream( $file );
339        $header = $stream['header'] ?? null;
340        if ( isset( $header['vorbis_version'] ) ) {
341            return (int)$header['audio_channels'];
342        } elseif ( isset( $header['opus_version'] ) ) {
343            return (int)$header['nb_channels'];
344        } else {
345            return 0;
346        }
347    }
348
349    /**
350     * @param File $file
351     * @return string
352     */
353    public function getShortDesc( $file ) {
354        global $wgLang;
355
356        $streamTypes = $this->getStreamTypes( $file );
357        if ( !$streamTypes ) {
358            return parent::getShortDesc( $file );
359        }
360        $config = MediaWikiServices::getInstance()->getMainConfig();
361        $mediaVideoTypes = $config->get( 'MediaVideoTypes' );
362        $mediaAudioTypes = $config->get( 'MediaAudioTypes' );
363        if ( array_intersect( $streamTypes, $mediaVideoTypes ) ) {
364            // Count multiplexed audio/video as video for short descriptions
365            $msg = 'timedmedia-ogg-short-video';
366        } elseif ( array_intersect( $streamTypes, $mediaAudioTypes ) ) {
367            $msg = 'timedmedia-ogg-short-audio';
368        } else {
369            $msg = 'timedmedia-ogg-short-general';
370        }
371        return wfMessage( $msg, implode( '/', $streamTypes ),
372            $wgLang->formatTimePeriod( $this->getLength( $file ) ) )->text();
373    }
374
375    /**
376     * @param File $file
377     * @return string
378     */
379    public function getLongDesc( $file ) {
380        $streamTypes = $this->getStreamTypes( $file );
381        if ( !$streamTypes ) {
382            $unpacked = $this->unpackMetadata( $file->getMetadata() );
383            if ( isset( $unpacked['error']['message'] ) ) {
384                return wfMessage( 'timedmedia-ogg-long-error', $unpacked['error']['message'] )
385                    ->sizeParams( $file->getSize() )
386                    ->text();
387            }
388            return wfMessage( 'timedmedia-ogg-long-no-streams' )
389                ->sizeParams( $file->getSize() )
390                ->text();
391        }
392        $config = MediaWikiServices::getInstance()->getMainConfig();
393        $mediaVideoTypes = $config->get( 'MediaVideoTypes' );
394        $mediaAudioTypes = $config->get( 'MediaAudioTypes' );
395        if ( array_intersect( $streamTypes, $mediaVideoTypes ) ) {
396            if ( array_intersect( $streamTypes, $mediaAudioTypes ) ) {
397                $msg = 'timedmedia-ogg-long-multiplexed';
398            } else {
399                $msg = 'timedmedia-ogg-long-video';
400            }
401        } elseif ( array_intersect( $streamTypes, $mediaAudioTypes ) ) {
402            $msg = 'timedmedia-ogg-long-audio';
403        } else {
404            $msg = 'timedmedia-ogg-long-general';
405        }
406        $size = 0;
407        $unpacked = $this->unpackMetadata( $file->getMetadata() );
408        if ( !$unpacked || isset( $unpacked['error'] ) ) {
409            $length = 0;
410        } else {
411            $length = $this->getLength( $file );
412            foreach ( $unpacked['streams'] as $stream ) {
413                if ( isset( $stream['size'] ) ) {
414                    $size += $stream['size'];
415                }
416            }
417        }
418        return wfMessage(
419            $msg,
420            implode( '/', $streamTypes )
421            )->timeperiodParams(
422                $length
423            )->bitrateParams(
424                $this->getBitRate( $file )
425            )->numParams(
426                $file->getWidth(),
427                $file->getHeight()
428            )->sizeParams(
429                $file->getSize()
430            )->text();
431    }
432
433    /**
434     * @param File $file
435     * @return float|int
436     */
437    public function getBitRate( $file ) {
438        $size = 0;
439        $unpacked = $this->unpackMetadata( $file->getMetadata() );
440        if ( !$unpacked || isset( $unpacked['error'] ) ) {
441            $length = 0;
442        } else {
443            $length = $this->getLength( $file );
444            if ( isset( $unpacked['streams'] ) ) {
445                foreach ( $unpacked['streams'] as $stream ) {
446                    if ( isset( $stream['size'] ) ) {
447                        $size += $stream['size'];
448                    }
449                }
450            }
451        }
452        return $length ? $size / $length * 8 : 0;
453    }
454}