Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.02% |
14 / 464 |
|
8.11% |
3 / 37 |
CRAP | |
0.00% |
0 / 1 |
WebVideoTranscode | |
3.02% |
14 / 464 |
|
8.11% |
3 / 37 |
16759.60 | |
0.00% |
0 / 1 |
getDerivativeFilePath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTranscodeFileBaseName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getTranscodedUrlForFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTargetEncodeFile | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getMaxSizeWebStream | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getProjectedFileSize | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getSources | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
getRemoteSources | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
getLocalSources | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
132 | |||
isTranscodeKeyPlayable | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isTranscodeReady | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
clearTranscodeCache | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTranscodeState | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
removeTranscodes | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
156 | |||
invalidatePagesWithFile | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
addSourceIfReady | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getPrimarySourceAttributes | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getDerivativeSourceAttributes | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
startJobQueue | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
updateStreamingManifests | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
cleanupTranscodes | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
isTranscodeEnabled | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
updateJobQueue | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
42 | |||
isTranscodePrioritized | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getQueueSize | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getMaxSizeTransform | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
isTargetLargerThanFile | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
isSmallestTranscodeForCodec | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
getMaxSize | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
filterAndSort | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
enabledTranscodes | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
enabledVideoTranscodes | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
enabledAudioTranscodes | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
validateTranscodeConfiguration | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
isBaseMediaFormat | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
expandRate | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
cleanupOrphanedTranscodes | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * WebVideoTranscode provides: |
4 | * encode keys |
5 | * encode settings |
6 | * |
7 | * extends api to return all the streams |
8 | * extends video tag output to provide all the available sources |
9 | */ |
10 | |
11 | namespace MediaWiki\TimedMediaHandler\WebVideoTranscode; |
12 | |
13 | use Exception; |
14 | use File; |
15 | use HTMLCacheUpdateJob; |
16 | use IForeignRepoWithDB; |
17 | use IForeignRepoWithMWApi; |
18 | use JobSpecification; |
19 | use LogicException; |
20 | use MediaWiki\Config\ConfigException; |
21 | use MediaWiki\Deferred\CdnCacheUpdate; |
22 | use MediaWiki\Deferred\DeferredUpdates; |
23 | use MediaWiki\FileBackend\FSFile\TempFSFileFactory; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\TimedMediaHandler\Handlers\FLACHandler\FLACHandler; |
27 | use MediaWiki\TimedMediaHandler\Handlers\ID3Handler\ID3Handler; |
28 | use MediaWiki\TimedMediaHandler\Handlers\MIDIHandler\MIDIHandler; |
29 | use MediaWiki\TimedMediaHandler\Handlers\MP3Handler\MP3Handler; |
30 | use MediaWiki\TimedMediaHandler\Handlers\MP4Handler\MP4Handler; |
31 | use MediaWiki\TimedMediaHandler\Handlers\OggHandler\OggHandler; |
32 | use MediaWiki\TimedMediaHandler\Handlers\WAVHandler\WAVHandler; |
33 | use MediaWiki\TimedMediaHandler\HLS\Multivariant; |
34 | use MediaWiki\Title\Title; |
35 | use TempFSFile; |
36 | use Wikimedia\Rdbms\IReadableDatabase; |
37 | |
38 | /** |
39 | * Main WebVideoTranscode Class hold some constants and config values |
40 | */ |
41 | class WebVideoTranscode { |
42 | /** @var array[] Static cache of transcode state per instantiation */ |
43 | public static $transcodeState = []; |
44 | |
45 | /** |
46 | * Encoding parameters are set via firefogg encode api |
47 | * |
48 | * For clarity and compatibility with passing down |
49 | * client side encode settings at point of upload |
50 | * |
51 | * http://firefogg.org/dev/index.html |
52 | * @var string[][] |
53 | */ |
54 | public static $derivativeSettings = [ |
55 | |
56 | // WebM VP8/Vorbis transcodes |
57 | // |
58 | // Two-pass encoding is a bit slower, but *massively* improves bitrate control. |
59 | // Trading off speed using the '-speed 3' parameter on the second pass. |
60 | // |
61 | // The current defaults do not include VP8 output, but it may be helpful |
62 | // at a limited resolution range for certain back-compatibility scenarios. |
63 | '160p.webm' => [ |
64 | 'maxSize' => '288x160', |
65 | 'videoBitrate' => '193k', |
66 | 'minrate' => '96k', |
67 | 'maxrate' => '280k', |
68 | 'crf' => '37', |
69 | 'speed' => '3', |
70 | 'twopass' => 'true', |
71 | 'videoCodec' => 'vp8', |
72 | 'audioCodec' => 'vorbis', |
73 | 'samplerate' => '48000', |
74 | 'channels' => '2', |
75 | 'audioBitrate' => '128k', |
76 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
77 | ], |
78 | '240p.webm' => [ |
79 | 'maxSize' => '426x240', |
80 | 'videoBitrate' => '385k', |
81 | 'minrate' => '193k', |
82 | 'maxrate' => '558k', |
83 | 'crf' => '37', |
84 | 'speed' => '3', |
85 | 'twopass' => 'true', |
86 | 'videoCodec' => 'vp8', |
87 | 'audioCodec' => 'vorbis', |
88 | 'samplerate' => '48000', |
89 | 'channels' => '2', |
90 | 'audioBitrate' => '128k', |
91 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
92 | ], |
93 | '360p.webm' => [ |
94 | 'maxSize' => '640x360', |
95 | 'videoBitrate' => '767k', |
96 | 'minrate' => '383k', |
97 | 'maxrate' => '1112k', |
98 | 'crf' => '36', |
99 | 'speed' => '3', |
100 | 'slices' => '2', |
101 | 'twopass' => 'true', |
102 | 'videoCodec' => 'vp8', |
103 | 'audioCodec' => 'vorbis', |
104 | 'samplerate' => '48000', |
105 | 'channels' => '2', |
106 | 'audioBitrate' => '128k', |
107 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
108 | ], |
109 | '480p.webm' => [ |
110 | 'maxSize' => '854x480', |
111 | 'videoBitrate' => '1250k', |
112 | 'minrate' => '625k', |
113 | 'maxrate' => '1813k', |
114 | 'crf' => '33', |
115 | 'speed' => '3', |
116 | 'slices' => '2', |
117 | 'twopass' => 'true', |
118 | 'videoCodec' => 'vp8', |
119 | 'audioCodec' => 'vorbis', |
120 | 'samplerate' => '48000', |
121 | 'channels' => '2', |
122 | 'audioBitrate' => '128k', |
123 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
124 | ], |
125 | '720p.webm' => [ |
126 | 'maxSize' => '1280x720', |
127 | 'videoBitrate' => '2491k', |
128 | 'minrate' => '1246k', |
129 | 'maxrate' => '3612k', |
130 | 'crf' => '32', |
131 | 'speed' => '3', |
132 | 'slices' => '4', |
133 | 'twopass' => 'true', |
134 | 'videoCodec' => 'vp8', |
135 | 'audioCodec' => 'vorbis', |
136 | 'samplerate' => '48000', |
137 | 'channels' => '2', |
138 | 'audioBitrate' => '128k', |
139 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
140 | ], |
141 | '1080p.webm' => [ |
142 | 'maxSize' => '1920x1080', |
143 | 'videoBitrate' => '4963k', |
144 | 'minrate' => '2482k', |
145 | 'maxrate' => '7197k', |
146 | 'crf' => '31', |
147 | 'speed' => '3', |
148 | 'slices' => '4', |
149 | 'twopass' => 'true', |
150 | 'videoCodec' => 'vp8', |
151 | 'audioCodec' => 'vorbis', |
152 | 'samplerate' => '48000', |
153 | 'channels' => '2', |
154 | 'audioBitrate' => '128k', |
155 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
156 | ], |
157 | '1440p.webm' => [ |
158 | 'maxSize' => '2560x1440', |
159 | 'videoBitrate' => '8094k', |
160 | 'minrate' => '4047k', |
161 | 'maxrate' => '11736k', |
162 | 'crf' => '24', |
163 | 'speed' => '2', |
164 | 'slices' => '8', |
165 | 'twopass' => 'true', |
166 | 'videoCodec' => 'vp8', |
167 | 'audioCodec' => 'vorbis', |
168 | 'samplerate' => '48000', |
169 | 'channels' => '2', |
170 | 'audioBitrate' => '128k', |
171 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
172 | ], |
173 | '2160p.webm' => [ |
174 | 'maxSize' => '3840x2160', |
175 | 'videoBitrate' => '16126k', |
176 | 'minrate' => '8063k', |
177 | 'maxrate' => '23382k', |
178 | 'crf' => '15', |
179 | 'speed' => '2', |
180 | 'slices' => '8', |
181 | 'twopass' => 'true', |
182 | 'videoCodec' => 'vp8', |
183 | 'audioCodec' => 'vorbis', |
184 | 'samplerate' => '48000', |
185 | 'channels' => '2', |
186 | 'audioBitrate' => '128k', |
187 | 'type' => 'video/webm; codecs="vp8, vorbis"', |
188 | ], |
189 | |
190 | // WebM VP9 transcode: |
191 | // |
192 | // Two-pass encoding is a bit slower, but *massively* improves bitrate control. |
193 | // Trading off speed using the '-speed 3' parameter on the second pass. |
194 | // |
195 | // Encoding speed is greatly affected by threading settings; HD videos can use up to |
196 | // 8 threads with a suitable ffmpeg/libvpx and $wgFFmpegVP9RowMT enabled ("row-mt"). |
197 | // Ultra-HD can use up to 16 threads. Be sure to set $wgFFmpegThreads to a suitable |
198 | // maximum values! |
199 | // |
200 | '120p.vp9.webm' => [ |
201 | 'maxSize' => '213x120', |
202 | 'videoBitrate' => '95k', |
203 | 'minrate' => '47k', |
204 | 'maxrate' => '137k', |
205 | 'crf' => '37', |
206 | 'speed' => '3', |
207 | 'videoCodec' => 'vp9', |
208 | 'twopass' => 'true', |
209 | 'audioCodec' => 'opus', |
210 | 'audioBitrate' => '96k', |
211 | 'type' => 'video/webm; codecs="vp9, opus"', |
212 | ], |
213 | '180p.vp9.webm' => [ |
214 | 'maxSize' => '320x180', |
215 | 'videoBitrate' => '189k', |
216 | 'minrate' => '94k', |
217 | 'maxrate' => '274k', |
218 | 'crf' => '37', |
219 | 'speed' => '3', |
220 | 'twopass' => 'true', |
221 | 'videoCodec' => 'vp9', |
222 | 'audioCodec' => 'opus', |
223 | 'audioBitrate' => '96k', |
224 | 'type' => 'video/webm; codecs="vp9, opus"', |
225 | ], |
226 | '240p.vp9.webm' => [ |
227 | 'maxSize' => '426x240', |
228 | 'videoBitrate' => '308k', |
229 | 'minrate' => '154k', |
230 | 'maxrate' => '447k', |
231 | 'crf' => '37', |
232 | 'speed' => '3', |
233 | 'twopass' => 'true', |
234 | 'videoCodec' => 'vp9', |
235 | 'audioCodec' => 'opus', |
236 | 'audioBitrate' => '96k', |
237 | 'type' => 'video/webm; codecs="vp9, opus"', |
238 | ], |
239 | '360p.vp9.webm' => [ |
240 | 'maxSize' => '640x360', |
241 | 'videoBitrate' => '613k', |
242 | 'minrate' => '307k', |
243 | 'maxrate' => '889k', |
244 | 'crf' => '36', |
245 | 'speed' => '3', |
246 | 'tileColumns' => '1', |
247 | 'twopass' => 'true', |
248 | 'videoCodec' => 'vp9', |
249 | 'audioCodec' => 'opus', |
250 | 'audioBitrate' => '96k', |
251 | 'type' => 'video/webm; codecs="vp9, opus"', |
252 | ], |
253 | '480p.vp9.webm' => [ |
254 | 'maxSize' => '854x480', |
255 | 'videoBitrate' => '1000k', |
256 | 'minrate' => '500k', |
257 | 'maxrate' => '1450k', |
258 | 'crf' => '33', |
259 | 'speed' => '3', |
260 | 'tileColumns' => '1', |
261 | 'twopass' => 'true', |
262 | 'videoCodec' => 'vp9', |
263 | 'audioCodec' => 'opus', |
264 | 'audioBitrate' => '96k', |
265 | 'type' => 'video/webm; codecs="vp9, opus"', |
266 | ], |
267 | '720p.vp9.webm' => [ |
268 | 'maxSize' => '1280x720', |
269 | 'videoBitrate' => '1993k', |
270 | 'minrate' => '996k', |
271 | 'maxrate' => '2890k', |
272 | 'crf' => '32', |
273 | 'speed' => '3', |
274 | 'tileColumns' => '2', |
275 | 'twopass' => 'true', |
276 | 'videoCodec' => 'vp9', |
277 | 'audioCodec' => 'opus', |
278 | 'audioBitrate' => '96k', |
279 | 'type' => 'video/webm; codecs="vp9, opus"', |
280 | ], |
281 | '1080p.vp9.webm' => [ |
282 | 'maxSize' => '1920x1080', |
283 | 'videoBitrate' => '3971k', |
284 | 'minrate' => '1985k', |
285 | 'maxrate' => '5757k', |
286 | 'crf' => '31', |
287 | 'speed' => '3', |
288 | 'tileColumns' => '2', |
289 | 'twopass' => 'true', |
290 | 'videoCodec' => 'vp9', |
291 | 'audioCodec' => 'opus', |
292 | 'audioBitrate' => '96k', |
293 | 'type' => 'video/webm; codecs="vp9, opus"', |
294 | ], |
295 | '1440p.vp9.webm' => [ |
296 | 'maxSize' => '2560x1440', |
297 | 'videoBitrate' => '6475k', |
298 | 'minrate' => '3238k', |
299 | 'maxrate' => '9389k', |
300 | 'crf' => '24', |
301 | 'speed' => '3', |
302 | 'tileColumns' => '3', |
303 | 'twopass' => 'true', |
304 | 'videoCodec' => 'vp9', |
305 | 'audioCodec' => 'opus', |
306 | 'audioBitrate' => '96k', |
307 | 'type' => 'video/webm; codecs="vp9, opus"', |
308 | ], |
309 | '2160p.vp9.webm' => [ |
310 | 'maxSize' => '3840x2160', |
311 | 'videoBitrate' => '12900k', |
312 | 'minrate' => '6450k', |
313 | 'maxrate' => '18706k', |
314 | 'crf' => '15', |
315 | 'speed' => '3', |
316 | 'tileColumns' => '3', |
317 | 'twopass' => 'true', |
318 | 'videoCodec' => 'vp9', |
319 | 'audioCodec' => 'opus', |
320 | 'audioBitrate' => '96k', |
321 | 'type' => 'video/webm; codecs="vp9, opus"', |
322 | ], |
323 | |
324 | // Adaptive streaming transcodes: |
325 | // * stereo.audio.mp3 audio (for Safari 16 and below) |
326 | // * stereo.audio.opus.mp4 audio (for Chromium, Firefox, Safari 17) |
327 | // * surround.audio.opus.mp4 audio (reserved for future expansion) |
328 | // * 144p.video.mjpeg.mov fallback video for old iOS (optional) |
329 | // * 180p .. 480p.video.mpeg4.mp4 fallback video for old iOS (optional) |
330 | // * 240p .. 2160p.video.vp9.mp4 video |
331 | // * .m3u8 playlists |
332 | // |
333 | 'stereo.audio.mp3' => [ |
334 | 'novideo' => 'true', |
335 | 'audioCodec' => 'mp3', |
336 | 'samplerate' => '48000', |
337 | 'channels' => '2', |
338 | 'audioBitrate' => '128k', |
339 | 'type' => 'audio/mpeg', |
340 | 'streaming' => 'hls', |
341 | ], |
342 | 'stereo.audio.opus.mp4' => [ |
343 | 'novideo' => 'true', |
344 | 'audioCodec' => 'opus', |
345 | 'samplerate' => '48000', |
346 | 'channels' => '2', |
347 | 'audioBitrate' => '96k', |
348 | 'type' => 'audio/mp4; codecs="opus"', |
349 | 'streaming' => 'hls', |
350 | ], |
351 | /* |
352 | // @todo implement surround support for input |
353 | // with >2 channels. note safari doesn't grok |
354 | // opus surround. |
355 | 'surround.audio.opus.mp4' => [ |
356 | 'novideo' => true, |
357 | 'audioCodec' => 'opus', |
358 | 'samplerate' => '48000', |
359 | 'minChannels' => 3, |
360 | 'audioBitrate' => '256k', |
361 | 'type' => 'audio/mp4; codecs="opus"', |
362 | 'streaming' => 'hls', |
363 | ], |
364 | */ |
365 | |
366 | // Optional back-compat |
367 | // Streaming Motion-JPEG track |
368 | // |
369 | // These are video-only, in fragmented .mov that allows adaptive streaming |
370 | // with chunks split at fragment boundaries listed in an associated .m3u8 |
371 | // streaming playlist. MJPEG works with iOS on hardware that doesn't support |
372 | // the VP9 codec, but is poorly compressed for the low resolution. |
373 | '144p.video.mjpeg.mov' => [ |
374 | 'width' => '176', |
375 | 'height' => '144', |
376 | 'fpsmax' => '30', |
377 | 'videoBitrate' => '1000k', |
378 | 'videoCodec' => 'mjpeg', |
379 | 'noaudio' => 'true', |
380 | 'type' => 'video/quicktime; codecs="jpeg"', |
381 | 'streaming' => 'hls', |
382 | 'intraframe' => true, |
383 | ], |
384 | '180p.video.mpeg4.mp4' => [ |
385 | 'maxSize' => '320x180', |
386 | 'videoBitrate' => '380k', |
387 | 'twopass' => 'true', |
388 | 'videoCodec' => 'mpeg4', |
389 | 'noaudio' => 'true', |
390 | 'type' => 'video/mp4; codecs="mp4v.20.5"', |
391 | 'streaming' => 'hls', |
392 | ], |
393 | '240p.video.mpeg4.mp4' => [ |
394 | 'maxSize' => '426x240', |
395 | 'videoBitrate' => '720k', |
396 | 'twopass' => 'true', |
397 | 'videoCodec' => 'mpeg4', |
398 | 'noaudio' => 'true', |
399 | 'type' => 'video/mp4; codecs="mp4v.20.5"', |
400 | 'streaming' => 'hls', |
401 | ], |
402 | '360p.video.mpeg4.mp4' => [ |
403 | 'maxSize' => '640x360', |
404 | 'videoBitrate' => '1280k', |
405 | 'twopass' => 'true', |
406 | 'videoCodec' => 'mpeg4', |
407 | 'noaudio' => 'true', |
408 | 'type' => 'video/mp4; codecs="mp4v.20.5"', |
409 | 'streaming' => 'hls', |
410 | ], |
411 | '480p.video.mpeg4.mp4' => [ |
412 | 'maxSize' => '854x480', |
413 | 'videoBitrate' => '2500k', |
414 | 'twopass' => 'true', |
415 | 'videoCodec' => 'mpeg4', |
416 | 'noaudio' => 'true', |
417 | 'type' => 'video/mp4; codecs="mp4v.20.5"', |
418 | 'streaming' => 'hls', |
419 | ], |
420 | |
421 | // VP9 streaming tracks |
422 | // |
423 | // These are video-only, in fragmented .mp4 that allows adaptive streaming |
424 | // with chunks split at fragment boundaries listed in an associated .m3u8 |
425 | // streaming playlist. |
426 | // |
427 | // The 'remuxFrom' key specifies that if a WebM tracks was previously made, |
428 | // it can be used as a data source via remuxing packets instead of doing |
429 | // a fresh encoding when doing bulk conversions with requeueTranscodes.php |
430 | // with the '--remux' option. |
431 | '240p.video.vp9.mp4' => [ |
432 | 'maxSize' => '426x240', |
433 | 'videoBitrate' => '308k', |
434 | 'crf' => '37', |
435 | 'speed' => '3', |
436 | 'twopass' => 'true', |
437 | 'videoCodec' => 'vp9', |
438 | 'noaudio' => 'true', |
439 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
440 | 'streaming' => 'hls', |
441 | 'remuxFrom' => [ '240p.vp9.webm' ], |
442 | ], |
443 | '360p.video.vp9.mp4' => [ |
444 | 'maxSize' => '640x360', |
445 | 'videoBitrate' => '613k', |
446 | 'crf' => '36', |
447 | 'speed' => '3', |
448 | 'tileColumns' => '1', |
449 | 'twopass' => 'true', |
450 | 'videoCodec' => 'vp9', |
451 | 'noaudio' => 'true', |
452 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
453 | 'streaming' => 'hls', |
454 | 'remuxFrom' => [ '360p.vp9.webm' ], |
455 | ], |
456 | '480p.video.vp9.mp4' => [ |
457 | 'maxSize' => '854x480', |
458 | 'videoBitrate' => '1000k', |
459 | 'crf' => '33', |
460 | 'speed' => '3', |
461 | 'tileColumns' => '1', |
462 | 'twopass' => 'true', |
463 | 'videoCodec' => 'vp9', |
464 | 'noaudio' => 'true', |
465 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
466 | 'streaming' => 'hls', |
467 | 'remuxFrom' => [ '480p.vp9.webm' ], |
468 | ], |
469 | '720p.video.vp9.mp4' => [ |
470 | 'maxSize' => '1280x720', |
471 | 'videoBitrate' => '1993k', |
472 | 'crf' => '32', |
473 | 'speed' => '3', |
474 | 'tileColumns' => '2', |
475 | 'twopass' => 'true', |
476 | 'videoCodec' => 'vp9', |
477 | 'noaudio' => 'true', |
478 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
479 | 'streaming' => 'hls', |
480 | 'remuxFrom' => [ '720p.vp9.webm' ], |
481 | ], |
482 | '1080p.video.vp9.mp4' => [ |
483 | 'maxSize' => '1920x1080', |
484 | 'videoBitrate' => '3971k', |
485 | 'crf' => '31', |
486 | 'speed' => '3', |
487 | 'tileColumns' => '2', |
488 | 'twopass' => 'true', |
489 | 'videoCodec' => 'vp9', |
490 | 'noaudio' => 'true', |
491 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
492 | 'streaming' => 'hls', |
493 | 'remuxFrom' => [ '1080p.vp9.webm' ], |
494 | ], |
495 | '1440p.video.vp9.mp4' => [ |
496 | 'maxSize' => '2560x1440', |
497 | 'videoBitrate' => '6475k', |
498 | 'crf' => '24', |
499 | 'speed' => '3', |
500 | 'tileColumns' => '3', |
501 | 'twopass' => 'true', |
502 | 'videoCodec' => 'vp9', |
503 | 'noaudio' => 'true', |
504 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
505 | 'streaming' => 'hls', |
506 | 'remuxFrom' => [ '1440p.vp9.webm' ], |
507 | ], |
508 | '2160p.video.vp9.mp4' => [ |
509 | 'maxSize' => '3840x2160', |
510 | 'videoBitrate' => '12900k', |
511 | 'crf' => '15', |
512 | 'speed' => '3', |
513 | 'tileColumns' => '3', |
514 | 'twopass' => 'true', |
515 | 'videoCodec' => 'vp9', |
516 | 'noaudio' => 'true', |
517 | 'type' => 'video/mp4; codecs="vp09.00.51.08"', |
518 | 'streaming' => 'hls', |
519 | 'remuxFrom' => [ '2160p.vp9.webm' ], |
520 | ], |
521 | |
522 | // Loosely defined per PCF guide to mp4 profiles: |
523 | // https://develop.participatoryculture.org/index.php/ConversionMatrix |
524 | // and apple HLS profile guide: |
525 | // https://developer.apple.com/library/ios/#documentation/networkinginternet/conceptual/streamingmediaguide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-DontLinkElementID_24 |
526 | |
527 | // high profile |
528 | // level 2 needed for 160p60 |
529 | // level 2.1 needed for 240p60 |
530 | // level 3 needed for 360p60, 480p60 |
531 | // level 4 needed for 720p60, 1080p30 |
532 | // level 4.1 needed for 1080p60 |
533 | // level 5 needed for 1440p60, 2160p30 |
534 | // level 5.1 needed for 2160p60 |
535 | |
536 | // deprecated |
537 | '160p.mp4' => [ |
538 | 'maxSize' => '288x160', |
539 | 'videoCodec' => 'h264', |
540 | 'videoBitrate' => '154k', |
541 | 'audioCodec' => 'aac', |
542 | 'audioBitrate' => '112k', |
543 | 'type' => 'video/mp4; codecs="avc1.640014, mp4a.40.2"', |
544 | ], |
545 | |
546 | '240p.mp4' => [ |
547 | 'maxSize' => '426x240', |
548 | 'videoCodec' => 'h264', |
549 | 'videoBitrate' => '308k', |
550 | 'audioCodec' => 'aac', |
551 | 'audioBitrate' => '112k', |
552 | 'type' => 'video/mp4; codecs="avc1.42E015, mp4a.40.2"', |
553 | ], |
554 | |
555 | // deprecated |
556 | '320p.mp4' => [ |
557 | 'maxSize' => '480x320', |
558 | 'videoCodec' => 'h264', |
559 | 'videoBitrate' => '460k', |
560 | 'audioCodec' => 'aac', |
561 | 'audioBitrate' => '112k', |
562 | 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', |
563 | ], |
564 | |
565 | '360p.mp4' => [ |
566 | 'maxSize' => '640x360', |
567 | 'videoCodec' => 'h264', |
568 | 'videoBitrate' => '613k', |
569 | 'audioCodec' => 'aac', |
570 | 'audioBitrate' => '112k', |
571 | 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', |
572 | ], |
573 | '480p.mp4' => [ |
574 | 'maxSize' => '854x480', |
575 | 'videoCodec' => 'h264', |
576 | 'videoBitrate' => '1000k', |
577 | 'audioCodec' => 'aac', |
578 | 'audioBitrate' => '112k', |
579 | 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', |
580 | ], |
581 | '720p.mp4' => [ |
582 | 'maxSize' => '1280x720', |
583 | 'videoCodec' => 'h264', |
584 | 'videoBitrate' => '1993k', |
585 | 'audioCodec' => 'aac', |
586 | 'audioBitrate' => '112k', |
587 | 'type' => 'video/mp4; codecs="avc1.42E028, mp4a.40.2"', |
588 | ], |
589 | '1080p.mp4' => [ |
590 | 'maxSize' => '1920x1080', |
591 | 'videoCodec' => 'h264', |
592 | 'videoBitrate' => '3971k', |
593 | 'audioCodec' => 'aac', |
594 | 'audioBitrate' => '128k', |
595 | 'type' => 'video/mp4; codecs="avc1.640029, mp4a.40.2"', |
596 | ], |
597 | // Recommend against due to size |
598 | '1440p.mp4' => [ |
599 | 'maxSize' => '2560x1440', |
600 | 'videoCodec' => 'h264', |
601 | 'videoBitrate' => '6475k', |
602 | 'audioCodec' => 'aac', |
603 | 'audioBitrate' => '112k', |
604 | 'type' => 'video/mp4; codecs="avc1.42E032, mp4a.40.2"', |
605 | ], |
606 | // Recommend against due to size |
607 | '2160p.mp4' => [ |
608 | 'maxSize' => '4096x2160', |
609 | 'videoCodec' => 'h264', |
610 | 'videoBitrate' => '12900k', |
611 | 'audioCodec' => 'aac', |
612 | 'audioBitrate' => '112k', |
613 | 'type' => 'video/mp4; codecs="avc1.42E033, mp4a.40.2"', |
614 | ], |
615 | |
616 | // Audio profiles |
617 | 'ogg' => [ |
618 | 'audioCodec' => 'vorbis', |
619 | 'audioQuality' => '3', |
620 | 'samplerate' => '44100', |
621 | 'noUpscaling' => 'true', |
622 | 'novideo' => 'true', |
623 | 'type' => 'audio/ogg; codecs="vorbis"', |
624 | ], |
625 | 'opus' => [ |
626 | 'audioCodec' => 'opus', |
627 | 'audioQuality' => '1', |
628 | 'samplerate' => '48000', |
629 | 'noUpscaling' => 'true', |
630 | 'novideo' => 'true', |
631 | 'type' => 'audio/ogg; codecs="opus"', |
632 | ], |
633 | 'mp3' => [ |
634 | 'audioCodec' => 'mp3', |
635 | 'audioQuality' => '1', |
636 | 'samplerate' => '44100', |
637 | 'channels' => '2', |
638 | 'noUpscaling' => 'true', |
639 | 'novideo' => 'true', |
640 | 'type' => 'audio/mpeg', |
641 | ], |
642 | 'm4a' => [ |
643 | 'audioCodec' => 'aac', |
644 | 'audioQuality' => '1', |
645 | 'samplerate' => '44100', |
646 | 'noUpscaling' => 'true', |
647 | 'novideo' => 'true', |
648 | 'type' => 'audio/mp4; codecs="mp4a.40.5"', |
649 | ], |
650 | ]; |
651 | |
652 | /** |
653 | * @param File $file |
654 | * @param string $transcodeKey |
655 | * @return string |
656 | */ |
657 | public static function getDerivativeFilePath( $file, $transcodeKey ) { |
658 | return $file->getTranscodedPath( static::getTranscodeFileBaseName( $file, $transcodeKey ) ); |
659 | } |
660 | |
661 | /** |
662 | * Get the name to use as the base name for the transcode. |
663 | * |
664 | * Swift has problems where the url-encoded version of |
665 | * the path (ie '0/00/filename.ogv/filename.ogv.720p.webm' ) |
666 | * is greater than > 1024 bytes, so shorten in that case. |
667 | * |
668 | * Future versions might respect FileRepo::$abbrvThreshold. |
669 | * |
670 | * @param File $file |
671 | * @param string $suffix Optional suffix (e.g. transcode key). |
672 | * @return string File name, or the string transcode. |
673 | */ |
674 | public static function getTranscodeFileBaseName( $file, $suffix = '' ) { |
675 | $name = $file->getName(); |
676 | $length = strlen( urlencode( '0/00/' . $name . '/' . $name . '.' . $suffix ) ); |
677 | if ( $length > 1024 ) { |
678 | return 'transcode' . '.' . $suffix; |
679 | } |
680 | return $name . '.' . $suffix; |
681 | } |
682 | |
683 | /** |
684 | * Get url for a transcode. |
685 | * |
686 | * @param File $file |
687 | * @param string $suffix Transcode key |
688 | * @return string |
689 | */ |
690 | public static function getTranscodedUrlForFile( $file, $suffix = '' ) { |
691 | return $file->getTranscodedUrl( static::getTranscodeFileBaseName( $file, $suffix ) ); |
692 | } |
693 | |
694 | /** |
695 | * Get temp file at target path for video encode |
696 | * |
697 | * @param File $file |
698 | * @param string $transcodeKey |
699 | * @param string $suffix |
700 | * |
701 | * @return TempFSFile|false at target encode path |
702 | */ |
703 | public static function getTargetEncodeFile( $file, $transcodeKey, $suffix = '' ) { |
704 | $filePath = static::getDerivativeFilePath( $file, $transcodeKey ) . $suffix; |
705 | $ext = strtolower( pathinfo( $filePath, PATHINFO_EXTENSION ) ); |
706 | |
707 | // Create a temp FS file with the same extension |
708 | $tmpFileFactory = new TempFSFileFactory(); |
709 | $tmpFile = $tmpFileFactory->newTempFSFile( 'transcode_' . $transcodeKey, $ext ); |
710 | if ( !$tmpFile ) { |
711 | return false; |
712 | } |
713 | return $tmpFile; |
714 | } |
715 | |
716 | /** |
717 | * Get the max size of the web stream ( constant bitrate ) |
718 | * @return int |
719 | */ |
720 | public static function getMaxSizeWebStream() { |
721 | $maxSize = 0; |
722 | foreach ( static::enabledVideoTranscodes() as $transcodeKey ) { |
723 | if ( isset( static::$derivativeSettings[$transcodeKey]['videoBitrate'] ) ) { |
724 | $currentSize = static::$derivativeSettings[$transcodeKey]['maxSize'] ?? null; |
725 | if ( $currentSize > $maxSize ) { |
726 | $maxSize = $currentSize; |
727 | } |
728 | } |
729 | } |
730 | return $maxSize; |
731 | } |
732 | |
733 | /** |
734 | * Give a rough estimate on file size |
735 | * Note this is not always accurate.. especially with variable bitrate codecs ;) |
736 | * @param File $file |
737 | * @param string $transcodeKey |
738 | * @suppress PhanTypePossiblyInvalidDimOffset |
739 | * @return int |
740 | */ |
741 | public static function getProjectedFileSize( $file, $transcodeKey ) { |
742 | $settings = static::$derivativeSettings[$transcodeKey]; |
743 | // FIXME broken, as bitrate settings can contain units (64k) |
744 | if ( $settings[ 'videoBitrate' ] && $settings['audioBitrate'] ) { |
745 | return $file->getLength() * 8 * ( |
746 | (int)$settings['videoBitrate'] |
747 | + |
748 | (int)$settings['audioBitrate'] |
749 | ); |
750 | } |
751 | // Else just return the size of the source video |
752 | // ( we have no idea how large the actual derivative size will be ) |
753 | |
754 | /** @var ID3Handler $handler */ |
755 | $handler = $file->getHandler(); |
756 | '@phan-var ID3Handler $handler'; |
757 | return $file->getLength() * $handler->getBitrate( $file ) * 8; |
758 | } |
759 | |
760 | /** |
761 | * Static function to get the set of video assets |
762 | * Checks if the file is local or remote and grabs respective sources |
763 | * @param File &$file |
764 | * @param array $options |
765 | * @return array|mixed |
766 | */ |
767 | public static function getSources( &$file, $options = [] ) { |
768 | if ( $file->isLocal() || $file->repo instanceof IForeignRepoWithDB ) { |
769 | return static::getLocalSources( $file, $options ); |
770 | } |
771 | |
772 | if ( $file->getRepo() instanceof IForeignRepoWithMWApi ) { |
773 | return static::getRemoteSources( $file, $options ); |
774 | } |
775 | |
776 | return []; |
777 | } |
778 | |
779 | /** |
780 | * Grabs sources from the remote repo via ApiQueryVideoInfo.php entry point. |
781 | * |
782 | * TODO: This method could use some rethinking. See comments on PS1 of |
783 | * <https://gerrit.wikimedia.org/r/#/c/117916/> |
784 | * |
785 | * Because this works with commons regardless of whether TimedMediaHandler is installed or not |
786 | * @param File $file The File must belong to a repo that is an instance of IForeignRepoWithMWApi |
787 | * @param array $options |
788 | * @return array|mixed |
789 | */ |
790 | public static function getRemoteSources( $file, $options = [] ) { |
791 | $regenerator = static function () use ( $file, $options ) { |
792 | // Setup source attribute options |
793 | $dataPrefix = in_array( 'nodata', $options, true ) ? '' : 'data-'; |
794 | |
795 | wfDebug( "Get Video sources from remote api for " . $file->getName() . "\n" ); |
796 | $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
797 | $query = [ |
798 | 'action' => 'query', |
799 | 'prop' => 'videoinfo', |
800 | 'viprop' => 'derivatives', |
801 | 'titles' => $namespaceInfo->getCanonicalName( NS_FILE ) . ':' . $file->getTitle()->getText() |
802 | ]; |
803 | |
804 | /** @var IForeignRepoWithMWApi $repo */ |
805 | $repo = $file->getRepo(); |
806 | '@phan-var IForeignRepoWithMWApi $repo'; |
807 | $data = $repo->fetchImageQuery( $query ); |
808 | |
809 | if ( isset( $data['warnings']['query'] ) && |
810 | $data['warnings']['query']['*'] === "Unrecognized value for parameter 'prop': videoinfo" |
811 | ) { |
812 | // The target wiki doesn't have TimedMediaHandler. |
813 | // Use the normal file repo system single source: |
814 | return [ static::getPrimarySourceAttributes( $file, [ $dataPrefix ] ) ]; |
815 | } |
816 | |
817 | $sources = []; |
818 | // Generate the source list from the data response: |
819 | if ( isset( $data['query']['pages'] ) ) { |
820 | $vidResult = array_shift( $data['query']['pages'] ); |
821 | if ( isset( $vidResult['videoinfo'] ) ) { |
822 | $derResult = array_shift( $vidResult['videoinfo'] ); |
823 | $derivatives = $derResult['derivatives']; |
824 | foreach ( $derivatives as $derivativeSource ) { |
825 | $sources[] = $derivativeSource; |
826 | } |
827 | } |
828 | } |
829 | |
830 | return $sources; |
831 | }; |
832 | |
833 | $repoInfo = $file->getRepo()->getInfo(); |
834 | $cacheTTL = $repoInfo['descriptionCacheExpiry'] ?? 0; |
835 | |
836 | if ( $cacheTTL > 0 ) { |
837 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
838 | $sources = $cache->getWithSetCallback( |
839 | $cache->makeKey( 'WebVideoSources-url', $file->getRepoName(), $file->getName() ), |
840 | $cacheTTL, |
841 | $regenerator |
842 | ); |
843 | } else { |
844 | $sources = $regenerator(); |
845 | } |
846 | |
847 | return $sources; |
848 | } |
849 | |
850 | /** |
851 | * Based on the $wgEnabledTranscodeSet set of enabled derivatives we |
852 | * return sources that are ready. |
853 | * |
854 | * This will not automatically update or queue anything! |
855 | * |
856 | * @param File &$file File object |
857 | * @param array $options Options, a set of options: |
858 | * 'nodata' Strips the data- attribute, useful when your output is not html |
859 | * @return array an associative array of sources suitable for <source> tag output |
860 | */ |
861 | public static function getLocalSources( &$file, $options = [] ) { |
862 | $sources = []; |
863 | |
864 | // Add the original file: |
865 | $sources[] = static::getPrimarySourceAttributes( $file, $options ); |
866 | |
867 | // If $wgEnableTranscode is false don't look for or add other local sources: |
868 | if ( MediaWikiServices::getInstance()->getMainConfig()->get( 'EnableTranscode' ) === false && |
869 | !( $file->repo instanceof IForeignRepoWithDB ) ) { |
870 | return $sources; |
871 | } |
872 | |
873 | // If an "oldFile" don't look for other sources: |
874 | if ( $file->isOld() ) { |
875 | return $sources; |
876 | } |
877 | |
878 | /** @var ID3Handler $handler */ |
879 | $handler = $file->getHandler(); |
880 | '@phan-var ID3Handler $handler'; |
881 | // Now Check for derivatives |
882 | if ( $handler->isAudio( $file ) ) { |
883 | $transcodeSet = static::enabledAudioTranscodes(); |
884 | } else { |
885 | $transcodeSet = static::enabledVideoTranscodes(); |
886 | } |
887 | |
888 | $lastHLS = null; |
889 | foreach ( $transcodeSet as $transcodeKey ) { |
890 | if ( static::isTranscodeKeyPlayable( $transcodeKey ) && |
891 | static::isTranscodeEnabled( $file, $transcodeKey ) |
892 | ) { |
893 | // Try and add the source |
894 | static::addSourceIfReady( $file, $sources, $transcodeKey, $options ); |
895 | } |
896 | $streaming = static::$derivativeSettings[$transcodeKey]['streaming'] ?? ''; |
897 | if ( $streaming === 'hls' && static::isTranscodeReady( $file, $transcodeKey ) ) { |
898 | $lastHLS = $transcodeKey; |
899 | } |
900 | } |
901 | if ( $lastHLS ) { |
902 | $src = static::getTranscodedUrlForFile( $file, 'm3u8' ); |
903 | $settings =& static::$derivativeSettings[$lastHLS]; |
904 | [ $width, $height ] = static::getMaxSizeTransform( |
905 | $file, |
906 | $settings['maxSize'] ?? ( |
907 | implode( 'x', [ |
908 | $settings['width'] ?? '0', |
909 | $settings['height'] ?? '0', |
910 | ] ) |
911 | ) |
912 | ); |
913 | $sources[] = [ |
914 | 'src' => $src, |
915 | 'title' => wfMessage( 'timedmedia-derivative-desc-m3u8' )->text(), |
916 | 'type' => 'application/vnd.apple.mpegurl', |
917 | 'shorttitle' => wfMessage( 'timedmedia-derivative-desc-m3u8' )->text(), |
918 | 'transcodekey' => 'm3u8', |
919 | 'width' => $width, |
920 | 'height' => $height, |
921 | ]; |
922 | } |
923 | |
924 | return $sources; |
925 | } |
926 | |
927 | /** |
928 | * Does this transcode key represent a directly-playable type? |
929 | * If not it's a backing track for adaptive streaming, and should |
930 | * not be exposed directly as a downloadable/playable derivative. |
931 | * |
932 | * @param string $transcodeKey |
933 | * @return bool |
934 | */ |
935 | public static function isTranscodeKeyPlayable( $transcodeKey ) { |
936 | $settings = static::$derivativeSettings[$transcodeKey] ?? null; |
937 | if ( !$settings ) { |
938 | return false; |
939 | } |
940 | $streaming = $settings['streaming'] ?? false; |
941 | return !$streaming; |
942 | } |
943 | |
944 | /** |
945 | * Get the transcode state for a given filename and transcodeKey |
946 | * |
947 | * @param File $file |
948 | * @param string $transcodeKey |
949 | * @return bool |
950 | */ |
951 | public static function isTranscodeReady( $file, $transcodeKey ) { |
952 | // Check if we need to populate the transcodeState cache: |
953 | $transcodeState = static::getTranscodeState( $file ); |
954 | |
955 | // If no state is found the cache for this file is false: |
956 | if ( !isset( $transcodeState[ $transcodeKey ] ) ) { |
957 | return false; |
958 | } |
959 | // Else return boolean ready state ( if not null, then ready ): |
960 | return ( $transcodeState[ $transcodeKey ]['time_success'] ) !== null; |
961 | } |
962 | |
963 | /** |
964 | * Clear the transcode state cache: |
965 | * @param string|null $fileName Optional fileName to clear transcode cache for |
966 | */ |
967 | public static function clearTranscodeCache( $fileName = null ) { |
968 | if ( $fileName ) { |
969 | unset( static::$transcodeState[ $fileName ] ); |
970 | } else { |
971 | static::$transcodeState = []; |
972 | } |
973 | } |
974 | |
975 | /** |
976 | * Populates the transcode table with the current DB state of transcodes |
977 | * if transcodes are not found in the database their state is set to "false" |
978 | * |
979 | * @param File $file File object |
980 | * @param IReadableDatabase|false $db |
981 | * @return array[] |
982 | */ |
983 | public static function getTranscodeState( $file, $db = false ) { |
984 | $fileName = $file->getName(); |
985 | if ( $db || !isset( static::$transcodeState[$fileName] ) ) { |
986 | if ( $db === false ) { |
987 | $db = $file->repo->getReplicaDB(); |
988 | } |
989 | // initialize the transcode state array |
990 | static::$transcodeState[ $fileName ] = []; |
991 | $res = $db->newSelectQueryBuilder() |
992 | ->select( '*' ) |
993 | ->from( 'transcode' ) |
994 | ->where( [ 'transcode_image_name' => $fileName ] ) |
995 | ->limit( 100 ) |
996 | ->caller( __METHOD__ ) |
997 | ->fetchResultSet(); |
998 | |
999 | // Populate the per transcode state cache |
1000 | foreach ( $res as $row ) { |
1001 | // strip the out the "transcode_" from keys |
1002 | $transcodeState = []; |
1003 | foreach ( $row as $k => $v ) { |
1004 | $transcodeState[ str_replace( 'transcode_', '', $k ) ] = $v; |
1005 | } |
1006 | static::$transcodeState[ $fileName ][ $row->transcode_key ] = $transcodeState; |
1007 | } |
1008 | } |
1009 | $sorted = static::$transcodeState[ $fileName ]; |
1010 | uksort( $sorted, 'strnatcmp' ); |
1011 | return $sorted; |
1012 | } |
1013 | |
1014 | /** |
1015 | * Remove any transcode files and db states associated with a given $file |
1016 | * Note that if you want to see them again, you must re-queue them by calling |
1017 | * startJobQueue() or updateJobQueue(). |
1018 | * |
1019 | * also remove the transcode files: |
1020 | * @param File $file File Object |
1021 | * @param string|false $transcodeKey Optional transcode key to remove only this key |
1022 | */ |
1023 | public static function removeTranscodes( $file, $transcodeKey = false ) { |
1024 | // if transcode key is non-false, non-null: |
1025 | if ( $transcodeKey ) { |
1026 | // only remove the requested $transcodeKey |
1027 | $removeKeys = [ $transcodeKey ]; |
1028 | } else { |
1029 | // Remove any existing files ( regardless of their state ) |
1030 | $res = $file->repo->getPrimaryDB()->newSelectQueryBuilder() |
1031 | ->select( 'transcode_key' ) |
1032 | ->from( 'transcode' ) |
1033 | ->where( [ 'transcode_image_name' => $file->getName() ] ) |
1034 | ->caller( __METHOD__ ) |
1035 | ->fetchResultSet(); |
1036 | |
1037 | $removeKeys = []; |
1038 | foreach ( $res as $transcodeRow ) { |
1039 | $removeKeys[] = $transcodeRow->transcode_key; |
1040 | } |
1041 | } |
1042 | |
1043 | // Remove files by key: |
1044 | $urlsToPurge = []; |
1045 | $filesToPurge = []; |
1046 | $hasHLS = false; |
1047 | foreach ( $removeKeys as $tKey ) { |
1048 | $urlPath = static::getTranscodedUrlForFile( $file, $tKey ); |
1049 | $filePath = static::getDerivativeFilePath( $file, $tKey ); |
1050 | $urlsToPurge[] = $urlPath; |
1051 | $filesToPurge[] = $filePath; |
1052 | |
1053 | $options = static::$derivativeSettings[$tKey] ?? []; |
1054 | $streaming = $options['streaming'] ?? null; |
1055 | if ( $streaming === 'hls' ) { |
1056 | $urlsToPurge[] = $urlPath . '.m3u8'; |
1057 | $filesToPurge[] = $filePath . '.m3u8'; |
1058 | $hasHLS = true; |
1059 | } |
1060 | } |
1061 | if ( $hasHLS && $transcodeKey === false ) { |
1062 | // Delete all derivatives including the main hls manifest |
1063 | $urlsToPurge[] = static::getTranscodedUrlForFile( $file, 'm3u8' ); |
1064 | $filesToPurge[] = static::getDerivativeFilePath( $file, 'm3u8' ); |
1065 | } |
1066 | foreach ( $filesToPurge as $filePath ) { |
1067 | if ( $file->repo->fileExists( $filePath ) ) { |
1068 | $res = $file->repo->quickPurge( $filePath ); |
1069 | if ( !$res ) { |
1070 | wfDebug( "Could not delete file $filePath\n" ); |
1071 | } |
1072 | } |
1073 | } |
1074 | |
1075 | $update = new CdnCacheUpdate( $urlsToPurge ); |
1076 | DeferredUpdates::addUpdate( $update ); |
1077 | |
1078 | // Build the sql query: |
1079 | $queryBuilder = $file->repo->getPrimaryDB()->newDeleteQueryBuilder() |
1080 | ->deleteFrom( 'transcode' ) |
1081 | ->where( [ 'transcode_image_name' => $file->getName() ] ); |
1082 | // Check if we are removing a specific transcode key |
1083 | if ( $transcodeKey !== false ) { |
1084 | $queryBuilder->andWhere( [ 'transcode_key' => $transcodeKey ] ); |
1085 | } |
1086 | // Remove the db entries |
1087 | $queryBuilder->caller( __METHOD__ )->execute(); |
1088 | |
1089 | // Purge the cache for pages that include this video: |
1090 | $titleObj = $file->getTitle(); |
1091 | static::invalidatePagesWithFile( $titleObj ); |
1092 | |
1093 | // Remove from local WebVideoTranscode cache: |
1094 | static::clearTranscodeCache( $titleObj->getDBkey() ); |
1095 | if ( $transcodeKey !== false ) { |
1096 | // We only removed a single transcode, so we need to update the manifests |
1097 | static::updateStreamingManifests( $file ); |
1098 | } |
1099 | } |
1100 | |
1101 | /** |
1102 | * @param Title $titleObj |
1103 | */ |
1104 | public static function invalidatePagesWithFile( $titleObj ) { |
1105 | wfDebug( "WebVideoTranscode:: Invalidate pages that include: " . $titleObj->getDBkey() . "\n" ); |
1106 | // Purge the main image page: |
1107 | $titleObj->invalidateCache(); |
1108 | |
1109 | // Invalidate cache for all pages using this file |
1110 | $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks( |
1111 | $titleObj, |
1112 | 'imagelinks', |
1113 | // TODO add 'causeAgent' => $user->getName() |
1114 | // and more accurate action |
1115 | [ 'causeAction' => 'tmh-transcode-update' ] |
1116 | ); |
1117 | MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob ); |
1118 | |
1119 | // TODO GlobalUsage |
1120 | } |
1121 | |
1122 | /** |
1123 | * Add a source to the sources list if the transcode job is ready |
1124 | * |
1125 | * If the source is not found, it will not be used yet... |
1126 | * Missing transcodes should be added by write tasks, not read tasks! |
1127 | * @param File $file |
1128 | * @param array &$sources |
1129 | * @param string $transcodeKey |
1130 | * @param array $dataPrefix |
1131 | */ |
1132 | public static function addSourceIfReady( $file, &$sources, $transcodeKey, $dataPrefix ) { |
1133 | // Check if the transcode is ready: |
1134 | if ( static::isTranscodeReady( $file, $transcodeKey ) ) { |
1135 | $sources[] = static::getDerivativeSourceAttributes( $file, $transcodeKey, $dataPrefix ); |
1136 | } |
1137 | } |
1138 | |
1139 | /** |
1140 | * Get the primary "source" asset used for other derivatives |
1141 | * @param File $file |
1142 | * @param array $options |
1143 | * @return array |
1144 | */ |
1145 | public static function getPrimarySourceAttributes( $file, $options = [] ) { |
1146 | $src = in_array( 'fullurl', $options, true ) ? wfExpandUrl( $file->getUrl() ) : $file->getUrl(); |
1147 | |
1148 | /** @var FLACHandler|MIDIHandler|MP3Handler|MP4Handler|OggHandler|WAVHandler $handler */ |
1149 | $handler = $file->getHandler(); |
1150 | '@phan-var FLACHandler|MIDIHandler|MP3Handler|MP4Handler|OggHandler|WAVHandler $handler'; |
1151 | $bitrate = $handler->getBitrate( $file ); |
1152 | |
1153 | $source = [ |
1154 | 'src' => $src, |
1155 | 'type' => $handler->getWebType( $file ), |
1156 | 'width' => (int)$file->getWidth(), |
1157 | 'height' => (int)$file->getHeight(), |
1158 | ]; |
1159 | |
1160 | if ( $bitrate ) { |
1161 | $source["bandwidth"] = round( $bitrate ); |
1162 | } |
1163 | return $source; |
1164 | } |
1165 | |
1166 | /** |
1167 | * Get derivative "source" attributes |
1168 | * @param File $file |
1169 | * @param string $transcodeKey |
1170 | * @param array $options |
1171 | * @return array |
1172 | * @suppress PhanTypePossiblyInvalidDimOffset |
1173 | */ |
1174 | public static function getDerivativeSourceAttributes( $file, $transcodeKey, $options = [] ) { |
1175 | $fileName = $file->getTitle()->getDBkey(); |
1176 | |
1177 | $src = static::getTranscodedUrlForFile( $file, $transcodeKey ); |
1178 | |
1179 | /** @var ID3Handler $handler */ |
1180 | $handler = $file->getHandler(); |
1181 | '@phan-var ID3Handler $handler'; |
1182 | if ( $handler->isAudio( $file ) ) { |
1183 | $width = $height = 0; |
1184 | } else { |
1185 | [ $width, $height ] = static::getMaxSizeTransform( |
1186 | $file, |
1187 | static::$derivativeSettings[$transcodeKey]['maxSize'] |
1188 | ); |
1189 | } |
1190 | |
1191 | // Setup the url src: |
1192 | $src = in_array( 'fullurl', $options, true ) ? wfExpandUrl( $src ) : $src; |
1193 | $fields = [ |
1194 | 'src' => $src, |
1195 | 'type' => static::$derivativeSettings[ $transcodeKey ][ 'type' ], |
1196 | 'transcodekey' => $transcodeKey, |
1197 | |
1198 | // Add data attributes per emerging DASH / webTV adaptive streaming attributes |
1199 | // eventually we will define a manifest xml entry point. |
1200 | "width" => (int)$width, |
1201 | "height" => (int)$height, |
1202 | ]; |
1203 | |
1204 | // a "ready" transcode should have a bitrate: |
1205 | if ( isset( static::$transcodeState[$fileName] ) ) { |
1206 | $fields["bandwidth"] = (int)static::$transcodeState[$fileName][$transcodeKey]['final_bitrate']; |
1207 | } |
1208 | return $fields; |
1209 | } |
1210 | |
1211 | /** |
1212 | * Queue up all enabled transcodes if missing. |
1213 | * @param File $file File object |
1214 | */ |
1215 | public static function startJobQueue( File $file ) { |
1216 | $keys = static::enabledTranscodes(); |
1217 | |
1218 | // 'Natural sort' puts the transcodes in ascending order by resolution, |
1219 | // which roughly gives us fastest-to-slowest order. |
1220 | natsort( $keys ); |
1221 | |
1222 | foreach ( $keys as $tKey ) { |
1223 | // Note the job queue will de-duplicate and handle various errors, so we |
1224 | // can just blast out the full list here. |
1225 | static::updateJobQueue( $file, $tKey ); |
1226 | } |
1227 | } |
1228 | |
1229 | /** |
1230 | * Regenerate the streaming manifests, currently the HLS multivariant playlist, |
1231 | * to refer to available completed transcodes. If there are no available |
1232 | * compatible transcodes the playlist will be written out empty. |
1233 | * |
1234 | * Simultaneous attempts to overwrite will result in whichever commits to |
1235 | * the filesystem or other backend last "winning". Locks in the database |
1236 | * have been known to cause production problems, and a more thorough queueing |
1237 | * system might be wise to look into later. |
1238 | * |
1239 | * @param File $file base file to check for transcodes on |
1240 | */ |
1241 | public static function updateStreamingManifests( File $file ): Status { |
1242 | $fileName = $file->getTitle()->getDBkey(); |
1243 | $repo = $file->getRepo(); |
1244 | if ( !is_a( $repo, 'LocalRepo' ) ) { |
1245 | return Status::newGood(); |
1246 | } |
1247 | $dbw = $repo->getPrimaryDB(); |
1248 | |
1249 | // Note that trying to use a database lock here plays hell with many |
1250 | // many scenarios in production, it seems, especially when deleting |
1251 | // files. |
1252 | // |
1253 | // See [T348689](https://phabricator.wikimedia.org/T348689) etc. |
1254 | // |
1255 | // To in future: serialize these updates through the job queue |
1256 | // or something else *clever* and non-destructive in terms of wait |
1257 | // states. |
1258 | |
1259 | static::clearTranscodeCache( $fileName ); |
1260 | |
1261 | // Currently only HLS streaming is output. |
1262 | $m3u8 = "$fileName.m3u8"; |
1263 | $keys = []; |
1264 | foreach ( static::$derivativeSettings as $key => $settings ) { |
1265 | $streaming = $settings['streaming'] ?? ''; |
1266 | if ( $streaming === 'hls' && static::isTranscodeReady( $file, $key ) ) { |
1267 | $keys[] = $key; |
1268 | } |
1269 | } |
1270 | // @todo look up the frame rate and final bitrates and use those |
1271 | $multivariant = new Multivariant( $fileName, $keys ); |
1272 | $playlist = $multivariant->playlist(); |
1273 | |
1274 | $tmpFileFactory = new TempFSFileFactory(); |
1275 | $tmpFile = $tmpFileFactory->newTempFSFile( $m3u8, 'm3u8' ); |
1276 | if ( !$tmpFile ) { |
1277 | return Status::newFatal( 'm3u8-error-create-temp', $m3u8 ); |
1278 | } |
1279 | $result = file_put_contents( $tmpFile->getPath(), $playlist ); |
1280 | if ( $result === false ) { |
1281 | return Status::newFatal( 'm3u8-error-write-temp', $m3u8 ); |
1282 | } |
1283 | |
1284 | $result = $repo->quickImport( |
1285 | $tmpFile, |
1286 | $file->getTranscodedPath( $m3u8 ) |
1287 | ); |
1288 | return $result; |
1289 | } |
1290 | |
1291 | /** |
1292 | * Make sure all relevant transcodes for the given file are tracked in the |
1293 | * transcodes table; add entries for any missing ones. |
1294 | * |
1295 | * @param File $file File object |
1296 | */ |
1297 | public static function cleanupTranscodes( File $file ) { |
1298 | $fileName = $file->getTitle()->getDBkey(); |
1299 | $dbw = $file->repo->getPrimaryDB(); |
1300 | |
1301 | $transcodeState = static::getTranscodeState( $file, $dbw ); |
1302 | |
1303 | $keys = static::enabledTranscodes(); |
1304 | foreach ( $keys as $transcodeKey ) { |
1305 | if ( !static::isTranscodeEnabled( $file, $transcodeKey ) ) { |
1306 | // This transcode is no longer enabled or erroneously included... |
1307 | // Leave it in place, allowing it to be removed manually; |
1308 | // it won't be used in playback and should be doing no harm. |
1309 | continue; |
1310 | } |
1311 | if ( !isset( $transcodeState[ $transcodeKey ] ) ) { |
1312 | $dbw->newInsertQueryBuilder() |
1313 | ->insertInto( 'transcode' ) |
1314 | ->ignore() |
1315 | ->row( [ |
1316 | 'transcode_image_name' => $fileName, |
1317 | 'transcode_key' => $transcodeKey, |
1318 | // Do not start transcode jobs automatically, as purging is too common. |
1319 | 'transcode_time_addjob' => null, |
1320 | 'transcode_error' => '', |
1321 | 'transcode_final_bitrate' => 0, |
1322 | ] ) |
1323 | ->caller( __METHOD__ )->execute(); |
1324 | } |
1325 | } |
1326 | |
1327 | // Remove from local WebVideoTranscode cache: |
1328 | static::clearTranscodeCache( $fileName ); |
1329 | } |
1330 | |
1331 | /** |
1332 | * Check if the given transcode key is appropriate for the file. |
1333 | * |
1334 | * @param File $file File object |
1335 | * @param string $transcodeKey transcode key |
1336 | * @return bool |
1337 | * @suppress PhanTypePossiblyInvalidDimOffset |
1338 | */ |
1339 | public static function isTranscodeEnabled( File $file, $transcodeKey ) { |
1340 | /** @var FLACHandler|MIDIHandler|MP3Handler|MP4Handler|OggHandler|WAVHandler $handler */ |
1341 | $handler = $file->getHandler(); |
1342 | '@phan-var FLACHandler|MIDIHandler|MP3Handler|MP4Handler|OggHandler|WAVHandler $handler'; |
1343 | $audio = $handler->isAudio( $file ); |
1344 | if ( $audio ) { |
1345 | $keys = static::enabledAudioTranscodes(); |
1346 | } else { |
1347 | $keys = static::enabledVideoTranscodes(); |
1348 | } |
1349 | |
1350 | if ( in_array( $transcodeKey, $keys, true ) ) { |
1351 | $settings = static::$derivativeSettings[$transcodeKey]; |
1352 | if ( $audio ) { |
1353 | $sourceCodecs = $handler->getStreamTypes( $file ); |
1354 | $sourceCodec = $sourceCodecs ? strtolower( $sourceCodecs[0] ) : ''; |
1355 | return ( $sourceCodec !== $settings['audioCodec'] ); |
1356 | } |
1357 | $streaming = $settings['streaming'] ?? false; |
1358 | $novideo = $settings['novideo'] ?? false; |
1359 | if ( $streaming && $novideo ) { |
1360 | // Streaming audio should be generated for all formats |
1361 | // if audio is present on the file, and for none if not. |
1362 | return $handler->hasAudio( $file ); |
1363 | } |
1364 | if ( static::isTargetLargerThanFile( $file, $settings['maxSize'] ?? '' ) ) { |
1365 | // Are we the smallest enabled transcode for this type? |
1366 | // Then go ahead and make a wee little transcode for compat. |
1367 | return static::isSmallestTranscodeForCodec( $transcodeKey ); |
1368 | } |
1369 | return true; |
1370 | } |
1371 | // Transcode key is invalid or has been disabled. |
1372 | return false; |
1373 | } |
1374 | |
1375 | /** |
1376 | * Update the job queue if the file is not already in the job queue: |
1377 | * @param File &$file File object |
1378 | * @param string $transcodeKey transcode key |
1379 | * @param array $options array with 'manualOverride' or 'remux' boolean options |
1380 | */ |
1381 | public static function updateJobQueue( &$file, $transcodeKey, $options = [] ) { |
1382 | $fileName = $file->getTitle()->getDBkey(); |
1383 | $dbw = $file->repo->getPrimaryDB(); |
1384 | |
1385 | $transcodeState = static::getTranscodeState( $file, $dbw ); |
1386 | |
1387 | if ( !static::isTranscodeEnabled( $file, $transcodeKey ) ) { |
1388 | return; |
1389 | } |
1390 | |
1391 | // If the job hasn't been added yet, attempt to do so |
1392 | if ( !isset( $transcodeState[ $transcodeKey ] ) ) { |
1393 | $dbw->newInsertQueryBuilder() |
1394 | ->insertInto( 'transcode' ) |
1395 | ->ignore() |
1396 | ->row( [ |
1397 | 'transcode_image_name' => $fileName, |
1398 | 'transcode_key' => $transcodeKey, |
1399 | 'transcode_time_addjob' => $dbw->timestamp(), |
1400 | 'transcode_error' => '', |
1401 | 'transcode_final_bitrate' => 0, |
1402 | ] ) |
1403 | ->caller( __METHOD__ )->execute(); |
1404 | |
1405 | if ( !$dbw->affectedRows() ) { |
1406 | // There is already a row for that job added by another request, no need to continue |
1407 | return; |
1408 | } |
1409 | |
1410 | // Set the priority |
1411 | $prioritized = static::isTranscodePrioritized( $file, $transcodeKey ); |
1412 | |
1413 | $job = new JobSpecification( $prioritized ? 'webVideoTranscodePrioritized' : 'webVideoTranscode', [ |
1414 | 'transcodeMode' => 'derivative', |
1415 | 'transcodeKey' => $transcodeKey, |
1416 | 'prioritized' => $prioritized, |
1417 | 'manualOverride' => $options['manualOverride'] ?? false, |
1418 | 'remux' => $options['remux'] ?? false, |
1419 | ], [], $file->getTitle() ); |
1420 | |
1421 | try { |
1422 | MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup()->push( $job ); |
1423 | // Clear the state cache ( now that we have updated the page ) |
1424 | static::clearTranscodeCache( $fileName ); |
1425 | } catch ( Exception $ex ) { |
1426 | // Adding job failed, update transcode row |
1427 | $dbw->newUpdateQueryBuilder() |
1428 | ->update( 'transcode' ) |
1429 | ->set( [ |
1430 | 'transcode_time_error' => $dbw->timestamp(), |
1431 | 'transcode_error' => "Failed to insert Job." |
1432 | ] ) |
1433 | ->where( [ |
1434 | 'transcode_image_name' => $fileName, |
1435 | 'transcode_key' => $transcodeKey, |
1436 | ] ) |
1437 | ->caller( __METHOD__ ) |
1438 | ->execute(); |
1439 | } |
1440 | } |
1441 | } |
1442 | |
1443 | /** |
1444 | * Check if this transcode belongs to the high-priority queue. |
1445 | * @param File $file |
1446 | * @param string $transcodeKey |
1447 | * @return bool |
1448 | */ |
1449 | public static function isTranscodePrioritized( File $file, $transcodeKey ) { |
1450 | $transcodeHeight = 0; |
1451 | $matches = []; |
1452 | if ( preg_match( '/^(\d+)p/', $transcodeKey, $matches ) ) { |
1453 | $transcodeHeight = (int)$matches[0]; |
1454 | } |
1455 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
1456 | return ( $transcodeHeight <= $config->get( 'TmhPriorityResolutionThreshold' ) ) |
1457 | && ( $file->getLength() <= $config->get( 'TmhPriorityLengthThreshold' ) ); |
1458 | } |
1459 | |
1460 | /** |
1461 | * Return job queue length for the queue that will run this transcode. |
1462 | * @param File $file |
1463 | * @param string $transcodeKey |
1464 | * @return int |
1465 | */ |
1466 | public static function getQueueSize( File $file, $transcodeKey ) { |
1467 | // Warning: this won't treat the prioritized queue separately. |
1468 | $db = $file->repo->getPrimaryDB(); |
1469 | return $db->newSelectQueryBuilder() |
1470 | ->from( 'transcode' ) |
1471 | ->where( [ |
1472 | 'transcode_time_addjob IS NOT NULL', |
1473 | 'transcode_time_startwork IS NULL', |
1474 | 'transcode_time_success IS NULL', |
1475 | 'transcode_time_error IS NULL', |
1476 | ] ) |
1477 | ->caller( __METHOD__ ) |
1478 | ->fetchRowCount(); |
1479 | } |
1480 | |
1481 | /** |
1482 | * Transforms the size per a given "maxSize" |
1483 | * if maxSize is > file, file size is used |
1484 | * @param File $file |
1485 | * @param string $targetMaxSize |
1486 | * @return int[] |
1487 | */ |
1488 | public static function getMaxSizeTransform( $file, $targetMaxSize ) { |
1489 | $maxSize = static::getMaxSize( $targetMaxSize ); |
1490 | $sourceWidth = (int)$file->getWidth(); |
1491 | $sourceHeight = (int)$file->getHeight(); |
1492 | if ( $sourceHeight === 0 ) { |
1493 | // Audio file |
1494 | return [ 0, 0 ]; |
1495 | } |
1496 | $sourceAspect = $sourceWidth / $sourceHeight; |
1497 | $targetWidth = $sourceWidth; |
1498 | $targetHeight = $sourceHeight; |
1499 | if ( $sourceAspect <= $maxSize['aspect'] ) { |
1500 | if ( $sourceHeight > $maxSize['height'] ) { |
1501 | $targetHeight = $maxSize['height']; |
1502 | $targetWidth = (int)( $targetHeight * $sourceAspect ); |
1503 | } |
1504 | } else { |
1505 | if ( $sourceWidth > $maxSize['width'] ) { |
1506 | $targetWidth = $maxSize['width']; |
1507 | $targetHeight = (int)( $targetWidth / $sourceAspect ); |
1508 | // some players do not like uneven frame sizes |
1509 | } |
1510 | } |
1511 | // some players do not like uneven frame sizes |
1512 | $targetWidth += $targetWidth % 2; |
1513 | $targetHeight += $targetHeight % 2; |
1514 | return [ $targetWidth, $targetHeight ]; |
1515 | } |
1516 | |
1517 | /** |
1518 | * Test if a given transcode target is larger than the source file |
1519 | * |
1520 | * @param File &$file File object |
1521 | * @param string $targetMaxSize |
1522 | * @return bool |
1523 | */ |
1524 | public static function isTargetLargerThanFile( &$file, $targetMaxSize ) { |
1525 | $maxSize = static::getMaxSize( $targetMaxSize ); |
1526 | $sourceWidth = $file->getWidth(); |
1527 | $sourceHeight = $file->getHeight(); |
1528 | $sourceAspect = (int)$sourceWidth / (int)$sourceHeight; |
1529 | if ( $sourceAspect <= $maxSize['aspect'] ) { |
1530 | return ( $maxSize['height'] > $sourceHeight ); |
1531 | } |
1532 | return ( $maxSize['width'] > $sourceWidth ); |
1533 | } |
1534 | |
1535 | /** |
1536 | * Is the given transcode key the smallest configured transcode for |
1537 | * its video codec? |
1538 | * @param string $transcodeKey |
1539 | * @return bool |
1540 | * @suppress PhanTypePossiblyInvalidDimOffset |
1541 | */ |
1542 | public static function isSmallestTranscodeForCodec( $transcodeKey ) { |
1543 | $settings = static::$derivativeSettings[$transcodeKey]; |
1544 | $vcodec = $settings['videoCodec']; |
1545 | $maxSize = static::getMaxSize( $settings['maxSize'] ); |
1546 | |
1547 | foreach ( static::enabledVideoTranscodes() as $tKey ) { |
1548 | $tsettings = static::$derivativeSettings[$tKey]; |
1549 | if ( isset( $tsettings['novideo'] ) ) { |
1550 | // This is an audio track for a video streaming set. |
1551 | // Always generate it. |
1552 | return true; |
1553 | } |
1554 | if ( $tsettings['videoCodec'] === $vcodec ) { |
1555 | $tmaxSize = static::getMaxSize( $tsettings['maxSize'] ); |
1556 | if ( $tmaxSize['width'] < $maxSize['width'] ) { |
1557 | return false; |
1558 | } |
1559 | if ( $tmaxSize['height'] < $maxSize['height'] ) { |
1560 | return false; |
1561 | } |
1562 | } |
1563 | } |
1564 | |
1565 | return true; |
1566 | } |
1567 | |
1568 | /** |
1569 | * Return maxSize array for given maxSize setting |
1570 | * |
1571 | * @param string $targetMaxSize |
1572 | * @return array |
1573 | */ |
1574 | public static function getMaxSize( $targetMaxSize ) { |
1575 | $maxSize = []; |
1576 | $targetMaxSize = explode( 'x', $targetMaxSize, 2 ); |
1577 | $maxSize['width'] = (int)$targetMaxSize[0]; |
1578 | if ( count( $targetMaxSize ) === 1 ) { |
1579 | $maxSize['height'] = (int)$targetMaxSize[0]; |
1580 | } else { |
1581 | $maxSize['height'] = (int)$targetMaxSize[1]; |
1582 | } |
1583 | // check for zero size ( audio ) |
1584 | if ( $maxSize['width'] === 0 || $maxSize['height'] === 0 ) { |
1585 | $maxSize['aspect'] = 0; |
1586 | } else { |
1587 | $maxSize['aspect'] = $maxSize['width'] / $maxSize['height']; |
1588 | } |
1589 | return $maxSize; |
1590 | } |
1591 | |
1592 | /** |
1593 | * @param array $set |
1594 | * |
1595 | * @return array |
1596 | */ |
1597 | private static function filterAndSort( array $set ) { |
1598 | $keys = array_keys( array_filter( $set ) ); |
1599 | natsort( $keys ); |
1600 | return $keys; |
1601 | } |
1602 | |
1603 | public static function enabledTranscodes() { |
1604 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
1605 | return static::filterAndSort( array_merge( |
1606 | $config->get( 'EnabledTranscodeSet' ), |
1607 | $config->get( 'EnabledAudioTranscodeSet' ) |
1608 | ) ); |
1609 | } |
1610 | |
1611 | public static function enabledVideoTranscodes() { |
1612 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
1613 | return static::filterAndSort( $config->get( 'EnabledTranscodeSet' ) ); |
1614 | } |
1615 | |
1616 | public static function enabledAudioTranscodes() { |
1617 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
1618 | return static::filterAndSort( $config->get( 'EnabledAudioTranscodeSet' ) ); |
1619 | } |
1620 | |
1621 | public static function validateTranscodeConfiguration() { |
1622 | foreach ( static::enabledTranscodes() as $transcodeKey ) { |
1623 | if ( !isset( static::$derivativeSettings[ $transcodeKey ] ) ) { |
1624 | throw new ConfigException( |
1625 | __METHOD__ . ": Invalid key '$transcodeKey' specified in" |
1626 | . " wgEnabledTranscodeSet or wgEnabledAudioTranscodeSet." |
1627 | ); |
1628 | } |
1629 | } |
1630 | } |
1631 | |
1632 | public static function isBaseMediaFormat( string $extension ): bool { |
1633 | $isos = [ 'mp4', 'm4v', 'm4a', 'mov', '3gp' ]; |
1634 | return in_array( $extension, $isos ); |
1635 | } |
1636 | |
1637 | /** |
1638 | * Expand a bitrate that may have a k/m/g suffix |
1639 | * |
1640 | * @param string|int $rate |
1641 | * @return int |
1642 | */ |
1643 | public static function expandRate( $rate ) { |
1644 | if ( is_int( $rate ) ) { |
1645 | return $rate; |
1646 | } |
1647 | $matches = []; |
1648 | if ( preg_match( '/^(\d+)([kmg])$/', strtolower( $rate ), $matches ) ) { |
1649 | $n = (int)$matches[1]; |
1650 | switch ( $matches[2] ) { |
1651 | case 'g': |
1652 | $n *= 1000; |
1653 | // fall through |
1654 | case 'm': |
1655 | $n *= 1000; |
1656 | // fall through |
1657 | case 'k': |
1658 | $n *= 1000; |
1659 | break; |
1660 | default: |
1661 | throw new LogicException( "Unexpected size suffix: " . $matches[2] ); |
1662 | } |
1663 | return $n; |
1664 | } else { |
1665 | return (int)$rate; |
1666 | } |
1667 | } |
1668 | |
1669 | /** |
1670 | * Remove stray transcode table entries that no longer refer to a living |
1671 | * file. Note this does not remove the backing files, if any. |
1672 | * |
1673 | * @param int $batchSize max number of rows to clean in this batch |
1674 | * @return int number of rows deleted |
1675 | */ |
1676 | public static function cleanupOrphanedTranscodes( int $batchSize ): int { |
1677 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
1678 | $dbw = $lbFactory->getPrimaryDatabase(); |
1679 | $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ ); |
1680 | $ids = $dbw->newSelectQueryBuilder() |
1681 | ->select( 'transcode_id' ) |
1682 | ->from( 'transcode' ) |
1683 | ->leftJoin( 'image', null, [ 'img_name = transcode_image_name' ] ) |
1684 | ->where( [ 'img_name' => null ] ) |
1685 | ->limit( $batchSize ) |
1686 | ->caller( __METHOD__ ) |
1687 | ->fetchFieldValues(); |
1688 | |
1689 | if ( count( $ids ) > 0 ) { |
1690 | $dbw->newDeleteQueryBuilder() |
1691 | ->delete( 'transcode' ) |
1692 | ->where( [ 'transcode_id' => $ids ] ) |
1693 | ->caller( __METHOD__ ) |
1694 | ->execute(); |
1695 | } |
1696 | $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket ); |
1697 | return count( $ids ); |
1698 | } |
1699 | } |