Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 508 |
|
0.00% |
0 / 29 |
CRAP | |
0.00% |
0 / 1 |
WebVideoTranscodeJob | |
0.00% |
0 / 508 |
|
0.00% |
0 / 29 |
22350 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
output | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFile | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getTargetEncodePath | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTargetPlaylistPath | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
fileTarget | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
purgeTargetEncodeFile | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getSourceFilePath | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
setTranscodeError | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 183 |
|
0.00% |
0 / 1 |
1406 | |||
getCommand | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
useScript | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
ffmpegEncode | |
0.00% |
0 / 119 |
|
0.00% |
0 / 1 |
1482 | |||
scaleRate | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
expandRate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
effectiveFrameRate | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
fractionToFloat | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
frameRate | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
ffmpegAddH264VideoOptions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
ffmpegAddMPEG4VideoOptions | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
ffmpegAddGenericVideoOptions | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
ffmpegAddVideoSizeOptions | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
ffmpegAddWebmVideoOptions | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
isInterlaced | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
shouldFrameDouble | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
ffmpegAddDeinterlaceOptions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
ffmpegAddAudioOptions | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
midiToAudioEncode | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | /** |
3 | * Job for transcode jobs |
4 | * |
5 | * @file |
6 | * @ingroup JobQueue |
7 | */ |
8 | |
9 | namespace MediaWiki\TimedMediaHandler\WebVideoTranscode; |
10 | |
11 | use Exception; |
12 | use File; |
13 | use FSFile; |
14 | use InvalidArgumentException; |
15 | use Job; |
16 | use LogicException; |
17 | use MediaWiki\Config\Config; |
18 | use MediaWiki\Deferred\CdnCacheUpdate; |
19 | use MediaWiki\Logger\LoggerFactory; |
20 | use MediaWiki\MainConfigNames; |
21 | use MediaWiki\Shell\CommandFactory; |
22 | use MediaWiki\Shell\Shell; |
23 | use MediaWiki\TimedMediaHandler\HLS\Segmenter; |
24 | use MediaWiki\TimedMediaHandler\TimedMediaHandler; |
25 | use MediaWiki\Title\Title; |
26 | use RepoGroup; |
27 | use Shellbox\Command\BoxedCommand; |
28 | use TempFSFile; |
29 | use Wikimedia\Rdbms\ILBFactory; |
30 | |
31 | /** |
32 | * Job for web video transcode |
33 | * |
34 | * Support two modes |
35 | * 1) non-free media transcode ( delays the media file being inserted, |
36 | * adds note to talk page once ready) |
37 | * 2) derivatives for video ( makes new sources for the asset ) |
38 | * |
39 | * @ingroup JobQueue |
40 | */ |
41 | |
42 | class WebVideoTranscodeJob extends Job { |
43 | |
44 | /** @var TempFSFile|null */ |
45 | public $targetEncodeFile; |
46 | |
47 | /** @var TempFSFile|null */ |
48 | public $targetPlaylistFile; |
49 | |
50 | /** @var string|null|false */ |
51 | public $sourceFilePath; |
52 | |
53 | /** @var File */ |
54 | public $file; |
55 | |
56 | /** @var FSFile|null */ |
57 | public $source; |
58 | |
59 | /** @var FSFile|null */ |
60 | public $remuxSource; |
61 | |
62 | /** @var CommandFactory */ |
63 | private $commandFactory; |
64 | |
65 | /** @var Config */ |
66 | private $config; |
67 | |
68 | /** @var ILBFactory */ |
69 | private $lbFactory; |
70 | |
71 | /** @var RepoGroup */ |
72 | private $repoGroup; |
73 | |
74 | /** |
75 | * @param Title $title |
76 | * @param array $params |
77 | * @param CommandFactory $commandFactory |
78 | * @param Config $config |
79 | * @param ILBFactory $lbFactory |
80 | * @param RepoGroup $repoGroup |
81 | */ |
82 | public function __construct( $title, $params, |
83 | CommandFactory $commandFactory, |
84 | Config $config, |
85 | ILBFactory $lbFactory, |
86 | RepoGroup $repoGroup |
87 | ) { |
88 | if ( isset( $params['prioritized'] ) && $params['prioritized'] ) { |
89 | $command = 'webVideoTranscodePrioritized'; |
90 | } else { |
91 | $command = 'webVideoTranscode'; |
92 | } |
93 | parent::__construct( $command, $title, $params ); |
94 | $this->removeDuplicates = true; |
95 | $this->commandFactory = $commandFactory; |
96 | $this->config = $config; |
97 | $this->lbFactory = $lbFactory; |
98 | $this->repoGroup = $repoGroup; |
99 | } |
100 | |
101 | /** |
102 | * Accessor for MainConfig |
103 | * @return Config |
104 | */ |
105 | protected function getConfig(): Config { |
106 | return $this->config; |
107 | } |
108 | |
109 | /** |
110 | * Wrapper around debug logger |
111 | * @param string $msg |
112 | */ |
113 | private function output( $msg ) { |
114 | LoggerFactory::getInstance( 'WebVideoTranscodeJob' )->debug( $msg ); |
115 | } |
116 | |
117 | /** |
118 | * @return File |
119 | */ |
120 | private function getFile() { |
121 | if ( !$this->file ) { |
122 | $this->file = $this->repoGroup->getLocalRepo() |
123 | ->findFile( $this->title, [ 'latest' => true ] ); |
124 | } |
125 | return $this->file; |
126 | } |
127 | |
128 | /** |
129 | * @return string |
130 | */ |
131 | private function getTargetEncodePath() { |
132 | if ( !$this->targetEncodeFile ) { |
133 | $this->targetEncodeFile = $this->fileTarget(); |
134 | } |
135 | return $this->targetEncodeFile->getPath(); |
136 | } |
137 | |
138 | /** |
139 | * @return string |
140 | */ |
141 | private function getTargetPlaylistPath() { |
142 | if ( !$this->targetPlaylistFile ) { |
143 | $this->targetPlaylistFile = $this->fileTarget( '.m3u8' ); |
144 | } |
145 | return $this->targetPlaylistFile->getPath(); |
146 | } |
147 | |
148 | /** |
149 | * @param string $suffix |
150 | * @return TempFSFile |
151 | */ |
152 | private function fileTarget( $suffix = '' ) { |
153 | $base = $this->getFile(); |
154 | $transcodeKey = $this->params[ 'transcodeKey' ]; |
155 | $file = WebVideoTranscode::getTargetEncodeFile( $base, $transcodeKey, $suffix ); |
156 | if ( !$file ) { |
157 | throw new LogicException( 'Internal state error' ); |
158 | } |
159 | $file->bind( $this ); |
160 | return $file; |
161 | } |
162 | |
163 | /** |
164 | * purge temporary encode target |
165 | */ |
166 | private function purgeTargetEncodeFile() { |
167 | if ( $this->targetEncodeFile ) { |
168 | $this->targetEncodeFile->purge(); |
169 | $this->targetEncodeFile = null; |
170 | } |
171 | if ( $this->targetPlaylistFile ) { |
172 | $this->targetPlaylistFile->purge(); |
173 | $this->targetPlaylistFile = null; |
174 | } |
175 | } |
176 | |
177 | /** |
178 | * @return string|false |
179 | */ |
180 | private function getSourceFilePath() { |
181 | if ( !$this->sourceFilePath ) { |
182 | $file = $this->getFile(); |
183 | $this->source = $file->repo->getLocalReference( $file->getPath() ); |
184 | if ( !$this->source ) { |
185 | $this->sourceFilePath = false; |
186 | } else { |
187 | $this->sourceFilePath = $this->source->getPath(); |
188 | } |
189 | } |
190 | return $this->sourceFilePath; |
191 | } |
192 | |
193 | /** |
194 | * Update the transcode table with failure time and error |
195 | * @param string $transcodeKey |
196 | * @param string $error |
197 | * |
198 | */ |
199 | private function setTranscodeError( $transcodeKey, $error ) { |
200 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
201 | $dbw->newUpdateQueryBuilder() |
202 | ->update( 'transcode' ) |
203 | ->set( [ |
204 | 'transcode_time_error' => $dbw->timestamp(), |
205 | 'transcode_error' => $error |
206 | ] ) |
207 | ->where( [ |
208 | 'transcode_image_name' => $this->getFile()->getName(), |
209 | 'transcode_key' => $transcodeKey |
210 | ] ) |
211 | ->caller( __METHOD__ ) |
212 | ->execute(); |
213 | $this->setLastError( $error ); |
214 | } |
215 | |
216 | /** |
217 | * Run the transcode request |
218 | * @return bool success |
219 | */ |
220 | public function run() { |
221 | $transcodeKey = $this->params['transcodeKey']; |
222 | |
223 | try { |
224 | // get a local pointer to the file |
225 | $file = $this->getFile(); |
226 | |
227 | // Validate the file exists: |
228 | if ( !$file ) { |
229 | $error = $this->title . ': File not found'; |
230 | $this->output( $error ); |
231 | $this->setTranscodeError( $transcodeKey, $error ); |
232 | return false; |
233 | } |
234 | |
235 | // Validate the transcode key param: |
236 | if ( !isset( WebVideoTranscode::$derivativeSettings[ $transcodeKey ] ) ) { |
237 | $error = "Transcode key $transcodeKey not found, skipping"; |
238 | $this->output( $error ); |
239 | $this->setTranscodeError( $transcodeKey, $error ); |
240 | return false; |
241 | } |
242 | |
243 | // Validate the source exists: |
244 | if ( !$this->getSourceFilePath() || !is_file( $this->getSourceFilePath() ) ) { |
245 | $status = $this->title . ': Source not found ' . $this->getSourceFilePath(); |
246 | $this->output( $status ); |
247 | $this->setTranscodeError( $transcodeKey, $status ); |
248 | return false; |
249 | } |
250 | |
251 | $options = WebVideoTranscode::$derivativeSettings[ $transcodeKey ]; |
252 | |
253 | if ( isset( $options[ 'novideo' ] ) ) { |
254 | if ( !isset( $options['audioCodec'] ) ) { |
255 | throw new LogicException( 'Invalid audio track options' ); |
256 | } |
257 | $this->output( "Encoding to audio codec: " . $options['audioCodec'] ); |
258 | } else { |
259 | if ( !isset( $options['videoCodec'] ) ) { |
260 | throw new LogicException( 'Invalid video track options' ); |
261 | } |
262 | $this->output( "Encoding to codec: " . $options['videoCodec'] ); |
263 | } |
264 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
265 | |
266 | // Check if we have "already started" the transcode ( possible error ) |
267 | $dbStartTime = $dbw->newSelectQueryBuilder() |
268 | ->select( 'transcode_time_startwork' ) |
269 | ->from( 'transcode' ) |
270 | ->where( [ |
271 | 'transcode_image_name' => $this->getFile()->getName(), |
272 | 'transcode_key' => $transcodeKey |
273 | ] ) |
274 | ->caller( __METHOD__ ) |
275 | ->fetchField(); |
276 | if ( $dbStartTime !== null ) { |
277 | $error = 'Error, running transcode job, for job that has already started'; |
278 | $this->output( $error ); |
279 | return true; |
280 | } |
281 | |
282 | // Update the transcode table letting it know we have "started work": |
283 | $jobStartTimeCache = wfTimestamp( TS_UNIX ); |
284 | $dbw->newUpdateQueryBuilder() |
285 | ->update( 'transcode' ) |
286 | ->set( [ 'transcode_time_startwork' => $dbw->timestamp( $jobStartTimeCache ) ] ) |
287 | ->where( [ |
288 | 'transcode_image_name' => $this->getFile()->getName(), |
289 | 'transcode_key' => $transcodeKey |
290 | ] ) |
291 | ->caller( __METHOD__ ) |
292 | ->execute(); |
293 | |
294 | // Avoid contention and "server has gone away" errors as |
295 | // the transcode will take a very long time in some cases |
296 | $this->lbFactory->commitPrimaryChanges( __METHOD__ ); |
297 | $this->lbFactory->flushPrimarySessions( __METHOD__ ); |
298 | $this->lbFactory->flushReplicaSnapshots( __METHOD__ ); |
299 | // We can't just leave the connection open either or it will |
300 | // eat up resources and block new connections, so make sure |
301 | // everything is dead and gone. |
302 | $this->lbFactory->closeAll(); |
303 | |
304 | // Check the codec see which encode method to call; |
305 | $streaming = $options['streaming'] ?? false; |
306 | $videoCodec = $options['videoCodec'] ?? ''; |
307 | $codecs = [ 'vp8', 'vp9', 'h264', 'h263', 'mpeg4', 'mjpeg' ]; |
308 | $twopass = isset( $options['twopass'] ); |
309 | |
310 | // Was the _job_ enqueued with the remux option variant? |
311 | $remux = $this->params['remux'] ?? false; |
312 | // Does the _transcode config_ have a list of remux sources? |
313 | $remuxFrom = $options['remuxFrom'] ?? false; |
314 | if ( $remux && $remuxFrom ) { |
315 | foreach ( $remuxFrom as $altKey ) { |
316 | $altSource = WebVideoTranscode::getDerivativeFilePath( $file, $altKey ); |
317 | $repo = $this->file->repo; |
318 | if ( $repo->fileExists( $altSource ) ) { |
319 | $remuxSource = $repo->getLocalReference( $altSource ); |
320 | if ( $remuxSource ) { |
321 | $this->remuxSource = $remuxSource; |
322 | $twopass = false; |
323 | break; |
324 | } |
325 | } |
326 | } |
327 | } |
328 | if ( isset( $options[ 'novideo' ] ) ) { |
329 | if ( $file->getMimeType() === 'audio/midi' ) { |
330 | $status = $this->midiToAudioEncode( $options ); |
331 | } else { |
332 | $status = $this->ffmpegEncode( $options ); |
333 | } |
334 | } elseif ( in_array( $videoCodec, $codecs ) ) { |
335 | // Check for twopass: |
336 | if ( $twopass ) { |
337 | // ffmpeg requires manual two pass |
338 | $status = $this->ffmpegEncode( $options, 2 ); |
339 | } else { |
340 | $status = $this->ffmpegEncode( $options ); |
341 | } |
342 | } else { |
343 | wfDebug( 'Error unknown codec:' . $videoCodec ); |
344 | $status = 'Error unknown target encode codec:' . $videoCodec; |
345 | } |
346 | |
347 | // Reconnect to the database... |
348 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
349 | |
350 | // Do a quick check to confirm the job was not restarted or removed while we were transcoding |
351 | // Confirm that the in memory $jobStartTimeCache matches db start time |
352 | $dbStartTime = $dbw->newSelectQueryBuilder() |
353 | ->select( 'transcode_time_startwork' ) |
354 | ->from( 'transcode' ) |
355 | ->where( [ |
356 | 'transcode_image_name' => $this->getFile()->getName(), |
357 | 'transcode_key' => $transcodeKey |
358 | ] ) |
359 | ->caller( __METHOD__ ) |
360 | ->fetchField(); |
361 | |
362 | // Check for ( hopefully rare ) issue of or job restarted while transcode in progress |
363 | if ( $dbStartTime === null || $jobStartTimeCache !== wfTimestamp( TS_UNIX, $dbStartTime ) ) { |
364 | $this->output( |
365 | 'Possible Error, |
366 | transcode task restarted, removed, or completed while transcode was in progress' |
367 | ); |
368 | // if an error; just error out, |
369 | // we can't remove temp files or update states, because the new job may be doing stuff. |
370 | if ( $status !== true ) { |
371 | $this->setTranscodeError( $transcodeKey, $status ); |
372 | return false; |
373 | } |
374 | // else just continue with db updates, |
375 | // and when the new job comes around it won't start because it will see |
376 | // that the job has already been started. |
377 | } |
378 | |
379 | // If status is ok and target does not exist, reset status |
380 | if ( $status === true && !is_file( $this->getTargetEncodePath() ) ) { |
381 | $status = 'Target does not exist: ' . $this->getTargetEncodePath(); |
382 | } |
383 | |
384 | // If status is ok and target is larger than 0 bytes |
385 | if ( $status === true && filesize( $this->getTargetEncodePath() ) > 0 ) { |
386 | |
387 | $file = $this->getFile(); |
388 | $mediaFilename = WebVideoTranscode::getTranscodeFileBaseName( $file, $transcodeKey ); |
389 | $mediaPath = WebVideoTranscode::getDerivativeFilePath( $file, $transcodeKey ); |
390 | $storeOptions = null; |
391 | $playlistStoreOptions = null; |
392 | |
393 | if ( $streaming === 'hls' ) { |
394 | $playlistKey = $transcodeKey . '.m3u8'; |
395 | $playlistFilename = WebVideoTranscode::getTranscodeFileBaseName( $file, $playlistKey ); |
396 | $playlistPath = WebVideoTranscode::getDerivativeFilePath( $file, $playlistKey ); |
397 | $playlistTemp = $this->getTargetPlaylistPath(); |
398 | |
399 | $segmenter = Segmenter::segment( $this->getTargetEncodePath() ); |
400 | // @fixme put the 10-second segment target in a constant somewhere |
401 | $segmenter->consolidate( 10 ); |
402 | $segmenter->rewrite(); |
403 | $playlist = $segmenter->playlist( 10, $mediaFilename ); |
404 | |
405 | file_put_contents( $playlistTemp, $playlist ); |
406 | $playlistStoreOptions = []; |
407 | $playlistStoreOptions['headers']['Content-Type'] = 'application/vnd.apple.mpegurl; charset=utf-8'; |
408 | } else { |
409 | $playlistTemp = null; |
410 | $playlistPath = null; |
411 | } |
412 | |
413 | if ( |
414 | strpos( $options['type'], '/ogg' ) !== false && |
415 | $file->getLength() |
416 | ) { |
417 | $storeOptions = []; |
418 | // Ogg files need a duration header for firefox |
419 | $storeOptions['headers']['X-Content-Duration'] = (float)$file->getLength(); |
420 | } |
421 | |
422 | // Avoid "server has gone away" errors as copying can be slow |
423 | $this->lbFactory->commitPrimaryChanges( __METHOD__ ); |
424 | $this->lbFactory->flushPrimarySessions( __METHOD__ ); |
425 | $this->lbFactory->flushReplicaSnapshots( __METHOD__ ); |
426 | $this->lbFactory->closeAll(); |
427 | |
428 | // Copy derivative from the FS into storage at $finalDerivativeFilePath |
429 | $result = $file->getRepo()->quickImport( |
430 | // temp file |
431 | $this->getTargetEncodePath(), |
432 | // storage |
433 | $mediaPath, |
434 | $storeOptions |
435 | ); |
436 | if ( $result->isOK() && $streaming === 'hls' && $playlistTemp && $playlistPath ) { |
437 | $result = $file->getRepo()->quickImport( |
438 | // temp file |
439 | $playlistTemp, |
440 | // storage |
441 | $playlistPath, |
442 | $playlistStoreOptions |
443 | ); |
444 | if ( $result->isOK() ) { |
445 | WebVideoTranscode::updateStreamingManifests( $file ); |
446 | } |
447 | } |
448 | |
449 | if ( !$result->isOK() ) { |
450 | // no need to invalidate all pages with video. |
451 | // Because all pages remain valid ( no $transcodeKey derivative ) |
452 | // just clear the file page ( so that the transcode table shows the error ) |
453 | $this->title->invalidateCache(); |
454 | $this->setTranscodeError( $transcodeKey, $result->getWikiText() ); |
455 | $status = false; |
456 | } else { |
457 | $bitrate = round( |
458 | (int)( filesize( $this->getTargetEncodePath() ) / $file->getLength() ) * 8 |
459 | ); |
460 | // Wikimedia\restoreWarnings(); |
461 | // Reconnect to the database... |
462 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
463 | // Update the transcode table with success time: |
464 | $dbw->newUpdateQueryBuilder() |
465 | ->update( 'transcode' ) |
466 | ->set( [ |
467 | 'transcode_error' => '', |
468 | 'transcode_time_error' => null, |
469 | 'transcode_time_success' => $dbw->timestamp(), |
470 | 'transcode_final_bitrate' => $bitrate |
471 | ] ) |
472 | ->where( [ |
473 | 'transcode_image_name' => $this->getFile()->getName(), |
474 | 'transcode_key' => $transcodeKey, |
475 | ] ) |
476 | ->caller( __METHOD__ ) |
477 | ->execute(); |
478 | // Commit to reduce contention |
479 | $dbw->commit( __METHOD__, 'flush' ); |
480 | WebVideoTranscode::invalidatePagesWithFile( $this->title ); |
481 | } |
482 | } else { |
483 | // Update the transcode table with failure time and error |
484 | $this->setTranscodeError( $transcodeKey, $status ); |
485 | // no need to invalidate all pages with video. |
486 | // Because all pages remain valid ( no $transcodeKey derivative ) |
487 | // just clear the file page ( so that the transcode table shows the error ) |
488 | $this->title->invalidateCache(); |
489 | } |
490 | // done with encoding target, clean up |
491 | $this->purgeTargetEncodeFile(); |
492 | |
493 | // Clear the webVideoTranscode cache ( so we don't keep out dated table cache around ) |
494 | WebVideoTranscode::clearTranscodeCache( $this->title->getDBkey() ); |
495 | |
496 | $url = WebVideoTranscode::getTranscodedUrlForFile( $file, $transcodeKey ); |
497 | $urls = [ $url ]; |
498 | if ( $streaming === 'hls' ) { |
499 | $urls[] = "$url.m3u8"; |
500 | } |
501 | $update = new CdnCacheUpdate( $urls ); |
502 | $update->doUpdate(); |
503 | |
504 | if ( $status !== true ) { |
505 | $this->setLastError( $status ); |
506 | } |
507 | return $status === true; |
508 | } catch ( Exception $e ) { |
509 | $error = "Exception: " . $e->getMessage(); |
510 | $trace = $e->getTraceAsString(); |
511 | $this->output( "$error\n$trace\n" ); |
512 | $this->setTranscodeError( $transcodeKey, $error ); |
513 | return false; |
514 | } |
515 | } |
516 | |
517 | /** |
518 | * Gets a boxedCommand executor |
519 | * @param string $name The route name for the BoxedCommand |
520 | * @return BoxedCommand |
521 | */ |
522 | private function getCommand( string $name ) { |
523 | $fullName = 'tmh-' . strtolower( $name ); |
524 | return $this->commandFactory |
525 | ->createBoxed( 'timedmediahandler' ) |
526 | ->disableNetwork() |
527 | ->firejailDefaultSeccomp() |
528 | ->routeName( $fullName ); |
529 | } |
530 | |
531 | /** |
532 | * Adds an input file from the scripts directory, sets the command to execute it |
533 | * @param BoxedCommand $command |
534 | * @param string $script |
535 | * |
536 | */ |
537 | private function useScript( BoxedCommand $command, string $script ) { |
538 | $file = __DIR__ . "/../../scripts/$script"; |
539 | if ( !is_file( $file ) ) { |
540 | throw new InvalidArgumentException( "File '$file' not found" ); |
541 | } |
542 | $command->inputFileFromFile( "scripts/$script", $file ) |
543 | ->params( $this->config->get( MainConfigNames::ShellboxShell ), 'scripts/' . $script ); |
544 | } |
545 | |
546 | /** |
547 | * Utility helper for ffmpeg mapping |
548 | * @param array $options |
549 | * @param int $passes the number of encoding passes to perform |
550 | * @return true|string |
551 | */ |
552 | private function ffmpegEncode( $options, $passes = 0 ) { |
553 | if ( !is_file( $this->getSourceFilePath() ) ) { |
554 | return "source file is missing, " . $this->getSourceFilePath() . ". Encoding failed."; |
555 | } |
556 | // Environment variables for shellbox |
557 | $optsEnv = []; |
558 | if ( $this->remuxSource ) { |
559 | $sourcePath = $this->remuxSource->getPath(); |
560 | } else { |
561 | $sourcePath = $this->getSourceFilePath(); |
562 | } |
563 | |
564 | $interval = 10; |
565 | $fps = 0; |
566 | // Set up all the video-related options |
567 | if ( isset( $options['novideo'] ) ) { |
568 | $optsEnv['TMH_OPTS_VIDEO'] = '-vn'; |
569 | } else { |
570 | $optsEnv['TMH_OPTS_VIDEO'] = ""; |
571 | $fps = $this->effectiveFrameRate( $options ); |
572 | if ( isset( $options['framerate'] ) ) { |
573 | // $options['framerate'] is a float |
574 | $optsEnv['TMH_OPTS_VIDEO'] .= '-r ' . strval( $options['framerate'] ); |
575 | } else { |
576 | // Note -fpsmax is not available on Wikimedia's Debian as of 2023-02-02 |
577 | // |
578 | // $cmd .= " -fpsmax " . wfEscapeShellArg( $options['fpsmax'] ); |
579 | // $cmd .= " -fpsmax " . self::MAX_FPS; |
580 | // |
581 | // Instead, manually check the detected framerate. |
582 | // Note some files report incorrectly via GetID3, and may |
583 | // end up actually increasing in frame rate because of this! |
584 | $orig = $this->frameRate(); |
585 | if ( $this->isInterlaced() ) { |
586 | $orig *= 2; |
587 | } |
588 | if ( $orig > $fps ) { |
589 | $optsEnv['TMH_OPTS_VIDEO'] .= '-r ' . strval( $fps ); |
590 | } |
591 | } |
592 | |
593 | if ( $this->remuxSource ) { |
594 | $optsEnv['TMH_OPTS_VIDEO'] .= ' -vcodec copy'; |
595 | $optsEnv['TMH_REMUX'] = "yes"; |
596 | } else { |
597 | $optsEnv['TMH_REMUX'] = "no"; |
598 | $optsEnv['TMH_OPT_VIDEOCODEC'] = $options['videoCodec']; |
599 | switch ( $options['videoCodec'] ) { |
600 | case 'vp8': |
601 | case 'vp9': |
602 | $optsEnv['TMH_OPTS_VIDEO'] .= $this->ffmpegAddWebmVideoOptions( $options ); |
603 | if ( isset( $options['speed'] ) ) { |
604 | $optsEnv['TMH_OPT_SPEED'] = (string)intval( $options['speed'] ); |
605 | } |
606 | break; |
607 | case 'h264': |
608 | $optsEnv['TMH_OPTS_VIDEO'] .= $this->ffmpegAddH264VideoOptions( $options ); |
609 | break; |
610 | case 'mpeg4': |
611 | $optsEnv['TMH_OPTS_VIDEO'] .= $this->ffmpegAddMPEG4VideoOptions( $options ); |
612 | break; |
613 | default: |
614 | $optsEnv['TMH_OPTS_VIDEO'] .= $this->ffmpegAddGenericVideoOptions( $options ); |
615 | } |
616 | } |
617 | |
618 | // needed for 2-pass & streaming to override file type detection |
619 | if ( $options['videoCodec'] === 'h264' || |
620 | $options['videoCodec'] === 'mpeg4' || |
621 | isset( $options['streaming'] ) ) { |
622 | $optsEnv['TMH_OPTS_VIDEO'] .= ' -f mp4'; |
623 | } elseif ( $options['videoCodec'] === 'vp8' || |
624 | $options['videoCodec'] === 'vp9' ) { |
625 | $optsEnv['TMH_OPTS_VIDEO'] .= ' -f webm'; |
626 | } |
627 | |
628 | // Check for keyframeInterval |
629 | $keyframeInterval = round( $fps * $interval ); |
630 | $optsEnv['TMH_OPTS_VIDEO'] .= ' -g ' . strval( $keyframeInterval ); |
631 | |
632 | if ( isset( $options['videoBitrate'] ) ) { |
633 | $base = $this->expandRate( $options['videoBitrate'] ); |
634 | $bitrate = $this->scaleRate( $options, $base ); |
635 | $optsEnv['TMH_OPTS_VIDEO'] .= " -b:v $bitrate"; |
636 | |
637 | // Estimate the output file size in KiB and bail out early |
638 | // if it's potentially very large. Could be a denial of |
639 | // service, or just a large file that probably is poorly |
640 | // compressed. |
641 | $duration = (float)$this->file->getLength(); |
642 | $estimatedSize = round( ( $bitrate / 8 ) * $duration / 1024 ); |
643 | $backgroundSizeLimit = $this->config->get( 'TranscodeBackgroundSizeLimit' ); |
644 | if ( $backgroundSizeLimit > 0 && $estimatedSize > $backgroundSizeLimit ) { |
645 | // This hard limit cannot be overridden by admins, except by raising the limit in config. |
646 | // @todo return an error code that can be localized later |
647 | return "estimated file size $estimatedSize KiB over hard limit $backgroundSizeLimit KiB"; |
648 | } |
649 | |
650 | $transcodeSoftSizeLimit = $this->config->get( 'TranscodeSoftSizeLimit' ); |
651 | if ( $transcodeSoftSizeLimit > 0 && $estimatedSize > $transcodeSoftSizeLimit ) { |
652 | // This soft limit can be overridden when a transcode is reset by hand via the web UI |
653 | // or API, or requeueTranscodes.php with --manual-override option. |
654 | $manualOverride = $this->params['manualOverride'] ?? false; |
655 | if ( !$manualOverride ) { |
656 | // @todo return an error code that can be localized later |
657 | return "estimated file size $estimatedSize KiB over soft limit $transcodeSoftSizeLimit KiB"; |
658 | } |
659 | } |
660 | |
661 | if ( isset( $options['minrate'] ) ) { |
662 | $minrate = $this->scaleRate( $options, $options['minrate'] ); |
663 | $optsEnv['TMH_OPTS_VIDEO'] .= " -minrate $minrate"; |
664 | } |
665 | if ( isset( $options['maxrate'] ) ) { |
666 | $maxrate = $this->scaleRate( $options, $options['maxrate'] ); |
667 | $optsEnv['TMH_OPTS_VIDEO'] .= " -maxrate $maxrate"; |
668 | } |
669 | } |
670 | |
671 | if ( !$this->remuxSource ) { |
672 | // If necessary, add deinterlacing options |
673 | $optsEnv['TMH_OPTS_VIDEO'] .= $this->ffmpegAddDeinterlaceOptions( $options ); |
674 | // Add size options: |
675 | $optsEnv['TMH_OPTS_VIDEO'] .= $this->ffmpegAddVideoSizeOptions( $options ); |
676 | } |
677 | } |
678 | |
679 | if ( !$this->config->get( 'UseFFmpeg2' ) ) { |
680 | // Work around https://trac.ffmpeg.org/ticket/6375 in ffmpeg 3.4/4.0 |
681 | // Sometimes caused transcode failures saying things like: |
682 | // "1 frames left in the queue on closing" |
683 | $optsEnv['TMH_OPTS_FFMPEG2'] = '-max_muxing_queue_size 1024'; |
684 | } else { |
685 | $optsEnv['TMH_OPTS_FFMPEG2'] = ''; |
686 | } |
687 | |
688 | // Audio options |
689 | $optsEnv['TMH_OPT_NOAUDIO'] = isset( $options['noaudio'] ) ? "yes" : "no"; |
690 | $optsEnv['TMH_OPTS_AUDIO'] = $this->ffmpegAddAudioOptions( $options ); |
691 | |
692 | $streaming = $options['streaming'] ?? false; |
693 | $transcodeKey = $this->params[ 'transcodeKey' ]; |
694 | $extension = substr( $transcodeKey, strrpos( $transcodeKey, '.' ) + 1 ); |
695 | |
696 | if ( WebVideoTranscode::isBaseMediaFormat( $extension ) ) { |
697 | $optsEnv['TMH_MOVFLAGS'] = '-movflags +faststart'; |
698 | } |
699 | |
700 | if ( $streaming === 'hls' ) { |
701 | if ( WebVideoTranscode::isBaseMediaFormat( $extension ) ) { |
702 | if ( !isset( $optsEnv['TMH_MOVFLAGS'] ) ) { |
703 | $optsEnv['TMH_MOVFLAGS'] = ''; |
704 | } |
705 | // Don't use the HLS muxer, as it'll want to manage |
706 | // filenames and we have to rewrite everything anyway. |
707 | // We'll generate an .m3u8 from the file structure after. |
708 | |
709 | if ( isset( $options['novideo'] ) || isset( $options['intraframe'] ) ) { |
710 | // Audio-only tracks should be fragmented around the standard interval. |
711 | // Intraframe-only codecs like Motion-JPEG should also be treated this way. |
712 | $optsEnv['TMH_MOVFLAGS'] .= " -movflags +empty_moov+default_base_moof"; |
713 | $optsEnv['TMH_MOVFLAGS'] .= " -frag_duration {$interval}000000"; |
714 | } else { |
715 | // Video keyframe interval is set to approximate the desired interval, but |
716 | // they may occur whenever the encoder thinks they would be desirable such |
717 | // as a visible scene change. |
718 | $optsEnv['TMH_MOVFLAGS'] .= " -movflags +frag_keyframe+empty_moov+default_base_moof"; |
719 | } |
720 | |
721 | // This is needed for opus on debian bullseye |
722 | $optsEnv['TMH_MOVFLAGS'] .= " -strict experimental"; |
723 | } elseif ( $extension === 'mp3' ) { |
724 | // No additional options needed at present. |
725 | } else { |
726 | return "Invalid HLS track media type, expected .mp4, .m4v, .m4a, .mov, .3gp, or .mp3"; |
727 | } |
728 | } |
729 | |
730 | $cmd = $this->getCommand( 'ffmpegencode' ); |
731 | $this->useScript( $cmd, 'ffmpeg-encode.sh' ); |
732 | // set up options that don't need mangling |
733 | |
734 | $backgroundMemoryLimit = $this->config->get( 'TranscodeBackgroundMemoryLimit' ) * 1024; |
735 | $wallTimeLimit = (int)$this->config->get( 'TranscodeBackgroundTimeLimit' ); |
736 | $cpuTimeLimit = (int)$this->config->get( 'FFmpegThreads' ) * $wallTimeLimit; |
737 | // cast to string to make phan happy |
738 | $ffmpegLocation = (string)$this->config->get( 'FFmpegLocation' ); |
739 | // Create an output file name with the correct extension |
740 | $target = $this->getTargetEncodePath(); |
741 | $outputFile = 'transcoded.' . pathinfo( $target, PATHINFO_EXTENSION ); |
742 | // Execute the conversion |
743 | $cmd->outputFileToFile( $outputFile, $this->getTargetEncodePath() ) |
744 | ->inputFileFromFile( 'original.video', $sourcePath ) |
745 | ->includeStderr() |
746 | ->environment( [ |
747 | 'TMH_OUTPUT_FILE' => $outputFile, |
748 | 'TMH_FFMPEG_PASSES' => strval( $passes ), |
749 | 'TMH_FFMPEG_PATH' => $ffmpegLocation, |
750 | ] + $optsEnv ); |
751 | $result = $cmd->memoryLimit( $backgroundMemoryLimit ) |
752 | ->wallTimeLimit( $wallTimeLimit ) |
753 | ->cpuTimeLimit( $cpuTimeLimit ) |
754 | ->execute(); |
755 | |
756 | // and pass it to this->output() |
757 | if ( $result->getExitCode() != 0 ) { |
758 | $host = wfHostname(); |
759 | return 'ffmpeg-encode.sh' . |
760 | "\n\nExitcode: " . $result->getExitCode() . |
761 | "\nMemory: $backgroundMemoryLimit" . |
762 | "\nHost: $host\n\n" |
763 | . $result->getStdout(); |
764 | } |
765 | |
766 | return true; |
767 | } |
768 | |
769 | // Bitrates and keyframe distances are specified for this |
770 | // common frame rate (30), and scaled accordingly to accomodate |
771 | // higher frame rates. |
772 | private const DEFAULT_FPS = 30; |
773 | private const MAX_FPS = 60; |
774 | private const MIN_FPS = 24; |
775 | |
776 | /** |
777 | * Scale a bitrate or frame count according to the frame rate |
778 | * of the file versus the default frame rate. This is not a |
779 | * straight linear multiplication; it's biased to reduce impact |
780 | * beyond 30 fps, to 1.5x base at 60 fps. |
781 | * |
782 | * @param array $options |
783 | * @param string|int $rate |
784 | * @return int |
785 | */ |
786 | private function scaleRate( $options, $rate ) { |
787 | $fps = $this->effectiveFrameRate( $options ); |
788 | $base = $this->expandRate( $rate ); |
789 | |
790 | $lofps = min( $fps, self::DEFAULT_FPS ); |
791 | $hifps = $fps - $lofps; |
792 | $scaled = $base * $lofps / self::DEFAULT_FPS + |
793 | 0.5 * $base * $hifps / self::DEFAULT_FPS; |
794 | return (int)$scaled; |
795 | } |
796 | |
797 | /** |
798 | * Expand a bitrate that may have a k/m/g suffix |
799 | * |
800 | * @param string|int $rate |
801 | * @return int |
802 | */ |
803 | private function expandRate( $rate ) { |
804 | return WebVideoTranscode::expandRate( $rate ); |
805 | } |
806 | |
807 | /** |
808 | * Grab the frame rate from the file, bounded by |
809 | * format-specific or generic limitations. |
810 | * Suitable for scaling linear parameters like the |
811 | * target bit rate. |
812 | * |
813 | * @param array $options |
814 | * @return float |
815 | */ |
816 | private function effectiveFrameRate( $options ) { |
817 | if ( isset( $options['framerate'] ) ) { |
818 | // fixed framerate |
819 | $fps = $this->fractionToFloat( $options['framerate'] ); |
820 | } else { |
821 | // @todo getid3 gets this wrong on some WebM input files |
822 | // consider reading from ffmpeg or ffprobe... |
823 | // We cap it, but this can cause a 29.97fps file to use |
824 | // the 60fps bitrate. Worst case it's a bloated file. |
825 | $fps = $this->frameRate(); |
826 | } |
827 | if ( $this->shouldFrameDouble( $options ) ) { |
828 | $fps *= 2; |
829 | } |
830 | |
831 | if ( $fps < self::MIN_FPS ) { |
832 | return self::MIN_FPS; |
833 | } |
834 | if ( isset( $options['fpsmax'] ) ) { |
835 | $max = $this->fractionToFloat( $options['fpsmax'] ); |
836 | } else { |
837 | $max = self::MAX_FPS; |
838 | } |
839 | if ( $fps > $max ) { |
840 | return $max; |
841 | } |
842 | return $fps; |
843 | } |
844 | |
845 | /** |
846 | * @param string $str |
847 | * @return float |
848 | */ |
849 | private function fractionToFloat( $str ) { |
850 | $fraction = explode( '/', $str, 2 ); |
851 | if ( count( $fraction ) > 1 ) { |
852 | return (float)$fraction[0] / (float)$fraction[1]; |
853 | } |
854 | return (float)$str; |
855 | } |
856 | |
857 | /** |
858 | * Return the actual frame rate of the file, or the default |
859 | * if can't retrieve it. |
860 | * |
861 | * @return float |
862 | */ |
863 | private function frameRate() { |
864 | $file = $this->getFile(); |
865 | $handler = $file->getHandler(); |
866 | if ( $handler instanceof TimedMediaHandler ) { |
867 | $fps = $handler->getFrameRate( $file ); |
868 | if ( $fps ) { |
869 | return $fps; |
870 | } |
871 | } |
872 | return self::DEFAULT_FPS; |
873 | } |
874 | |
875 | /** |
876 | * Adds ffmpeg shell options for h264 |
877 | * |
878 | * @param array $options |
879 | * @return string |
880 | */ |
881 | public function ffmpegAddH264VideoOptions( $options ) { |
882 | // Set the codec: |
883 | $cmd = " -threads " . (int)$this->config->get( 'FFmpegThreads' ) . " -vcodec libx264"; |
884 | $cmd .= ' -pix_fmt yuv420p'; |
885 | $cmd .= ' -rc-lookahead 16'; |
886 | |
887 | return $cmd; |
888 | } |
889 | |
890 | /** |
891 | * Adds ffmpeg shell options for h264 |
892 | * |
893 | * @param array $options |
894 | * @return string |
895 | */ |
896 | public function ffmpegAddMPEG4VideoOptions( $options ) { |
897 | $cmd = " -vcodec mpeg4"; |
898 | |
899 | // Force to 4:2:0 chroma subsampling. |
900 | $cmd .= ' -pix_fmt yuv420p'; |
901 | |
902 | return $cmd; |
903 | } |
904 | |
905 | /** |
906 | * @param array $options |
907 | * @return string |
908 | */ |
909 | private function ffmpegAddGenericVideoOptions( $options ) { |
910 | $cmd = ' -vcodec ' . $options['videoCodec']; |
911 | |
912 | // Force to 4:2:0 chroma subsampling. |
913 | $cmd .= ' -pix_fmt yuv420p'; |
914 | |
915 | return $cmd; |
916 | } |
917 | |
918 | /** |
919 | * @param array $options |
920 | * |
921 | * @return string |
922 | */ |
923 | private function ffmpegAddVideoSizeOptions( $options ) { |
924 | $cmd = ''; |
925 | // Get a local pointer to the file object |
926 | $file = $this->getFile(); |
927 | |
928 | // Check for aspect ratio |
929 | $aspectRatio = $options['aspect'] ?? $file->getWidth() . ':' . $file->getHeight(); |
930 | if ( ( isset( $options['width'] ) && $options['width'] > 0 ) |
931 | && |
932 | ( isset( $options['height'] ) && $options['height'] > 0 ) |
933 | ) { |
934 | $cmd .= ' -s ' . (int)$options['width'] . 'x' . (int)$options['height']; |
935 | $cmd .= ' -aspect ' . $aspectRatio; |
936 | } elseif ( isset( $options['maxSize'] ) ) { |
937 | // Get size transform ( if maxSize is > file, file size is used: |
938 | |
939 | [ $width, $height ] = WebVideoTranscode::getMaxSizeTransform( $file, $options['maxSize'] ); |
940 | $cmd .= ' -s ' . (int)$width . 'x' . (int)$height; |
941 | } |
942 | return $cmd; |
943 | } |
944 | |
945 | /** |
946 | * Adds ffmpeg shell options for webm |
947 | * |
948 | * @param array $options |
949 | * @return string |
950 | */ |
951 | private function ffmpegAddWebmVideoOptions( $options ) { |
952 | $cmd = ' -threads ' . (int)$this->config->get( 'FFmpegThreads' ); |
953 | if ( $this->config->get( 'FFmpegVP9RowMT' ) && $options['videoCodec'] === 'vp9' ) { |
954 | // Macroblock row multithreading allows using more CPU cores |
955 | // for VP9 encoding. This is not yet the default, and the option |
956 | // will fail on a version of ffmpeg that is too old or is built |
957 | // against a libvpx that is too old, so we have to enable it |
958 | // conditionally for now. |
959 | // |
960 | // Requires libvpx 1.7 and ffmpeg 3.3. |
961 | $cmd .= ' -row-mt 1'; |
962 | } |
963 | |
964 | // Force to 4:2:0 chroma subsampling. Others are supported in Theora |
965 | // and in VP9 profile 1, but Chrome and Edge don't grok them. |
966 | $cmd .= ' -pix_fmt yuv420p'; |
967 | |
968 | // libvpx-specific constant quality or constrained quality |
969 | // note the range is different between VP8 and VP9 |
970 | // Also an integer. |
971 | if ( isset( $options['crf'] ) ) { |
972 | $cmd .= " -crf " . (string)intval( $options['crf'] ); |
973 | } |
974 | |
975 | // Set the codec: |
976 | if ( $options['videoCodec'] === 'vp9' ) { |
977 | $cmd .= " -vcodec libvpx-vp9"; |
978 | if ( isset( $options['tileColumns'] ) ) { |
979 | $cmd .= ' -tile-columns ' . (string)intval( $options['tileColumns'] ); |
980 | } |
981 | } else { |
982 | $cmd .= " -vcodec libvpx"; |
983 | if ( isset( $options['slices'] ) ) { |
984 | $cmd .= ' -slices ' . (string)intval( $options['slices'] ); |
985 | } |
986 | } |
987 | |
988 | $cmd .= ' -quality good'; |
989 | return $cmd; |
990 | } |
991 | |
992 | /** |
993 | * @return bool |
994 | */ |
995 | private function isInterlaced() { |
996 | $handler = $this->file->getHandler(); |
997 | return ( $handler instanceof TimedMediaHandler && $handler->isInterlaced( $this->file ) ); |
998 | } |
999 | |
1000 | /** |
1001 | * Whether to produce one frame per field when deinterlacing. |
1002 | * This will double the output frame rate. |
1003 | * |
1004 | * @param array $options |
1005 | * @return bool |
1006 | */ |
1007 | private function shouldFrameDouble( $options ) { |
1008 | if ( $this->isInterlaced() ) { |
1009 | if ( isset( $options['framerate'] ) ) { |
1010 | // Fixed framerate, don't mess with it. |
1011 | return false; |
1012 | } |
1013 | if ( isset( $options['fpsmax'] ) && $this->fractionToFloat( $options['fpsmax'] ) < 60 ) { |
1014 | return false; |
1015 | } |
1016 | return true; |
1017 | } |
1018 | return false; |
1019 | } |
1020 | |
1021 | /** |
1022 | * @param array $options |
1023 | * @return string |
1024 | */ |
1025 | private function ffmpegAddDeinterlaceOptions( $options ) { |
1026 | if ( $this->isInterlaced() ) { |
1027 | if ( $this->shouldFrameDouble( $options ) ) { |
1028 | // Send one frame per field for full motion smoothness. |
1029 | return ' -vf yadif=1'; |
1030 | } |
1031 | // Send one frame per field |
1032 | return ' -vf yadif=0'; |
1033 | } |
1034 | return ''; |
1035 | } |
1036 | |
1037 | /** |
1038 | * @param array $options |
1039 | * @return string |
1040 | */ |
1041 | private function ffmpegAddAudioOptions( $options ) { |
1042 | $cmd = ''; |
1043 | if ( isset( $options['audioQuality'] ) ) { |
1044 | $cmd .= " -aq " . (string)intval( $options['audioQuality'] ); |
1045 | } |
1046 | if ( isset( $options['audioBitrate'] ) ) { |
1047 | $cmd .= " -ab " . $this->expandRate( $options['audioBitrate'] ); |
1048 | } |
1049 | if ( isset( $options['samplerate'] ) ) { |
1050 | $cmd .= " -ar " . (string)intval( $options['samplerate'] ); |
1051 | } |
1052 | if ( isset( $options['channels'] ) ) { |
1053 | $cmd .= " -ac " . (string)intval( $options['channels'] ); |
1054 | } |
1055 | |
1056 | if ( isset( $options['audioCodec'] ) ) { |
1057 | $encoders = [ |
1058 | 'vorbis' => 'libvorbis', |
1059 | 'opus' => 'libopus', |
1060 | 'mp3' => 'libmp3lame', |
1061 | ]; |
1062 | $codec = $encoders[$options['audioCodec']] ?? $options['audioCodec']; |
1063 | $cmd .= " -acodec " . $codec; |
1064 | if ( $codec === 'aac' ) { |
1065 | // the aac encoder is currently "experimental" in libav 9? :P |
1066 | $cmd .= ' -strict experimental'; |
1067 | } |
1068 | } else { |
1069 | // if no audio codec set use vorbis : |
1070 | $cmd .= " -acodec libvorbis "; |
1071 | } |
1072 | return $cmd; |
1073 | } |
1074 | |
1075 | /** |
1076 | * Utility helper for midi to an audio format conversion |
1077 | * @param array $options |
1078 | * @return true|string |
1079 | */ |
1080 | private function midiToAudioEncode( $options ) { |
1081 | if ( !is_file( $this->getSourceFilePath() ) ) { |
1082 | return "source file is missing, " . $this->getSourceFilePath() . ". Encoding failed."; |
1083 | } |
1084 | $cmd = $this->getCommand( 'miditoaudio' ); |
1085 | $this->useScript( $cmd, 'midi-encode.sh' ); |
1086 | // set up options |
1087 | $optsEnv = []; |
1088 | $optToEnv = [ |
1089 | 'audioQuality' => 'QUALITY', |
1090 | 'audioBitrate' => 'BITRATE', |
1091 | 'samplerate' => 'SAMPLERATE', |
1092 | 'channels' => 'CHANNELS' |
1093 | ]; |
1094 | foreach ( $optToEnv as $opt => $label ) { |
1095 | # Here we're passing the argument directly to the shell, so we want it escaped |
1096 | if ( isset( $options[$opt] ) ) { |
1097 | $optsEnv['TMH_OPT_' . $label] = Shell::escape( $options[$opt] ); |
1098 | } |
1099 | } |
1100 | $outputFile = 'output_audio.' . pathinfo( $this->getTargetEncodePath(), PATHINFO_EXTENSION ); |
1101 | // Execute the conversion |
1102 | $backgroundMemoryLimit = $this->config->get( 'TranscodeBackgroundMemoryLimit' ) * 1024; |
1103 | $wallTimeLimit = (int)$this->config->get( 'TranscodeBackgroundTimeLimit' ); |
1104 | $cpuTimeLimit = (int)$this->config->get( 'FFmpegThreads' ) * $wallTimeLimit; |
1105 | $cmd->outputFileToFile( $outputFile, $this->getTargetEncodePath() ) |
1106 | ->inputFileFromFile( 'input.mid', $this->getSourceFilePath() ) |
1107 | ->includeStderr() |
1108 | ->environment( [ |
1109 | 'TMH_FLUIDSYNTH_PATH' => $this->config->get( 'TmhFluidsynthLocation' ), |
1110 | 'TMH_FFMPEG_PATH' => $this->config->get( 'FFmpegLocation' ), |
1111 | 'TMH_SOUNDFONT_PATH' => $this->config->get( 'TmhSoundfontLocation' ), |
1112 | 'TMH_AUDIO_CODEC' => $options['audioCodec'], |
1113 | 'TMH_OUTPUT_FILE' => $outputFile, |
1114 | ] + $optToEnv ); |
1115 | $result = $cmd->memoryLimit( $backgroundMemoryLimit ) |
1116 | ->wallTimeLimit( $wallTimeLimit ) |
1117 | ->cpuTimeLimit( $cpuTimeLimit ) |
1118 | ->execute(); |
1119 | |
1120 | if ( $result->getExitCode() != 0 ) { |
1121 | return 'midi-encode.sh' . |
1122 | "\n\nExitcode: " . $result->getExitCode() . "\nMemory: $backgroundMemoryLimit\n\n" |
1123 | . $result->getStdout(); |
1124 | } |
1125 | return true; |
1126 | } |
1127 | } |