Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.22% covered (warning)
66.22%
98 / 148
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebMHandler
66.22% covered (warning)
66.22%
98 / 148
0.00% covered (danger)
0.00%
0 / 9
151.29
0.00% covered (danger)
0.00%
0 / 1
 getID3
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 getImageSize
43.33% covered (danger)
43.33%
13 / 30
0.00% covered (danger)
0.00%
0 / 1
62.58
 getMetadataType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWebType
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 getStreamTypes
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
12.57
 getShortDesc
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getLongDesc
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 formatMetadata
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCommonMetaArray
96.55% covered (success)
96.55%
56 / 58
0.00% covered (danger)
0.00%
0 / 1
9
1<?php
2
3namespace MediaWiki\TimedMediaHandler\Handlers\WebMHandler;
4
5use File;
6use IContextSource;
7use MediaWiki\TimedMediaHandler\Handlers\ID3Handler\ID3Handler;
8
9/**
10 * WebM handler
11 */
12class WebMHandler extends ID3Handler {
13
14    /**
15     * @param string $path
16     * @return array
17     */
18    protected function getID3( $path ) {
19        $id3 = parent::getID3( $path );
20        // Unset some parts of id3 that are too detailed and matroska specific:
21        // @todo include the basic file codec and other metadata too?
22        if ( isset( $id3['matroska'] ) ) {
23            if ( isset( $id3['matroska']['comments'] ) ) {
24                $comments = $id3['matroska']['comments'];
25                $id3['matroska'] = [
26                    'comments' => $comments
27                ];
28            } else {
29                unset( $id3['matroska'] );
30            }
31        }
32        return $id3;
33    }
34
35    /**
36     * Get the "media size"
37     * @param File $file
38     * @param string $path
39     * @param false|string|array $metadata
40     * @return array|false
41     */
42    public function getImageSize( $file, $path, $metadata = false ) {
43        // Just return the size of the first video stream
44        if ( $metadata === false ) {
45            $metadata = $file->getMetadata();
46        }
47
48        if ( is_string( $metadata ) ) {
49            $metadata = $this->unpackMetadata( $metadata );
50        }
51
52        if ( isset( $metadata['error'] ) ) {
53            return false;
54        }
55
56        $size = [ false, false ];
57        // display_x/display_y is only set if DisplayUnit
58        // is pixels, otherwise display_aspect_ratio is set
59        if ( isset( $metadata['video']['display_x'] ) && isset( $metadata['video']['display_y'] ) ) {
60            $size = [
61                $metadata['video']['display_x'],
62                $metadata['video']['display_y']
63            ];
64        } elseif ( isset( $metadata['video']['resolution_x'] ) && isset( $metadata['video']['resolution_y'] ) ) {
65            $size = [
66                $metadata['video']['resolution_x'],
67                $metadata['video']['resolution_y']
68            ];
69            if ( isset( $metadata['video']['crop_top'] ) ) {
70                $size[1] -= $metadata['video']['crop_top'];
71            }
72            if ( isset( $metadata['video']['crop_bottom'] ) ) {
73                $size[1] -= $metadata['video']['crop_bottom'];
74            }
75            if ( isset( $metadata['video']['crop_left'] ) ) {
76                $size[0] -= $metadata['video']['crop_left'];
77            }
78            if ( isset( $metadata['video']['crop_right'] ) ) {
79                $size[0] -= $metadata['video']['crop_right'];
80            }
81        }
82        if ( $size[0] && $size[1] && isset( $metadata['video']['display_aspect_ratio'] ) ) {
83            // for wide images (i.e. 16:9) take native height as base
84            if ( $metadata['video']['display_aspect_ratio'] >= 1 ) {
85                $size[0] = (int)( $size[1] * $metadata['video']['display_aspect_ratio'] );
86            } else {
87                // for tall images (i.e. 9:16) take width as base
88                $size[1] = (int)( $size[0] / $metadata['video']['display_aspect_ratio'] );
89            }
90        }
91        return $size;
92    }
93
94    /**
95     * @param File $file
96     * @return string
97     */
98    public function getMetadataType( $file ) {
99        return 'webm';
100    }
101
102    /**
103     * @param File $file
104     * @return string
105     */
106    public function getWebType( $file ) {
107        $baseType = ( !$file->getWidth() && !$file->getHeight() ) ? 'audio' : 'video';
108
109        $streams = $this->getStreamTypes( $file );
110        if ( !$streams ) {
111            return $baseType . '/webm';
112        }
113
114        $codecs = strtolower( implode( ', ', $streams ) );
115
116        return $baseType . '/webm; codecs="' . $codecs . '"';
117    }
118
119    /**
120     * @param File $file
121     * @return string[]|false
122     */
123    public function getStreamTypes( $file ) {
124        $streamTypes = [];
125        $metadata = $this->unpackMetadata( $file->getMetadata() );
126        if ( !$metadata || isset( $metadata['error'] ) ) {
127            return false;
128        }
129        // id3 gives 'V_VP8' for what we call VP8
130        if ( isset( $metadata['video'] ) ) {
131            if ( $metadata['video']['dataformat'] === 'vp8' ) {
132                $streamTypes[] = 'VP8';
133            } elseif ( $metadata['video']['dataformat'] === 'vp9'
134                || $metadata['video']['dataformat'] === 'V_VP9'
135            ) {
136                // Currently getID3 calls it V_VP9. That will probably change to vp9
137                // once getID3 actually gets support for the codec.
138                $streamTypes[] = 'VP9';
139            } elseif ( $metadata['video']['dataformat'] === 'V_AV1' ) {
140                $streamTypes[] = 'AV1';
141            }
142        }
143        if ( isset( $metadata['audio'] ) ) {
144            if ( $metadata['audio']['dataformat'] === 'vorbis' ) {
145                $streamTypes[] = 'Vorbis';
146            } elseif ( $metadata['audio']['dataformat'] === 'opus'
147                || $metadata['audio']['dataformat'] === 'A_OPUS'
148            ) {
149                // Currently getID3 calls it A_OPUS. That will probably change to 'opus'
150                // once getID3 actually gets support for the codec.
151                $streamTypes[] = 'Opus';
152            }
153        }
154
155        return $streamTypes;
156    }
157
158    /**
159     * @param File $file
160     * @return string
161     */
162    public function getShortDesc( $file ) {
163        global $wgLang;
164
165        $streamTypes = $this->getStreamTypes( $file );
166        if ( !$streamTypes ) {
167            return parent::getShortDesc( $file );
168        }
169        return wfMessage( 'timedmedia-webm-short-video', implode( '/', $streamTypes ),
170            $wgLang->formatTimePeriod( $this->getLength( $file ) ) )->text();
171    }
172
173    /**
174     * @param File $file
175     * @return string
176     */
177    public function getLongDesc( $file ) {
178        $streamTypes = $this->getStreamTypes( $file );
179        if ( !$streamTypes ) {
180            return parent::getLongDesc( $file );
181        }
182        return wfMessage(
183            'timedmedia-webm-long-video',
184            implode( '/', $streamTypes )
185            )->timeperiodParams(
186                $this->getLength( $file )
187            )->bitrateParams(
188                $this->getBitRate( $file )
189            )->numParams(
190                $file->getWidth(),
191                $file->getHeight()
192            )->sizeParams(
193                $file->getSize()
194            )->text();
195    }
196
197    /**
198     * Display metadata box on file description page.
199     *
200     * @param File $file
201     * @param false|IContextSource $context Context to use (optional)
202     * @return array|false
203     */
204    public function formatMetadata( $file, $context = false ) {
205        $meta = $this->getCommonMetaArray( $file );
206        if ( !$meta ) {
207            return false;
208        }
209
210        return $this->formatMetadataHelper( $meta, $context );
211    }
212
213    /**
214     * Get file metadata as an array
215     *
216     * Reuse MediaWiki's support for image metadata so
217     * translate WebM metadata keywords to equivalent exif values
218     * @see http://wiki.webmproject.org/webm-metadata/global-metadata
219     *
220     * @param File $file
221     * @return array|false
222     */
223    public function getCommonMetaArray( File $file ) {
224        $metadata = $file->getMetadata();
225
226        if ( is_string( $metadata ) ) {
227            $metadata = $this->unpackMetadata( $metadata );
228        }
229
230        if ( isset( $metadata['error'] ) ) {
231            return false;
232        }
233
234        if ( !$metadata ) {
235            return false;
236        }
237
238        $props = [];
239
240        if ( isset( $metadata['matroska']['comments'] ) ) {
241            $comments = $metadata['matroska']['comments'];
242            // Map comments from getid3's matroska handler to output format
243            // Localization of labels by FormatMetadata...
244            // Based on http://wiki.webmproject.org/webm-metadata/global-metadata
245            // and https://matroska.org/technical/tagging.html
246            // matched against descriptions in language/i18n/exif/qqq.json
247            $map = [
248                'muxingapp' => 'Software',
249                'writingapp' => 'Software',
250                'title' => 'ObjectName',
251                'summary' => 'ImageDescription',
252                'synopsis' => 'ImageDescription',
253                'artist' => 'Artist',
254                'publisher' => 'dc-publisher',
255                'genre' => 'dc-type',
256                'content_type' => 'dc-type',
257                'keywords' => 'Keywords',
258                'law_rating' => 'ContentWarning',
259                'date_released' => 'DateTimeReleased',
260                'date_recorded' => 'DateTimeOriginal',
261                'date_encoded' => 'DateTimeDigitized',
262                'date_digitized' => 'DateTimeDigitized',
263                'date_tagged' => 'DateTimeMetadata',
264                'date_purchased' => 'dc-date',
265                'date_written' => 'dc-date',
266                'comment' => 'UserComment',
267                'rating' => 'Rating',
268                'encoder' => 'Software',
269                'copyright' => 'Copyright',
270                'production_copyright' => 'Copyright',
271                'license' => 'UsageTerms',
272                'terms_of_use' => 'UsageTerms',
273                'catalog_number' => 'Identifier',
274                'url' => 'Identifier',
275                'isrc' => 'Identifier',
276                'isbn' => 'Identifier',
277                'barcode' => 'Identifier',
278                'lccn' => 'Identifier',
279                'imdb' => 'Identifier',
280                'tmdb' => 'Identifier',
281                'tvdb' => 'Identifier',
282                'tvdb2' => 'Identifier',
283                // Not official but seen in files
284                'language' => 'LanguageCode',
285                'webstatement' => 'WebStatement',
286                'licenseurl' => 'LicenseUrl'
287            ];
288            foreach ( $map as $commentTag => $propTag ) {
289                if ( isset( $comments[$commentTag] ) ) {
290                    if ( !isset( $props[$propTag] ) ) {
291                        $props[$propTag] = [];
292                    }
293                    $props[$propTag] = array_merge( (array)$comments[$commentTag], $props[$propTag] );
294                }
295            }
296            foreach ( $props as &$tag ) {
297                // We put multiple similar tags
298                // under same name, which sometimes
299                // results in duplication.
300                $tag = array_unique( $tag );
301            }
302        }
303
304        return $props;
305    }
306
307}