diff --git a/extension.json b/extension.json
index 20747cd9..23fc0a38 100644
--- a/extension.json
+++ b/extension.json
@@ -1,544 +1,552 @@
{
"name": "TimedMediaHandler",
"namemsg": "timedmediahandler-extensionname",
"version": "0.6.0",
"author": [
"Michael Dale",
"Jan Gerber",
"Derk-Jan Hartman",
"Brion Vibber",
"Tim Starling",
"..."
],
"url": "https://www.mediawiki.org/wiki/Extension:TimedMediaHandler",
"descriptionmsg": "timedmediahandler-desc",
"license-name": "GPL-2.0-or-later",
"type": "media",
"requires": {
"MediaWiki": ">= 1.38.0"
},
"APIModules": {
"timedtext": {
"class": "MediaWiki\\TimedMediaHandler\\ApiTimedText",
"services": [
"LanguageNameUtils",
"RepoGroup",
"MainWANObjectCache",
"WikiPageFactory"
]
},
"transcodereset": "MediaWiki\\TimedMediaHandler\\ApiTranscodeReset"
},
"APIPropModules": {
"videoinfo": "MediaWiki\\TimedMediaHandler\\ApiQueryVideoInfo",
"transcodestatus": "MediaWiki\\TimedMediaHandler\\ApiTranscodeStatus"
},
"AvailableRights": [
"transcode-reset",
"transcode-status"
],
"ExtensionFunctions": [
"MediaWiki\\TimedMediaHandler\\Hooks::register"
],
"GroupPermissions": {
"sysop": {
"transcode-reset": true,
"transcode-status": true
},
"autoconfirmed": {
"transcode-reset": true,
"transcode-status": true
}
},
"GrantPermissions": {
"uploadeditmovefile": {
"transcode-reset": true
}
},
"LogActionsHandlers": {
"timedmediahandler/resettranscode": "LogFormatter"
},
"LogTypes": [
"timedmediahandler"
],
"SpecialPages": {
"OrphanedTimedText": "MediaWiki\\TimedMediaHandler\\SpecialOrphanedTimedText",
"TranscodeStatistics": "MediaWiki\\TimedMediaHandler\\SpecialTranscodeStatistics"
},
"MessagesDirs": {
"TimedMediaHandler": [
"i18n",
"i18n/api"
]
},
"ExtensionMessagesFiles": {
"TimedMediaHandlerMagic": "i18n/TimedMediaHandler.i18n.magic.php",
"TimedMediaHandlerAliases": "i18n/TimedMediaHandler.i18n.alias.php"
},
"AutoloadClasses": {
"File_Ogg": "includes/Handlers/OggHandler/File_Ogg/File/Ogg.php",
"File_Ogg_Bitstream": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Bitstream.php",
"File_Ogg_Flac": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Flac.php",
"File_Ogg_Media": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Media.php",
"File_Ogg_Opus": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Opus.php",
"File_Ogg_Speex": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Speex.php",
"File_Ogg_Theora": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Theora.php",
"File_Ogg_Vorbis": "includes/Handlers/OggHandler/File_Ogg/File/Ogg/Vorbis.php",
"WebVideoTranscodeJob": "includes/WebVideoTranscode/WebVideoTranscodeJob.php",
"MediaWiki\\TimedMediaHandler\\WebVideoTranscode\\WebVideoTranscodeJob": "includes/WebVideoTranscode/WebVideoTranscodeJob.php"
},
"AutoloadNamespaces": {
"MediaWiki\\TimedMediaHandler\\": "includes/"
},
"TestAutoloadClasses": {
"ApiVideoUploadTestCase": "tests/phpunit/ApiVideoUploadTestCase.php",
"MockOggHandler": "tests/phpunit/mocks/MockOggHandler.php"
},
"Hooks": {
"ArticleContentOnDiff": "main",
"ArticleFromTitle": "main",
"ArticlePurge": "main",
"BeforePageDisplay": "main",
"CanonicalNamespaces": "main",
"FileDeleteComplete": "main",
"FileUpload": "main",
"ImageOpenShowImageInlineBefore": "main",
"ImagePageAfterImageLinks": "main",
"ImagePageFileHistoryLine": "main",
"LoadExtensionSchemaUpdates": "main",
"MediaWikiPerformAction": "iframe",
"RevisionFromEditComplete": "main",
"ParserTestGlobals": "main",
"SkinTemplateNavigation::Universal": "main",
"TitleMove": "main",
"wgQueryPages": "main"
},
"HookHandlers": {
"main": {
"class": "MediaWiki\\TimedMediaHandler\\Hooks"
},
"iframe": {
"class": "MediaWiki\\TimedMediaHandler\\TimedMediaIframeOutput"
}
},
"config": {
"WaitTimeForTranscodeReset": {
"description": "How long you have to wait between transcode resets for non-error transcodes",
"public": true,
"value": 3600
},
"MinimumVideoPlayerSize": {
"description": "The minimum size for an embed video player (smaller than this size, it uses a pop-up player)",
"public": true,
"value": 200
},
"MediaVideoTypes": {
"description": "Set the supported ogg codecs for video",
"public": true,
"value": [
"Theora",
"VP8"
]
},
"MediaAudioTypes": {
"description": "Set the supported ogg codecs for audio",
"public": true,
"value": [
"Vorbis",
"Speex",
"FLAC",
"Opus"
]
},
"EnableIframeEmbed": {
"description": "Support iframe for remote embedding",
"public": true,
"value": true
},
"EnableTranscode": {
"description": "If transcoding is enabled for this wiki (if disabled, no transcode jobs are added and no transcode status is displayed). Note if remote embedding an asset we will still check if the remote repo has transcoding enabled and associated flavors for that media embed.",
"public": true,
"value": true
},
"EnableNiceBackgroundTranscodeJobs": {
"description": "If the job runner should run transcode commands in a background thread and monitor the transcoding progress. This enables more fine grain control of the transcoding process, wraps encoding commands in a lower priority 'nice' call, and kills long running transcodes that are not making any progress. If set to false, the job runner will use the more compatible php blocking shell exec command.",
"public": true,
"value": false
},
"TranscodeBackgroundPriority": {
"description": "The priority to be used with the nice transcode commands.",
"public": true,
"value": 19
},
"TranscodeBackgroundTimeLimit": {
"description": "The total amount of time a transcoding shell command can take",
"public": true,
"value": 28800
},
"TranscodeBackgroundMemoryLimit": {
"description": "Maximum amount of virtual memory available to transcoding processes in KiB. 2GiB. ffmpeg mmap resources, so virtual memory needs to be high enough",
"public": true,
"value": 2097152
},
"TranscodeBackgroundSizeLimit": {
"description": "Maximum file size transcoding processes can create, in KiB. 3GiB.",
"public": true,
"value": 3145728
},
+ "TranscodeSoftSizeLimit": {
+ "description": "Maximum estimated file size for transcodes without admin approval, in KiB. Larger files must be manually re-enqueued by an admin via web or CLI interfaces. 2GiB.",
+ "public": true,
+ "value": 2097152
+ },
"FFmpegThreads": {
"description": "Number of threads to use in ffmpeg for transcoding",
"public": true,
"value": 1
},
"FFmpegVP9RowMT": {
"description": "Whether to enable macroblock row multithreading for VP9 (-row-mt 1)\n\nThis allows increasing $wgFFmpegThreads to a larger number for VP9 transcodes; without it encoding will be limited to 4 threads for HD, or less for low resolution.\n\nThis requires libvpx 1.7 and a matching build of ffmpeg 3.3 or higher or it will fail with an unrecognized option error.",
"public": true,
"value": false
},
"FFmpegLocation": {
"description": "Location of the ffmpeg binary (used to encode WebM and for thumbnails)",
"public": false,
"value": "/usr/bin/ffmpeg"
},
"UseFFmpeg2": {
"description": "Compatible with ffmpeg 2",
"value": false
},
"TimedTextNS": {
"description": "The NS for TimedText (registered on MediaWiki.org) https://www.mediawiki.org/wiki/Extension_namespace_registration Note that Wikimedia Commons pre-dates TimedMediaHandler, and sets this to 102.",
"public": true,
"value": 710
},
"TimedTextForeignNamespaces": {
"description": "Set TimedText namespace for ForeignDBViaLBRepo on a per wikiID basis like `$wgTimedTextForeignNamespaces = [ 'commonswiki' => 102 ];`.",
"public": true,
"value": []
},
"EnabledTranscodeSet": {
"description": "Default enabled video transcodes\n\n- If set to empty array, no derivatives will be created\n- These transcodes are *in addition to* the source file.\n- Only derivatives with smaller width than the source asset size will be created\n- Regardless of source size at least one WebM and Ogg source will be created from the $wgEnabledTranscodeSet\n- Derivative jobs are added to the MediaWiki JobQueue the first time the asset is displayed\n- Derivative should be listed min to max\n\nAllowed values:\n* Values for WebM VP8/Vorbis, our primary free/open video format supported by Chrome/Firefox/Opera natively, and plays back in Safari/IE/Edge via ogv.js:\n** 160p.webm – Very low-bitrate web streamable WebM video\n** 240p.webm – Low-bitrate web streamable WebM video\n** 360p.webm – Medium-bitrate web streamable WebM video\n** 480p.webm – Moderate-bitrate web streamable WebM video\n** 720p.webm – A high quality WebM stream\n** 1080p.webm – A full-HD high quality WebM stream\n** 1440p.webm – A 2K full high quality WebM stream\n** 2160p.webm – A 4K full high quality WebM stream\n* Values for WebM VP9, our future free/open video format:\n** 160p.vp9.webm – Very low\n** 240p.vp9.webm – Low\n** 360p.vp9.webm – A least common denominator h.264 stream; first gen iPhone, iPods, early Android etc.\n** 480p.vp9.webm – A mid range h.264 stream; mid range phones and low end tablets\n** 720p.vp9.webm – An high quality HD stream; higher end phones, tablets, smart tvs\n** 1080p.vp9.webm – A full-HD high quality stream; higher end phones, tablets, smart tvs\n** 1440p.vp9.webm – A 2K high quality stream; higher end phones, tablets, smart tvs\n** 2160p.vp9.webm – A 4K high quality stream; higher end phones, tablets, smart tvs\n* Values for MP4 H.264/AAC, the primary format for the Apple/Microsoft world. Check patent licensing issues in your country before use! Similar to WebM in quality/bitrate:\n** 160p.mp4 – Very low\n** 240p.mp4 – Low\n** 320p.mp4 – Pretty low\n** 360p.mp4 – A least common denominator h.264 stream; first gen iPhone, iPods, early Android etc.\n** 480p.mp4 – A mid range h.264 stream; mid range phones and low end tablets\n** 720p.mp4 – An high quality HD stream; higher end phones, tablets, smart tvs\n** 1080p.mp4 – A full-HD high quality stream; higher end phones, tablets, smart tvs\n** 1440p.mp4 – A 2K high quality stream; higher end phones, tablets, smart tvs\n** 2160p.mp4 – A 4K high quality stream; higher end phones, tablets, smart tvs\n* Note: Ogg video profiles have been removed as of January 2018. Use WebM output for royalty-free codec output.",
"public": true,
"value": {
"160p.webm": false,
- "240p.webm": false,
- "360p.webm": false,
- "480p.webm": false,
+ "240p.webm": true,
+ "360p.webm": true,
+ "480p.webm": true,
"720p.webm": false,
"1080p.webm": false,
"1440p.webm": false,
"2160p.webm": false,
- "120p.vp9.webm": true,
- "180p.vp9.webm": true,
+ "120p.vp9.webm": false,
+ "180p.vp9.webm": false,
"240p.vp9.webm": true,
"360p.vp9.webm": true,
"480p.vp9.webm": true,
"720p.vp9.webm": true,
"1080p.vp9.webm": true,
"1440p.vp9.webm": true,
"2160p.vp9.webm": true,
"160p.mp4": false,
"240p.mp4": false,
"320p.mp4": false,
"360p.mp4": false,
"480p.mp4": false,
"720p.mp4": false,
"1080p.mp4": false,
"1440p.mp4": false,
"2160p.mp4": false
}
},
"EnabledAudioTranscodeSet": {
"description": "Default enabled audio transcodes\n\n If set to empty array, no derivatives will be created\n- These transcodes are *in addition to* the source file.\n- Derivative should be listed min to max\n\nllowed values:\n* ogg – Ogg Vorbis\n* opus – Note that Opus support must be available in ffmpeg\n* mp3 – Note that fmpeg needs libmp3lame support\n* m4a – Note that ffmpeg needs libvo_aacenc support",
"public": true,
"value": {
"ogg": true,
"opus": false,
"mp3": true,
"m4a": false
}
},
"TmhEnableMp4Uploads": {
"description": "If MP4 source assets can be ingested",
"public": true,
"value": false
},
"TmhFileExtensions": {
"description": "List of extensions handled by Timed Media Handler since its referenced in a few places. You should not modify this variable.",
"public": true,
"value": [
"ogg",
"ogv",
"oga",
"flac",
"opus",
"wav",
"webm",
"mp4",
"mp3",
"midi",
"mid",
"mpg",
"mpeg"
]
},
"TmhPriorityResolutionThreshold": {
"description": "Transcode resolutions higher than this will run in the low-priority queue. This'll give us SD transcodes as fast as possible, then do HD later.",
"public": true,
"value": 480
},
"TmhPriorityLengthThreshold": {
"description": "Transcodes of files longer than this (seconds) will run in the low-priority queue; defaults to 15 minutes. This'll mean long videos won't flood the high-priority queue.",
"public": true,
"value": 900
},
"ParserTestMediaHandlers": {
"description": "MockOggHandler::class",
"public": true,
"value": {
"application/ogg": "MockOggHandler"
}
},
"TmhFluidsynthLocation": {
"description": "Path of a Fluidsynth executable for MIDI conversion to audio formats",
"public": true,
"value": null
},
"TmhSoundfontLocation": {
"description": "Path of a soundfont to use for MIDI-converted audio",
"public": true,
"value": null
}
},
"ResourceFileModulePaths": {
"localBasePath": "",
"remoteExtPath": "TimedMediaHandler"
},
"ResourceModules": {
"ext.tmh.video-js": {
"es6": true,
"scripts": "resources/videojs/alt/video.core.min.js",
"styles": "resources/videojs/video-js.css",
"noflip": true,
"targets": [
"mobile",
"desktop"
],
"languageScripts": {
"ar": "resources/videojs/lang/ar.js",
"ba": "resources/videojs/lang/ba.js",
"bg": "resources/videojs/lang/bg.js",
"ca": "resources/videojs/lang/ca.js",
"cs": "resources/videojs/lang/cs.js",
"cy": "resources/videojs/lang/cy.js",
"da": "resources/videojs/lang/da.js",
"de": "resources/videojs/lang/de.js",
"el": "resources/videojs/lang/el.js",
"en": "resources/videojs/lang/en.js",
"es": "resources/videojs/lang/es.js",
"fa": "resources/videojs/lang/fa.js",
"fi": "resources/videojs/lang/fi.js",
"fr": "resources/videojs/lang/fr.js",
"gd": "resources/videojs/lang/gd.js",
"gl": "resources/videojs/lang/gl.js",
"he": "resources/videojs/lang/he.js",
"hi": "resources/videojs/lang/hi.js",
"hr": "resources/videojs/lang/hr.js",
"hu": "resources/videojs/lang/hu.js",
"it": "resources/videojs/lang/it.js",
"ja": "resources/videojs/lang/ja.js",
"ko": "resources/videojs/lang/ko.js",
"lv": "resources/videojs/lang/lv.js",
"nb": "resources/videojs/lang/nb.js",
"nl": "resources/videojs/lang/nl.js",
"nn": "resources/videojs/lang/nn.js",
"oc": "resources/videojs/lang/oc.js",
"pl": "resources/videojs/lang/pl.js",
"pt": "resources/videojs/lang/pt-PT.js",
"pt-br": "resources/videojs/lang/pt-BR.js",
"ro": "resources/videojs/lang/ro.js",
"ru": "resources/videojs/lang/ru.js",
"sk": "resources/videojs/lang/sk.js",
"sl": "resources/videojs/lang/sl.js",
"sr": "resources/videojs/lang/sr.js",
"sv": "resources/videojs/lang/sv.js",
"te": "resources/videojs/lang/te.js",
"th": "resources/videojs/lang/th.js",
"tr": "resources/videojs/lang/tr.js",
"uk": "resources/videojs/lang/uk.js",
"vi": "resources/videojs/lang/vi.js",
"zh-cn": "resources/videojs/lang/zh-CN.js",
"zh-hans": "resources/videojs/lang/zh-Hans.js",
"zh-hant": "resources/videojs/lang/zh-Hant.js",
"zh-tw": "resources/videojs/lang/zh-TW.js"
}
},
"ext.tmh.videojs-ogvjs": {
"es6": true,
"scripts": "resources/videojs-ogvjs/videojs-ogvjs.js",
"targets": [
"mobile",
"desktop"
],
"dependencies": [
"ext.tmh.video-js",
"ext.tmh.OgvJs"
]
},
"ext.tmh.player": {
"es6": true,
"packageFiles": [
"resources/ext.tmh.player.js",
"resources/ext.tmh.player.element.js"
],
"targets": [
"mobile",
"desktop"
],
"dependencies": [
"ext.tmh.player.styles",
"ext.tmh.OgvJsSupport",
"mediawiki.Title"
],
"messages": [
"timedmedia-play-media",
"timedmedia-subtitles-available",
"timedmedia-duration-hms",
"timedmedia-duration-ms",
"timedmedia-duration-s"
]
},
"ext.tmh.player.dialog": {
"es6": true,
"packageFiles": [
"resources/ext.tmh.player.dialog.loader.js",
"resources/ext.tmh.player.dialog.js"
],
"styles": [
"resources/ext.tmh.player.dialog.less"
],
"targets": [
"mobile",
"desktop"
],
"dependencies": [
"oojs-ui-core",
"oojs-ui-windows",
"ext.tmh.player.inline"
],
"messages": [ "timedmedia-dialog-close" ]
},
"ext.tmh.player.inline": {
"es6": true,
"packageFiles": [
"resources/ext.tmh.player.inline.js",
"resources/mw-info-button/mw-info-button.js",
"resources/videojs-resolution-switcher/videojs-resolution-switcher.js",
"resources/mw-subtitles-button/mw-subtitles-create.js",
"resources/mw-subtitles-button/mw-subtitles-button.js"
],
"styles": [
"resources/ext.tmh.player.inline.styles.less",
"resources/mw-info-button/mw-info-button.css",
"resources/videojs-resolution-switcher/videojs-resolution-switcher.css",
"resources/ext.tmh.player.inline.styles.less"
],
"targets": [
"mobile",
"desktop"
],
"dependencies": [
"ext.tmh.video-js",
"mediawiki.Title"
],
"messages": [
"timedmedia-resolution-120",
"timedmedia-resolution-160",
"timedmedia-resolution-180",
"timedmedia-resolution-240",
+ "timedmedia-resolution-288",
"timedmedia-resolution-360",
"timedmedia-resolution-480",
"timedmedia-resolution-720",
"timedmedia-resolution-1080",
"timedmedia-resolution-1440",
"timedmedia-resolution-2160",
"videojs-more-information",
"videojs-quality",
"videojs-subtitles-create",
"videojs-captions-create"
]
},
"ext.tmh.player.styles": {
"styles": "resources/ext.tmh.player.styles.less",
"targets": [
"mobile",
"desktop"
]
},
"ext.tmh.transcodetable": {
"scripts": "resources/transcode-table/transcode-table.js",
"styles": "resources/transcode-table/transcode-table.css",
"dependencies": [
"mediawiki.api",
"oojs-ui"
],
"messages": [
"timedmedia-reset-button-cancel",
"timedmedia-reset-button-dismiss",
"timedmedia-reset-button-reset",
"timedmedia-reset-error",
"timedmedia-reset",
"timedmedia-reset-areyousure",
- "timedmedia-reset-explanation"
+ "timedmedia-reset-explanation",
+ "timedmedia-error-details",
+ "timedmedia-error-dismiss"
]
},
"ext.tmh.timedtextpage.styles": {
"styles": "resources/ext.tmh.timedtextpage.styles.less"
},
"ext.tmh.OgvJsSupport": {
"es6": true,
"scripts": [
"resources/ogv.js/ogv-support.js",
"resources/ext.tmh.OgvJsSupport.js"
],
"targets": [
"mobile",
"desktop"
]
},
"ext.tmh.OgvJs": {
"es6": true,
"scripts": [
"resources/ogv.js/ogv.js"
],
"dependencies": "ext.tmh.OgvJsSupport",
"targets": [
"mobile",
"desktop"
]
},
"embedPlayerIframeStyle": {
"styles": "resources/embedPlayerIframe.css",
"targets": [
"mobile",
"desktop"
]
}
},
"MediaHandlers": {
"application/ogg": "MediaWiki\\TimedMediaHandler\\Handlers\\OggHandler\\OggHandler",
"audio/flac": "MediaWiki\\TimedMediaHandler\\Handlers\\FLACHandler\\FLACHandler",
"audio/midi": "MediaWiki\\TimedMediaHandler\\Handlers\\MidiHandler\\MidiHandler",
"audio/mpeg": "MediaWiki\\TimedMediaHandler\\Handlers\\Mp3Handler\\Mp3Handler",
"audio/wav": "MediaWiki\\TimedMediaHandler\\Handlers\\WAVHandler\\WAVHandler",
"audio/webm": "MediaWiki\\TimedMediaHandler\\Handlers\\WebMHandler\\WebMHandler",
"audio/x-flac": "MediaWiki\\TimedMediaHandler\\Handlers\\FLACHandler\\FLACHandler",
"video/mp4": "MediaWiki\\TimedMediaHandler\\Handlers\\Mp4Handler\\Mp4Handler",
"video/mpeg": "MediaWiki\\TimedMediaHandler\\Handlers\\MPEGHandler\\MPEGHandler",
"video/webm": "MediaWiki\\TimedMediaHandler\\Handlers\\WebMHandler\\WebMHandler"
},
"JobClasses": {
"webVideoTranscode": "MediaWiki\\TimedMediaHandler\\WebVideoTranscode\\WebVideoTranscodeJob",
"webVideoTranscodePrioritized": "MediaWiki\\TimedMediaHandler\\WebVideoTranscode\\WebVideoTranscodeJob"
},
"load_composer_autoloader": true,
"manifest_version": 2
}
diff --git a/i18n/en.json b/i18n/en.json
index 8f3aba6e..6753b385 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1,192 +1,195 @@
{
"@metadata": {
"authors": []
},
"timedmediahandler-extensionname": "TimedMediaHandler",
"timedmediahandler-desc": "Handler for audio, video and timed text, with format support for WebM, Ogg Theora, Vorbis, srt",
"timedmedia-ogg-short-audio": "Ogg $1 sound file, $2",
"timedmedia-ogg-short-video": "Ogg $1 video file, $2",
"timedmedia-ogg-short-general": "Ogg $1 media file, $2",
"timedmedia-ogg-long-audio": "Ogg $1 sound file, length $2, $3",
"timedmedia-ogg-long-video": "Ogg $1 video file, length $2, $4 × $5 pixels, $3",
"timedmedia-ogg-long-multiplexed": "Ogg multiplexed audio/video file, $1, length $2, $4 × $5 pixels, $3 overall",
"timedmedia-ogg-long-general": "Ogg media file, length $2, $3",
"timedmedia-ogg-long-error": "Invalid Ogg file: $1",
"timedmedia-ogg-long-no-streams": "Ogg media file. Warning: None of the codecs used in this file are recognized.",
"timedmedia-webm-short-video": "WebM $1 video file, $2",
"timedmedia-webm-long-video": "WebM audio/video file, $1, length $2, $4 × $5 pixels, $3 overall",
"timedmedia-flac-short-audio": "FLAC audio file, $1",
"timedmedia-flac-long-audio": "FLAC audio file, length $1, $2 overall",
"timedmedia-wav-short-audio": "WAV audio file, $1",
"timedmedia-wav-long-audio": "WAV audio file, length $1, $2 overall",
"timedmedia-wav-pcm-required": "You can only upload PCM (Pulse Code Modulation) WAV.",
"timedmedia-mp3-short-audio": "MP3 audio file, $1",
"timedmedia-mp3-long-audio": "MP3 audio file, length $1, $2 overall",
"timedmedia-midi-short-audio": "MIDI audio file, $1",
"timedmedia-midi-long-audio": "MIDI audio file, length $1, $2 overall",
"timedmedia-mp4-short-video": "MP4 $1 video file, $2",
"timedmedia-mp4-long-video": "MP4 audio/video file, $1, length $2, $4 × $5 pixels, $3 overall",
"timedmedia-mpeg-short-video": "MPEG $1 video file, $2",
"timedmedia-mpeg-long-video": "MPEG audio/video file, $1, length $2, $4 × $5 pixels, $3 overall",
"timedmedia-more": "More…",
"timedmedia-dismiss": "Close",
"timedmedia-download": "Download file",
"timedmedia-play-media": "Play media",
"timedmedia-desc-link": "About this file",
"timedmedia-status-header": "Transcode status",
"timedmedia-update-status": "Update transcode status",
"timedmedia-status": "Status",
"timedmedia-status-unknown": "Unknown status",
"timedmedia-transcodebitrate": "Bitrate",
"timedmedia-transcodeduration": "Encode time",
"timedmedia-transcodeinfo": "Format",
"timedmedia-actions": "Actions",
"timedmedia-direct-link": "Download",
"timedmedia-not-ready": "Not ready",
"timedmedia-completed-on": "Completed $1",
"timedmedia-error-on": "Error on $1",
"timedmedia-started-transcode": "Started $1 ago. $2",
"timedmedia-percent-done": "About $1% done",
"timedmedia-in-job-queue": "Added to Job queue $1 ago",
"timedmedia-unknown-target-size": "Unknown target size, $1 encoded",
"timedmedia-days": "{{PLURAL:$1|1 day|$1 days}}",
"timedmedia-hours": "{{PLURAL:$1|1 hour|$1 hours}}",
"timedmedia-minutes": "{{PLURAL:$1|1 minute|$1 minutes}}",
"timedmedia-seconds": "{{PLURAL:$1|1 second|$1 seconds}}",
"timedmedia-reset": "Reset transcode",
"timedmedia-reset-areyousure": "Are you sure you want to proceed?",
"timedmedia-reset-button-cancel": "Don't reset",
"timedmedia-reset-button-dismiss": "Dismiss",
"timedmedia-reset-button-reset": "Reset",
"timedmedia-reset-error": "Error in resetting transcode job.",
"timedmedia-reset-explanation": "Resetting this transcode will remove any existing file (if present), and it will re-add the transcode to the job queue. It will take some time to re-transcode.",
+ "timedmedia-error-details": "Transcode error details",
+ "timedmedia-error-dismiss": "Dismiss",
"timedmedia-ogg": "Ogg",
"timedmedia-webm": "WebM",
"timedmedia-mp4": "MP4",
"timedmedia-wav": "WAV",
"timedmedia-flac": "FLAC",
"timedmedia-mp3": "MP3",
"timedmedia-mpeg": "MPEG",
"timedmedia-source-file": "$1 source",
"timedmedia-source-file-desc": "Original $1 file, $2 × $3 ($4)",
"timedmedia-source-audio-file-desc": "Original $1 file ($2)",
"timedmedia-derivative-160p.ogv": "Ogg 160P",
"timedmedia-derivative-desc-160p.ogv": "Low bandwidth Ogg video (160P)",
"timedmedia-derivative-240p.ogv": "Ogg 240P",
"timedmedia-derivative-desc-240p.ogv": "Small Ogg video (240P)",
"timedmedia-derivative-360p.ogv": "Ogg 360P",
"timedmedia-derivative-desc-360p.ogv": "Ogg video (360P)",
"timedmedia-derivative-480p.ogv": "Ogg 480P",
"timedmedia-derivative-desc-480p.ogv": "SD Ogg video (480P)",
"timedmedia-derivative-720p.ogv": "Ogg 720P",
"timedmedia-derivative-desc-720p.ogv": "HD Ogg video (720P)",
"timedmedia-derivative-1080p.ogv": "Ogg 1080P",
"timedmedia-derivative-desc-1080p.ogv": "Full HD Ogg video (1080P)",
"timedmedia-derivative-160p.webm": "WebM 160P",
"timedmedia-derivative-desc-160p.webm": "Low bandwidth WebM (160P)",
"timedmedia-derivative-240p.webm": "WebM 240P",
"timedmedia-derivative-desc-240p.webm": "Small WebM (240P)",
"timedmedia-derivative-360p.webm": "WebM 360P",
"timedmedia-derivative-desc-360p.webm": "WebM (360P)",
"timedmedia-derivative-480p.webm": "WebM 480P",
"timedmedia-derivative-desc-480p.webm": "SD WebM (480P)",
"timedmedia-derivative-720p.webm": "WebM 720P",
"timedmedia-derivative-desc-720p.webm": "HD WebM (720P)",
"timedmedia-derivative-1080p.webm": "WebM 1080P",
"timedmedia-derivative-desc-1080p.webm": "Full HD WebM (1080P)",
"timedmedia-derivative-1440p.webm": "WebM 1440P",
"timedmedia-derivative-desc-1440p.webm": "UHD WebM (1440P)",
"timedmedia-derivative-2160p.webm": "WebM 2160P",
"timedmedia-derivative-desc-2160p.webm": "Full UHD WebM (2160P)",
"timedmedia-derivative-120p.vp9.webm": "VP9 120P",
"timedmedia-derivative-desc-120p.vp9.webm": "Lowest bandwidth VP9 (120P)",
"timedmedia-derivative-180p.vp9.webm": "VP9 180P",
"timedmedia-derivative-desc-180p.vp9.webm": "Low bandwidth VP9 (180P)",
"timedmedia-derivative-240p.vp9.webm": "VP9 240P",
"timedmedia-derivative-desc-240p.vp9.webm": "Small VP9 (240P)",
"timedmedia-derivative-360p.vp9.webm": "VP9 360P",
"timedmedia-derivative-desc-360p.vp9.webm": "VP9 (360P)",
"timedmedia-derivative-480p.vp9.webm": "VP9 480P",
"timedmedia-derivative-desc-480p.vp9.webm": "SD VP9 (480P)",
"timedmedia-derivative-720p.vp9.webm": "VP9 720P",
"timedmedia-derivative-desc-720p.vp9.webm": "HD VP9 (720P)",
"timedmedia-derivative-1080p.vp9.webm": "VP9 1080P",
"timedmedia-derivative-desc-1080p.vp9.webm": "Full HD VP9 (1080P)",
"timedmedia-derivative-1440p.vp9.webm": "VP9 1440P",
"timedmedia-derivative-desc-1440p.vp9.webm": "UHD VP9 (1440P)",
"timedmedia-derivative-2160p.vp9.webm": "VP9 2160P",
"timedmedia-derivative-desc-2160p.vp9.webm": "4K UHD VP9 (2160P)",
"timedmedia-derivative-160p.mp4": "H264 160P",
"timedmedia-derivative-desc-160p.mp4": "Low bandwidth MP4 H.264 (160P)",
"timedmedia-derivative-240p.mp4": "H264 240P",
"timedmedia-derivative-desc-240p.mp4": "Small MP4 H.264 (240P)",
"timedmedia-derivative-320p.mp4": "H264 320P",
"timedmedia-derivative-desc-320p.mp4": "MP4 H.264 (320P)",
"timedmedia-derivative-360p.mp4": "H264 360P",
"timedmedia-derivative-desc-360p.mp4": "MP4 H.264 (360P)",
"timedmedia-derivative-480p.mp4": "H264 480P",
"timedmedia-derivative-desc-480p.mp4": "SD MP4 H.264 (480P)",
"timedmedia-derivative-720p.mp4": "H264 720P",
"timedmedia-derivative-desc-720p.mp4": "HD MP4 H.264 (720P)",
"timedmedia-derivative-1080p.mp4": "H264 1080P",
"timedmedia-derivative-desc-1080p.mp4": "Full HD MP4 H.264 (1080P)",
"timedmedia-derivative-1440p.mp4": "H264 1440P",
"timedmedia-derivative-desc-1440p.mp4": "UHD MP4 H.264 (1440P)",
"timedmedia-derivative-2160p.mp4": "H264 2160P",
"timedmedia-derivative-desc-2160p.mp4": "4K UHD MP4 H.264 (2160P)",
"timedmedia-derivative-ogg": "Ogg Vorbis",
"timedmedia-derivative-desc-ogg": "Ogg Vorbis",
"timedmedia-derivative-opus": "Opus",
"timedmedia-derivative-desc-opus": "Opus",
"timedmedia-derivative-mp3": "MP3",
"timedmedia-derivative-desc-mp3": "MP3",
"timedmedia-derivative-m4a": "AAC",
"timedmedia-derivative-desc-m4a": "AAC",
"timedmedia-resolution-120": "Low 120p",
"timedmedia-resolution-160": "Low 160p",
"timedmedia-resolution-180": "Low 180p",
"timedmedia-resolution-240": "Low 240p",
+ "timedmedia-resolution-288": "Low 288p",
"timedmedia-resolution-360": "SD 360p",
"timedmedia-resolution-480": "SD 480p",
"timedmedia-resolution-720": "HD 720p",
"timedmedia-resolution-1080": "HD 1080p",
"timedmedia-resolution-1440": "UHD 1440p",
"timedmedia-resolution-2160": "UHD 2160p",
"timedmedia-subtitle-new": "Create new translation or edit existing",
"timedmedia-subtitle-new-desc": "Select language and press the '''{{int:Timedmedia-subtitle-new-go}}''' button",
"timedmedia-subtitle-new-go": "Go",
"timedmedia-subtitle-language": "$1 {{BIDI:($2)}}",
"timedmedia-subtitle-no-video": "There is no video associated with the current subtitle page.",
"timedmedia-subtitle-no-subtitles": "There are currently no subtitles in $1 for this video, you can [{{fullurl:{{FULLPAGENAME}}|action=edit}} edit this page] to add them.",
"timedmedia-subtitle-remote": "Timed text for this file is hosted on $1",
"timedmedia-subtitle-remote-link": "You can [$1 view the description page] for this file on $2",
"timedmedia-subtitles-available": "Subtitles available.",
"timedmedia-derivative-state-transcodes": "{{PLURAL:$1|$1 transcode|$1 transcodes}}",
"timedmedia-derivative-state-active": "{{PLURAL:$1|$1 running transcode|$1 running transcodes}}",
"timedmedia-derivative-state-queued": "{{PLURAL:$1|$1 queued transcode|$1 queued transcodes}}",
"timedmedia-derivative-state-failed": "{{PLURAL:$1|$1 failed transcode|$1 failed transcodes}}",
"timedmedia-derivative-state-missing": "{{PLURAL:$1|$1 uninitialized transcode|$1 uninitialized transcodes}}",
"timedmedia-no-derivatives": "No transcoding required.",
"timedmedia-file": "File",
"timedmedia-dialog-close": "Close media player",
"timedmedia-duration-hms": "{{PLURAL:$1|$1 hour|$1 hours}}, {{PLURAL:$2|$2 minute|$2 minutes}} and {{PLURAL:$3|$3 second|$3 seconds}}",
"timedmedia-duration-ms": "{{PLURAL:$1|$1 minute|$1 minutes}} and {{PLURAL:$2|$2 second|$2 seconds}}",
"timedmedia-duration-s": "{{PLURAL:$1|$1 second|$1 seconds}}",
"right-transcode-reset": "Reset failed or transcoded videos so they are inserted into the job queue again",
"right-transcode-status": "View [[Special:TimedMediaHandler|information about the current transcode activity]]",
"action-transcode-reset": "reset transcodes",
"action-transcode-status": "view the current transcoding status",
"log-name-timedmediahandler": "TimedMediaHandler log",
"log-description-timedmediahandler": "This is a log of actions related to timed media transcodes.",
"logentry-timedmediahandler-resettranscode": "$1 {{GENDER:$2|reset}} a transcode of $3 to the format \"$4\"",
"orphanedtimedtext": "Orphaned TimedText pages",
"orphanedtimedtext-summary": "List of [[{{#special:AllPages/TimedText:}}|{{ns:TimedText}}]] pages which do not have a corresponding file.",
"orphanedtimedtext-unsupported": "This special page is only supported on MySQL databases.",
"videojs-more-information": "More information",
"videojs-quality": "Video quality",
"videojs-subtitles-create": "Create subtitles",
"videojs-captions-create": "Create captions",
"transcodestatistics": "Transcode statistics",
"timedmedia-timedtext-title-edit-subtitles": "$1 subtitles for clip: $2",
"timedmedia-timedtext-title-create-subtitles": "No $1 subtitles were found for clip: $2"
}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 15265369..cac07bb4 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -1,212 +1,215 @@
{
"@metadata": {
"authors": [
"Amire80",
"Aotake",
"Beginneruser",
"BrokenArrow",
"EugeneZelenko",
"Fryed-peach",
"Jon Harald Søby",
"Liuxinyu970226",
"Meno25",
"Minh Nguyen",
"Mormegil",
"Purodha",
"Raymond",
"Robby",
"Shirayuki",
"Siebrand",
"Tmv",
"Umherirrender",
"아라"
]
},
"timedmediahandler-extensionname": "{{name}}\nThe name of the extension",
"timedmediahandler-desc": "{{desc|name=TimedMediaHandler|url=https://www.mediawiki.org/wiki/Extension:TimedMediaHandler}}",
"timedmedia-ogg-short-audio": "File details for Ogg sound (audio) files, short version.\nParameters:\n* $1 - stream type name. Any one of the following: Vorbis, Speex, FLAC\n* $2 - duration of the sound/audio (localized) - e.g. 1m34s\n{{Related|Timedmedia-ogg-short}}",
"timedmedia-ogg-short-video": "File details for Ogg video files, short version.\nParameters:\n* $1 - stream type name: Theora\n* $2 - duration of the video (localized) - e.g. 1m34s\n{{Related|Timedmedia-ogg-short}}",
"timedmedia-ogg-short-general": "File details for generic (non-audio, non-video) Ogg files, short version.\nParameters:\n* $1 - stream type name\n* $2 - duration of the media (localized) - e.g. 1m34s\n{{Related|Timedmedia-ogg-short}}",
"timedmedia-ogg-long-audio": "File details for Ogg sound (audio) files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - stream type name. Any one of the following: Vorbis, Speex, FLAC\n* $2 - duration of the sound (localized) - e.g. 1m34s\n* $3 - bit-rate of the sound (localized) - e.g. 97kbps\n{{Related|Timedmedia-ogg-long}}",
"timedmedia-ogg-long-video": "File details for Ogg video files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - stream type name: Theora\n* $2 - duration of the video (localized) - e.g. 1m34s\n* $3 - bit-rate of the video (localized) - e.g. 97kbps\n* $4 - width of the video (in pixels)\n* $5 - height of the video (in pixels)\n{{Related|Timedmedia-ogg-long}}",
"timedmedia-ogg-long-multiplexed": "{{doc-important|Start with a lowercase letter, unless the first word is \"Ogg\".}}\nFile details for Ogg multiplexed audio/video files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - stream type names, slash-separated - e.g. Theora/Vorbis\n* $2 - duration (localized) - e.g. 1m34s\n* $3 - bit-rate (localized) - e.g. 97kbps\n* $4 - width of the video (in pixels)\n* $5 - height of the video (in pixels)\n{{Related|Timedmedia-ogg-long}}",
"timedmedia-ogg-long-general": "File details for Ogg generic (non-video, non-audio) files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - (Unused)\n* $2 - duration (localized) - e.g. 1m34s\n* $3 - bit-rate (localized) - e.g. 97kbps\n{{Related|Timedmedia-ogg-long}}",
"timedmedia-ogg-long-error": "Used as error message. Parameters:\n* $1 - error message. e.g. File not found, invalid stream, etc.",
"timedmedia-ogg-long-no-streams": "Similar to {{msg-mw|timedmedia-ogg-long-error}}. Used as the long description for an ogg media file if there are no streams in the file using a recognized codec. This can be caused by an invalid ogg file, or by one using an obscure type of data (e.g. dirac video with no audio).",
"timedmedia-webm-short-video": "File details for WebM video files, short version.\nParameters:\n* $1 - stream type names (slash separated) - e.g. Vorbis/VP8\n* $2 - duration of the video (localized) - e.g. 1m34s\nSee also:\n* {{msg-mw|Timedmedia-webm-long-video}}",
"timedmedia-webm-long-video": "File details for WebM multiplexed audio/video files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - stream type names (slash separated) - e.g. Vorbis/VP8\n* $2 - duration (localized) - e.g. 1m34s\n* $3 - bit-rate (localized) - e.g. 97kbps\n* $4 - width of the video (in pixels)\n* $5 - height of the video (in pixels)\nSee also:\n* {{msg-mw|Timedmedia-webm-short-video}}",
"timedmedia-flac-short-audio": "File details for FLAC audio files, short version.\nParameters:\n* $1 - duration of the audio (localized) - e.g. 1m34s\n\nPrimarily used on [[Special:Search]] results pages, i.e.\n https://commons.wikimedia.org/w/index.php?title=Special%3ASearch&profile=advanced&search=What%27s+a+love+dart&fulltext=Search&ns6=1&profile=advanced\n\nSee also:\n* {{msg-mw|Timedmedia-flac-long-audio}}\n* {{msg-mw|Timedmedia-ogg-short-audio}}",
"timedmedia-flac-long-audio": "File details for FLAC files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - duration (localized) - e.g. 1m34s\n* $2 - bit-rate (localized) - e.g. 97kbps\nSee also:\n* {{msg-mw|Timedmedia-flac-short-audio}}",
"timedmedia-wav-short-audio": "File details for WAV audio files, short version.\nParameters:\n* $1 - duration of the audio (localized) - e.g. 1m34s\nSee also:\n* {{msg-mw|Timedmedia-wav-long-audio}}",
"timedmedia-wav-long-audio": "File details for WAV files, long version.\nShown after the filename in the image description page.\n\nParameters:\n* $1 - duration of the audio (localized) - e.g. 1m34s\n* $2 - bit-rate (localized) - e.g. 97kbps\nSee also:\n* {{msg-mw|Timedmedia-wav-short-audio}}",
"timedmedia-wav-pcm-required": "Message shown at upload if user tries to upload a WAV file using a codec that is not PCM",
"timedmedia-mp3-short-audio": "File details for MP3 audio files, short version.\nParameters:\n* $1 - duration of the audio (localized) - e.g. 1m34s\n\nPrimarily used on [[Special:Search]] results pages, i.e.\n https://commons.wikimedia.org/w/index.php?title=Special%3ASearch&profile=advanced&search=What%27s+a+love+dart&fulltext=Search&ns6=1&profile=advanced\n\nSee also:\n* {{msg-mw|Timedmedia-mp3-long-audio}}\n* {{msg-mw|Timedmedia-ogg-short-audio}}",
"timedmedia-mp3-long-audio": "File details for MP3 files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - duration (localized) - e.g. 1m34s\n* $2 - bit-rate (localized) - e.g. 97kbps\nSee also:\n* {{msg-mw|Timedmedia-mp3-short-audio}}",
"timedmedia-midi-short-audio": "File details for MIDI audio files, short version.\nParameters:\n* $1 - duration of the audio (localized) - e.g. 1m34s\n\nPrimarily used on [[Special:Search]] results pages, i.e.\n https://commons.wikimedia.org/w/index.php?title=Special%3ASearch&profile=advanced&search=What%27s+a+love+dart&fulltext=Search&ns6=1&profile=advanced\n\nSee also:\n* {{msg-mw|Timedmedia-midi-long-audio}}\n* {{msg-mw|Timedmedia-ogg-short-audio}}",
"timedmedia-midi-long-audio": "File details for MIDI files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - duration (localized) - e.g. 1m34s\n* $2 - bit-rate (localized) - e.g. 97kbps\nSee also:\n* {{msg-mw|Timedmedia-midi-short-audio}}",
"timedmedia-mp4-short-video": "File details for MP4 video files, short version.\nParameters:\n* $1 - stream type names (slash separated) - e.g. AAC/h.264\n* $2 - duration of the video (localized) - e.g. 1m34s\nSee also:\n* {{msg-mw|Timedmedia-mp4-long-video}}",
"timedmedia-mp4-long-video": "File details for MP4 multiplexed audio/video files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - stream type names (slash separated) - e.g. AAC/h.264\n* $2 - duration (localized) - e.g. 1m34s\n* $3 - bit-rate (localized) - e.g. 97kbps\n* $4 - width of the video (in pixels)\n* $5 - height of the video (in pixels)\nSee also:\n* {{msg-mw|Timedmedia-mp4-short-video}}",
"timedmedia-mpeg-short-video": "File details for MPEG 1/2 video files, short version.\nParameters:\n* $1 - stream type names (slash separated) - e.g. MPEG-2\n* $2 - duration of the video (localized) - e.g. 1m34s\nSee also:\n* {{msg-mw|Timedmedia-mpeg-long-video}}",
"timedmedia-mpeg-long-video": "File details for MPEG-1/2 multiplexed audio/video files, long version.\n\nShown after the filename in the image description page.\n\nParameters:\n* $1 - stream type names (slash separated) - e.g. MPEG-2\n* $2 - duration (localized) - e.g. 1m34s\n* $3 - bit-rate (localized) - e.g. 97kbps\n* $4 - width of the video (in pixels)\n* $5 - height of the video (in pixels)\nSee also:\n* {{msg-mw|Timedmedia-mpeg-short-video}}",
"timedmedia-more": "Unused at this time.\n{{Identical|More...}}",
"timedmedia-dismiss": "Unused at this time.\n{{Identical|Close}}",
"timedmedia-download": "Used as tooltip for the Download button.\n{{Identical|Download}}",
"timedmedia-play-media": "Used as tooltip for the link.",
"timedmedia-desc-link": "Unused at this time.\n{{Identical|About this file}}",
"timedmedia-status-header": "Used as section heading.",
"timedmedia-update-status": "Used as text for the link which is used to purge.",
"timedmedia-status": "Used as column header.\n{{Identical|Status}}",
"timedmedia-status-unknown": "Used as status message on error.\n{{Identical|Unknown status}}",
"timedmedia-transcodebitrate": "A table column header for description of transcode bitrate",
"timedmedia-transcodeduration": "A table column header for description of transcode encoding time",
"timedmedia-transcodeinfo": "A table column header for description of Transcode derivative\n{{Identical|Format}}",
"timedmedia-actions": "Used as column header.\n{{Identical|Action}}",
"timedmedia-direct-link": "Used as column header.\n{{Identical|Download}}",
"timedmedia-not-ready": "State of a given transcode job being not yet complete or not yet ready",
"timedmedia-completed-on": "Completed transcode message. Parameters:\n* $1 - the timestamp (time and date) that the transcode was completed",
"timedmedia-error-on": "Parameters:\n* $1 - timestamp",
"timedmedia-started-transcode": "Status update for Transcodes. Parameters:\n* $1 - time passed since transcoded started (e.g. \"{{int:timedmedia-minutes|1}}{{int:comma-separator}}{{int:timedmedia-seconds|42}}\"), uses the following messages:\n** {{msg-mw|Timedmedia-days}}\n** {{msg-mw|Timedmedia-hours}}\n** {{msg-mw|Timedmedia-minutes}}\n** {{msg-mw|Timedmedia-seconds}}\n** {{msg-mw|Comma-separator}}\n* $2 - percentage of transcode complete",
"timedmedia-percent-done": "Commented out at this time.\n\nStatus update for Transcodes. Parameters:\n* $1 - percentage of the file transcoded so far\nSee also:\n* {{msg-mw|timedmedia-unknown-target-size}}",
"timedmedia-in-job-queue": "Shown on the file description page in the {{msg-mw|timedmedia-status-header}} section.\n\nParameters:\n* $1 - the time the media has been in the job queue (e.g. \"{{int:timedmedia-minutes|1}}{{int:comma-separator}}{{int:timedmedia-seconds|42}}\"), uses the following messages:\n** {{msg-mw|Timedmedia-days}}\n** {{msg-mw|Timedmedia-hours}}\n** {{msg-mw|Timedmedia-minutes}}\n** {{msg-mw|Timedmedia-seconds}}\n** {{msg-mw|Comma-separator}}",
"timedmedia-unknown-target-size": "Commented out at this time.\n\nStatus update for Transcodes. Parameters:\n* $1 - the number of bytes of the file transcoded so far (localized)\nSee also:\n* {{msg-mw|Timedmedia-percent-done}}",
"timedmedia-days": "Used as the duration, as $1
in the following messages:\n* {{msg-mw|Timedmedia-in-job-queue}}\n* {{msg-mw|Timedmedia-started-transcode}}\nParameters:\n* $1 - number of days\n{{Related|Timedmedia-days}}\n{{Identical|Day}}",
"timedmedia-hours": "Used as the duration, as $1
in the following messages:\n* {{msg-mw|Timedmedia-in-job-queue}}\n* {{msg-mw|Timedmedia-started-transcode}}\nParameters:\n* $1 - number of hours\n{{Related|Timedmedia-days}}\n{{Identical|Hour}}",
"timedmedia-minutes": "Used as the duration, as $1
in the following messages:\n* {{msg-mw|Timedmedia-in-job-queue}}\n* {{msg-mw|Timedmedia-started-transcode}}\nParameters:\n* $1 - minutes\n{{Related|Timedmedia-days}}\n{{Identical|Minute}}",
"timedmedia-seconds": "Used as the duration, as $1
in the following messages:\n* {{msg-mw|Timedmedia-in-job-queue}}\n* {{msg-mw|Timedmedia-started-transcode}}\nParameters:\n* $1 - number of seconds\n{{Related|Timedmedia-days}}\n{{Identical|Second}}",
"timedmedia-reset": "Used as action link text and as dialog title.\n\nThe contents of the dialog are {{msg-mw|Timedmedia-reset-explanation}} and {{msg-mw|Timedmedia-reset-areyousure}}.",
"timedmedia-reset-areyousure": "Used as the last confirmation request in the dialog which has the title {{msg-mw|Timedmedia-reset}}.",
"timedmedia-reset-button-cancel": "Button in reset dialog to cancel reset request to transcode.",
"timedmedia-reset-button-dismiss": "Dismiss button in error dialog presented when transcode request fails.\n{{identical|Dismiss}}",
"timedmedia-reset-button-reset": "Button in reset dialog to approve new transcode.\n{{Identical|Reset}}",
"timedmedia-reset-error": "Used as generic error message.\n要翻訳",
"timedmedia-reset-explanation": "Explains the consequence of resetting in the dialog which has the title {{msg-mw|Timedmedia-reset}}.",
+ "timedmedia-error-details": "Title for dialog showing low-level error message from a failed media transcode.",
+ "timedmedia-error-dismiss": "Dismiss button in dialog showing low-level error message from a failed media transcode operations.\n{{identical|Dismiss}}",
"timedmedia-ogg": "{{optional}}\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-webm": "{{optional}}\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-mp4": "{{Optional}}\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-wav": "{{Optional}}\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-flac": "{{Optional}}\n\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-mp3": "{{Optional}}\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-mpeg": "{{Optional}}\nUsed as $1
in the following messages:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n* {{msg-mw|Timedmedia-source-file}}",
"timedmedia-source-file": "The source file. Parameters:\n* $1 - file type; any one of the following messages:\n** {{msg-mw|Timedmedia-ogg}}\n** {{msg-mw|Timedmedia-webm}}\n** {{msg-mw|Timedmedia-mp4}}\n** {{msg-mw|Timedmedia-flac}}\n** {{msg-mw|Timedmedia-wav}}\nSee also:\n* {{msg-mw|Timedmedia-source-audio-file-desc}}\n* {{msg-mw|Timedmedia-source-file-desc}}\n{{Identical|Source}}",
"timedmedia-source-file-desc": "Source file description. This is a file title in the primary source attributes of a timed media file. I can look like \"''Original Ogg file, 640 × 480 (10 Mbps)''\".\n\nParameters:\n* $1 - file type; any one of the following messages:\n** {{msg-mw|Timedmedia-ogg}}\n** {{msg-mw|Timedmedia-webm}}\n** {{msg-mw|Timedmedia-mp4}}\n** {{msg-mw|Timedmedia-flac}}\n** {{msg-mw|Timedmedia-wav}}\n* $2 - resolution width\n* $3 - resolution height\n* $4 - human readable bitrate\nSee also:\n* {{msg-mw|Timedmedia-source-audio-file-desc}} - for audio file",
"timedmedia-source-audio-file-desc": "Source file description. Parameters:\n* $1 - file type; any one of the following messages:\n** {{msg-mw|Timedmedia-ogg}}\n** {{msg-mw|Timedmedia-webm}}\n** {{msg-mw|Timedmedia-mp4}}\n** {{msg-mw|Timedmedia-flac}}\n** {{msg-mw|Timedmedia-wav}}\n* $2 - human readable bitrate\nSee also:\n* {{msg-mw|Timedmedia-source-file-desc}} - for video file",
"timedmedia-derivative-160p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-160p.ogv": "A type of media format encoding, translate at least \"Low bandwidth\"",
"timedmedia-derivative-240p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-240p.ogv": "A type of media format encoding, translate at least \"Small\"",
"timedmedia-derivative-360p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-360p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-480p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-480p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-720p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-720p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-1080p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1080p.ogv": "{{optional}} A type of media format encoding",
"timedmedia-derivative-160p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-160p.webm": "A type of media format encoding, translate at least \"Low bandwidth\"",
"timedmedia-derivative-240p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-240p.webm": "A type of media format encoding, translate at least \"Small\"",
"timedmedia-derivative-360p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-360p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-480p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-480p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-720p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-720p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-1080p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1080p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-1440p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1440p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-2160p.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-2160p.webm": "{{optional}}\nA type of media format encoding",
"timedmedia-derivative-120p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-120p.vp9.webm": "A type of media format encoding, translate at least \"Lowest bandwidth\"",
"timedmedia-derivative-180p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-180p.vp9.webm": "A type of media format encoding, translate at least \"Low bandwidth\"",
"timedmedia-derivative-240p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-240p.vp9.webm": "A type of media format encoding, translate at least \"Small\"",
"timedmedia-derivative-360p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-360p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-480p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-480p.vp9.webm": "{{optional}}\nA type of media format encoding",
"timedmedia-derivative-720p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-720p.vp9.webm": "{{optional}}\nA type of media format encoding",
"timedmedia-derivative-1080p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1080p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-1440p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1440p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-2160p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-2160p.vp9.webm": "{{optional}} A type of media format encoding",
"timedmedia-derivative-160p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-160p.mp4": "A type of media format encoding, translate at least \"Low bandwidth\"",
"timedmedia-derivative-240p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-240p.mp4": "A type of media format encoding, translate at least \"Small\"",
"timedmedia-derivative-320p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-320p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-360p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-360p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-480p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-480p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-720p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-720p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-1080p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1080p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-1440p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-1440p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-2160p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-desc-2160p.mp4": "{{optional}} A type of media format encoding",
"timedmedia-derivative-ogg": "{{optional}} Short form of a media format.",
"timedmedia-derivative-desc-ogg": "{{optional}} Media format.",
"timedmedia-derivative-opus": "{{optional}} Short form of a media format.",
"timedmedia-derivative-desc-opus": "{{optional}} Media format.",
"timedmedia-derivative-mp3": "{{optional}} Short form of a media format.",
"timedmedia-derivative-desc-mp3": "{{optional}} Media format.",
"timedmedia-derivative-m4a": "{{optional}} Short form of a media format.",
"timedmedia-derivative-desc-m4a": "{{optional}} Media format.",
"timedmedia-resolution-120": "Label for video player resolution switcher for 120p low resolution.",
"timedmedia-resolution-160": "Label for video player resolution switcher for 160p low resolution.",
"timedmedia-resolution-180": "Label for video player resolution switcher for 180p low resolution.",
"timedmedia-resolution-240": "Label for video player resolution switcher for 240p low resolution.",
+ "timedmedia-resolution-288": "Label for video player resolution switcher for 288p low resolution.",
"timedmedia-resolution-360": "{{optional}}\nLabel for video player resolution switcher for 360p standard definition.",
"timedmedia-resolution-480": "{{optional}}\nLabel for video player resolution switcher for 480p standard definition.",
"timedmedia-resolution-720": "{{optional}}\nLabel for video player resolution switcher for 720p high definition.",
"timedmedia-resolution-1080": "{{optional}}\nLabel for video player resolution switcher for 1080p high definition.",
"timedmedia-resolution-1440": "{{optional}}\nLabel for video player resolution switcher for 1440p ultra-high definition.",
"timedmedia-resolution-2160": "{{optional}}\nLabel for video player resolution switcher for 2160p ultra-high definition.",
"timedmedia-subtitle-new": "Used as page title.",
"timedmedia-subtitle-new-desc": "Refers to {{msg-mw|Timedmedia-subtitle-new-go}}.",
"timedmedia-subtitle-new-go": "Used as label for the Submit button.\n\nPreceded by (and used in) the description {{msg-mw|Timedmedia-subtitle-new-desc}}.\n{{Identical|Go}}",
"timedmedia-subtitle-language": "{{optional}}\nSubtitle names. Parameters are:\n* $1 - subtitle language\n* $2 - subtitle key",
"timedmedia-subtitle-no-video": "Message for a TimedText page without an associated video file",
"timedmedia-subtitle-no-subtitles": "Parameters:\n* $1 is a language name.",
"timedmedia-subtitle-remote": "Used as page title. Parameters:\n* $1 - the display name of the repository\nThe page body for this page title is:\n* {{msg-mw|timedmedia-subtitle-remote-link}}",
"timedmedia-subtitle-remote-link": "Used as page body. Parameters:\n* $1 - the description URL of the file\n* $2 - the display name of the repository\nThe page title for this message is:\n* {{msg-mw|timedmedia-subtitle-remote}}",
"timedmedia-subtitles-available": "Accessibility label to indicate if a media file has subtitles available. Represented with a CC-icon overlayed on top of the placeholder to begin media playback.",
"timedmedia-derivative-state-transcodes": "Number of transcodes. Parameters:\n* $1 - number of transcodes",
"timedmedia-derivative-state-active": "currently active transcodes\nParameters are:\n* $1 number of transcodes",
"timedmedia-derivative-state-queued": "queued transcode jobs\nParameters are:\n* $1 number of transcodes",
"timedmedia-derivative-state-failed": "failed transcode jobs\nParameters are:\n* $1 number of transcodes",
"timedmedia-derivative-state-missing": "uninitialized transcodes\nParameters are:\n* $1 number of transcodes",
"timedmedia-no-derivatives": "Shown on file description page instead of a table with the transcoded derivates if this file does not require transcoding.",
"timedmedia-file": "Used as table column header.\n{{Identical|File}}",
"timedmedia-dialog-close": "Close button for the media player dialog",
"timedmedia-duration-hms": "Accessibility label for the duration of the media file, shown as a label on the placeholder to begin playback of the media file.\nParameters are:\n* $1 number of hours\n* $2 number of minutes\n* $3 number of seconds",
"timedmedia-duration-ms": "Accessibility label for the duration of the media file, shown as a label on the placeholder to begin playback of the media file.\nParameters are:\n* $1 number of minutes\n* $2 number of seconds",
"timedmedia-duration-s": "Accessibility label for the duration of the media file, shown as a label on the placeholder to begin playback of the media file.\nParameters are:\n* $1 number of seconds",
"right-transcode-reset": "{{doc-right|transcode-reset}}",
"right-transcode-status": "{{doc-right|transcode-status}}",
"action-transcode-reset": "{{doc-action|transcode-reset}}",
"action-transcode-status": "{{doc-action|transcode-status}}",
"log-name-timedmediahandler": "{{doc-logpage}}\n\nAs well as log page title and page header for [[Special:Log/timedmediahandler]].",
"log-description-timedmediahandler": "Log description on [[Special:Log/timedmediahandler]].",
"logentry-timedmediahandler-resettranscode": "{{logentry|[[Special:Log/timedmediahandler]]}}\nParameters:\n* $4 - format to transcode to, e.g. '480p.ogv'",
"orphanedtimedtext": "{{doc-special|OrphanedTimedText}}",
"orphanedtimedtext-summary": "Summary of Special:OrphanedTimedText.",
"orphanedtimedtext-unsupported": "Shown if Special:OrphanedTimedText isn't supported for the current database back-end.",
"videojs-more-information": "Used as tooltip for more information button in video.js toolbar",
"videojs-quality": "Used as tooltip for resolution selector in video.js toolbar",
"videojs-subtitles-create": "Menu entry in the subtitles menu of the video player, to navigate to the create subtitles interface.",
"videojs-captions-create": "Menu entry in the captions menu of the video player, to navigate to the create captions interface. In some languages captions are known as subtitles.",
"transcodestatistics": "{{doc-special|TranscodeStatistics}}",
"timedmedia-timedtext-title-edit-subtitles": "$1 is language name for subtitles (e.g. \"English\"), $2 is title of file that timed text is for.\nShown as the page title on a TimedText namespace page when the page exists. See also {{msg-mw|timedmedia-timedtext-title-create-subtitles}}.",
"timedmedia-timedtext-title-create-subtitles": "$1 is language name, $2 is title of file that timed text is for.\nShown as the page title on a TimedText namespace page when the page does not exist. See also {{msg-mw|timedmedia-timedtext-title-edit-subtitles}}."
}
diff --git a/includes/ApiTranscodeReset.php b/includes/ApiTranscodeReset.php
index 96b15672..b3e3675e 100644
--- a/includes/ApiTranscodeReset.php
+++ b/includes/ApiTranscodeReset.php
@@ -1,183 +1,184 @@
getConfig()->get( 'EnableTranscode' ) ) {
$this->dieWithError( 'apierror-timedmedia-disabledtranscode', 'disabledtranscode' );
}
$params = $this->extractRequestParams();
$titleObj = Title::newFromText( $params['title'] );
// Make sure we have a valid Title
if ( !$titleObj || $titleObj->isExternal() ) {
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
}
// Check that the user has permmission to reset transcodes on the file
$this->checkTitleUserPermissions( $titleObj, 'transcode-reset' );
// Make sure the title can be transcoded
if ( !Hooks::isTranscodableTitle( $titleObj ) ) {
$this->dieWithError(
[
'apierror-timedmedia-invalidtranscodetitle',
wfEscapeWikiText( $titleObj->getPrefixedText() )
],
'invalidtranscodetitle'
);
}
$transcodeKey = false;
// Make sure it's an enabled transcode key we are trying to remove:
// ( if you update your transcode keys the api is not how you purge the database of expired keys )
if ( isset( $params['transcodekey'] ) ) {
$transcodeSet = WebVideoTranscode::enabledTranscodes();
if ( !in_array( $params['transcodekey'], $transcodeSet, true ) ) {
$this->dieWithError(
[ 'apierror-timedmedia-badtranscodekey', wfEscapeWikiText( $params['transcodekey'] ) ],
'badtranscodekey'
);
} else {
$transcodeKey = $params['transcodekey'];
}
}
// Don't reset if less than 1 hour has passed and we have no error )
$file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $titleObj );
$timeSinceLastReset = self::checkTimeSinceLastRest( $file, $transcodeKey );
$waitTimeForTranscodeReset = $this->getConfig()->get( 'WaitTimeForTranscodeReset' );
if ( $timeSinceLastReset < $waitTimeForTranscodeReset ) {
$msg = $this->msg(
'apierror-timedmedia-notenoughtimereset',
TimedMediaHandler::getTimePassedMsg( $waitTimeForTranscodeReset - $timeSinceLastReset )
);
$this->dieWithError( $msg, 'notenoughtimereset' );
}
// All good do the transcode removal:
WebVideoTranscode::removeTranscodes( $file, $transcodeKey );
// Oh and we wanted to reset it, right? Trigger again.
- WebVideoTranscode::updateJobQueue( $file, $transcodeKey );
+ $manualOverride = true;
+ WebVideoTranscode::updateJobQueue( $file, $transcodeKey, $manualOverride );
$logEntry = new ManualLogEntry( 'timedmediahandler', 'resettranscode' );
$logEntry->setPerformer( $this->getUser() );
$logEntry->setTarget( $titleObj );
$logEntry->setParameters( [
'4::transcodekey' => $transcodeKey,
] );
$logEntry->insert();
$this->getResult()->addValue( null, 'success', 'removed transcode' );
}
/**
* @param File $file
* @param string|false $transcodeKey
* @return int|string
*/
public static function checkTimeSinceLastRest( $file, $transcodeKey ) {
global $wgWaitTimeForTranscodeReset;
$transcodeStates = WebVideoTranscode::getTranscodeState( $file );
if ( $transcodeKey ) {
if ( !$transcodeStates[$transcodeKey] ) {
// transcode key not found
return $wgWaitTimeForTranscodeReset + 1;
}
return self::getStateResetTime( $transcodeStates[$transcodeKey] );
}
// least wait is set to reset time:
$leastWait = $wgWaitTimeForTranscodeReset + 1;
// else check for lowest reset time
foreach ( $transcodeStates as $state ) {
$ctime = self::getStateResetTime( $state );
if ( $ctime < $leastWait ) {
$leastWait = $ctime;
}
}
return $leastWait;
}
/**
* @param array $state
* @return int|string
*/
public static function getStateResetTime( $state ) {
global $wgWaitTimeForTranscodeReset;
$db = wfGetDB( DB_REPLICA );
// if an error return waitTime +1
if ( $state['time_error'] !== null ) {
return $wgWaitTimeForTranscodeReset + 1;
}
// return wait time from most recent event
foreach ( [ 'time_success', 'time_startwork', 'time_addjob' ] as $timeField ) {
if ( ( $state[ $timeField ] ) !== null ) {
return (int)$db->timestamp() - (int)$db->timestamp( $state[ $timeField ] );
}
}
// No time info, return resetWaitTime
return $wgWaitTimeForTranscodeReset + 1;
}
public function mustBePosted() {
return true;
}
public function isWriteMode() {
return true;
}
/** @inheritDoc */
protected function getAllowedParams() {
return [
'title' => [
ParamValidator::PARAM_TYPE => 'string',
ParamValidator::PARAM_REQUIRED => true
],
'transcodekey' => null,
'token' => null,
];
}
public function needsToken() {
return 'csrf';
}
/**
* @see ApiBase::getExamplesMessages()
* @return array
*/
protected function getExamplesMessages() {
return [
'action=transcodereset&title=File:Clip.webm&token=123ABC'
=> 'apihelp-transcodereset-example-1',
'action=transcodereset&title=File:Clip.webm&transcodekey=360_560kbs.webm&token=123ABC'
=> 'apihelp-transcodereset-example-2',
];
}
}
diff --git a/includes/Handlers/ID3Handler/ID3Handler.php b/includes/Handlers/ID3Handler/ID3Handler.php
index e018e4f4..28e7935a 100644
--- a/includes/Handlers/ID3Handler/ID3Handler.php
+++ b/includes/Handlers/ID3Handler/ID3Handler.php
@@ -1,126 +1,126 @@
option_tag_id3v1 = false;
// Read and process ID3v2 tags
$getID3->option_tag_id3v2 = false;
// Read and process Lyrics3 tags
$getID3->option_tag_lyrics3 = false;
// Read and process APE tags
$getID3->option_tag_apetag = false;
// Copy tags to root key 'tags' and encode to $this->encoding
$getID3->option_tags_process = false;
// Copy tags to root key 'tags_html' properly translated from various encodings to HTML entities
$getID3->option_tags_html = false;
// Analyze file to get metadata structure:
$id3 = $getID3->analyze( $path );
// remove file paths
unset( $id3['filename'] );
unset( $id3['filepath'] );
unset( $id3['filenamepath'] );
// Update the version
$id3['version'] = self::METADATA_VERSION;
return $id3;
}
/**
* @param File $file
* @param string $path
* @return string
*/
public function getMetadata( $file, $path ) {
$id3 = $this->getID3( $path );
return serialize( $id3 );
}
/**
* @param string $metadata
* @return false|mixed
*/
public function unpackMetadata( $metadata ) {
AtEase::suppressWarnings();
$unser = unserialize( $metadata );
AtEase::restoreWarnings();
if ( isset( $unser['version'] ) && $unser['version'] === self::METADATA_VERSION ) {
return $unser;
}
return false;
}
/**
* @param File $file
* @return mixed
*/
public function getBitrate( $file ) {
$metadata = $this->unpackMetadata( $file->getMetadata() );
if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['bitrate'] ) ) {
return 0;
}
return $metadata['bitrate'];
}
/**
* @param File $file
* @return int
*/
public function getLength( $file ) {
$metadata = $this->unpackMetadata( $file->getMetadata() );
if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['playtime_seconds'] ) ) {
return 0;
}
return $metadata['playtime_seconds'];
}
/**
* @param File $file
- * @return false|int
+ * @return float framerate as floating point; 0 indicates no valid rate data
*/
public function getFramerate( $file ) {
$metadata = $this->unpackMetadata( $file->getMetadata() );
if ( !$metadata || isset( $metadata['error'] ) ) {
return 0;
}
// return the frame rate of the first found video stream:
- return $metadata['video']['frame_rate'] ?? false;
+ return (float)( $metadata['video']['frame_rate'] ?? 0 );
}
/**
* Returns true if the file contains an interlaced video track.
* @param File $file
* @return bool
*/
public function isInterlaced( $file ) {
$metadata = $this->unpackMetadata( $file->getMetadata() );
if ( !$metadata || isset( $metadata['error'] ) ) {
return false;
}
return (bool)( $metadata['video']['interlaced'] ?? false );
}
}
diff --git a/includes/TimedMediaHandler.php b/includes/TimedMediaHandler.php
index 18e4d028..702d4c5f 100644
--- a/includes/TimedMediaHandler.php
+++ b/includes/TimedMediaHandler.php
@@ -1,495 +1,512 @@
'width',
'timedmedia_thumbtime' => 'thumbtime',
'timedmedia_starttime' => 'start',
'timedmedia_endtime' => 'end',
'timedmedia_disablecontrols' => 'disablecontrols',
'timedmedia_loop' => 'loop',
'timedmedia_muted' => 'muted',
];
}
/**
* Validate a embed file parameters
*
* @param string $name Name of the param
* @param mixed $value Value to validated
* @return bool
*/
public function validateParam( $name, $value ) {
if ( $name === 'thumbtime' || $name === 'start' || $name === 'end' ) {
if ( self::parseTimeString( $value ) === false ) {
return false;
}
} elseif ( $name === 'disablecontrols' ) {
$values = explode( ',', $value );
foreach ( $values as $v ) {
if ( !in_array( $v, [ 'options', 'timedText', 'fullscreen' ] ) ) {
return false;
}
}
} elseif ( $name === 'width' || $name === 'height' ) {
return $value > 0;
}
return true;
}
/**
* TODO we should really have "$file" available here to validate the param string
* @param array $params
* @return string
*/
public function makeParamString( $params ) {
// Add the width param string ( same as images {width}px )
$paramString = ( isset( $params['width'] ) ) ? $params['width'] . 'px' : '';
$paramString .= ( $paramString !== '' ) ? '-' : '';
// Get the raw thumbTime from thumbtime or start param
if ( isset( $params['thumbtime'] ) ) {
$thumbTime = $params['thumbtime'];
} elseif ( isset( $params['start'] ) ) {
$thumbTime = $params['start'];
} else {
$thumbTime = false;
}
if ( $thumbTime !== false ) {
$time = self::parseTimeString( $thumbTime );
if ( $time !== false ) {
return $paramString . 'seek=' . $time;
}
}
if ( !$paramString ) {
$paramString = 'mid';
}
return $paramString;
}
/**
* Used by thumb.php to find url parameters
*
* @param string $str
* @return array|false Array of thumbnail parameters, or false if string cannot be parsed
*/
public function parseParamString( $str ) {
$params = [];
if ( preg_match( '/^(mid|(\d*)px-)*(seek=([\d.]+))*$/', $str, $matches ) ) {
$size = $thumbtime = null;
if ( isset( $matches[2] ) ) {
$size = $matches[2];
}
if ( isset( $matches[4] ) ) {
$thumbtime = $matches[4];
}
if ( $size !== null && $size !== '' ) {
$params['width'] = (int)$size;
}
if ( $thumbtime !== null ) {
$params['thumbtime'] = (float)$thumbtime;
}
// valid thumbnail URL
return $params;
}
// invalid parameter string
return false;
}
/**
* @param File $image
* @param array &$params
* @return bool
*/
public function normaliseParams( $image, &$params ) {
$timeParam = [ 'thumbtime', 'start', 'end' ];
// Parse time values if endtime or thumbtime can't be more than length -1
foreach ( $timeParam as $pn ) {
if ( isset( $params[$pn] ) && $params[$pn] !== false ) {
$length = $this->getLength( $image );
$time = self::parseTimeString( $params[$pn] );
if ( $time === false ) {
return false;
}
if ( $time > $length - 1 ) {
$params[$pn] = $length - 1;
} elseif ( $time <= 0 ) {
$params[$pn] = 0;
}
}
}
if ( $this->isAudio( $image ) ) {
// Assume a default for audio files
$size = [
'width' => 220,
'height' => 23,
];
} else {
$size = [
'width' => $image->getWidth(),
'height' => $image->getHeight(),
];
}
// Make sure we don't try and up-scale the asset:
if ( !$this->isAudio( $image ) && isset( $params['width'] )
&& (int)$params['width'] > $size['width']
) {
$params['width'] = $size['width'];
}
if ( isset( $params['height'] ) && $params['height'] !== -1 ) {
if ( $params['width'] * $size['height'] > $params['height'] * $size['width'] ) {
$params['width'] = self::fitBoxWidth( $size['width'], $size['height'], $params['height'] );
}
}
if ( isset( $params['width'] ) ) {
$params['height'] = File::scaleHeight( $size['width'], $size['height'], $params['width'] );
}
// Make sure start time is not > than end time
if (
isset( $params['start'] ) && isset( $params['end'] ) &&
$params['start'] !== false &&
$params['end'] !== false &&
( self::parseTimeString( $params['start'] ) > self::parseTimeString( $params['end'] ) )
) {
return false;
}
foreach ( [ 'loop', 'muted' ] as $flag ) {
$params[ $flag ] = isset( $params[ $flag ] );
}
return true;
}
/**
* Parser output hook only adds the required modules
*
* The core embedPlayer module lazy loaded by the loader modules
*
* @param Parser $parser
* @param ?File $file
*/
public function parserTransformHook( $parser, $file ) {
$parserOutput = $parser->getOutput();
if ( $parserOutput->getExtensionData( 'mw_ext_TMH_hasTimedMediaTransform' ) ) {
return;
}
$parserOutput->addModuleStyles( [ 'ext.tmh.player.styles' ] );
$parserOutput->addModules( [ 'ext.tmh.player' ] );
$parserOutput->setExtensionData( 'mw_ext_TMH_hasTimedMediaTransform', true );
}
/**
* Utility functions
* @param string $timeString
* @param false|int $length
* @return false|int
*/
public static function parseTimeString( $timeString, $length = false ) {
$parts = explode( ':', $timeString );
$time = 0;
$partsCount = count( $parts );
// Check for extra :s
if ( $partsCount > 3 ) {
return false;
}
foreach ( $parts as $i => $iValue ) {
if ( !is_numeric( $iValue ) ) {
return false;
}
$time += (float)$iValue * pow( 60, $partsCount - $i - 1 );
}
if ( $time < 0 ) {
wfDebug( __METHOD__ . ": specified negative time, using zero\n" );
return 0;
}
// We don't need more than millisecond precisions
// And for duration (length) seconds precision is ok
$time = $length ? ceil( $time ) : round( $time, 3 );
if ( $length !== false && $time > $length - 1 ) {
wfDebug( __METHOD__ .
": specified near-end or past-the-end time {$time}s, using end minus 1s\n" );
$time = $length - 1;
}
return $time;
}
/**
* @param int $timePassed
* @return string|array As from Message::listParam if available, otherwise
* a corresponding string in the language from $wgLang.
*/
public static function getTimePassedMsg( $timePassed ) {
$t = [];
$t['days'] = floor( $timePassed / 60 / 60 / 24 );
$t['hours'] = floor( $timePassed / 60 / 60 ) % 24;
$t['minutes'] = floor( $timePassed / 60 ) % 60;
$t['seconds'] = $timePassed % 60;
foreach ( $t as $k => $v ) {
if ( !$v ) {
unset( $t[$k] );
} else {
// Give grep a chance to find the usages:
// timedmedia-days, timedmedia-hours, timedmedia-minutes,timedmedia-seconds
$t[$k] = wfMessage( 'timedmedia-' . $k, $v );
}
}
if ( count( $t ) === 0 ) {
$t = [ wfMessage( 'timedmedia-seconds', 0 ) ];
}
if ( is_callable( [ 'Message', 'listParam' ] ) ) {
return Message::listParam( array_values( $t ), 'comma' );
}
global $wgLang;
return $wgLang->commaList( array_map( static function ( $m ) {
return $m->text();
}, $t ) );
}
/**
* Converts seconds to Normal play time (NPT) time format:
* consist of hh:mm:ss.ms
* also see: http://www.ietf.org/rfc/rfc2326.txt section 3.6
*
* @param int $time Seconds to be converted to npt time format
* @return false|string
*/
public static function seconds2npt( $time ) {
if ( !is_numeric( $time ) ) {
wfDebug( __METHOD__ . ": trying to get npt time on NaN: " . $time );
return false;
}
if ( $time < 0 ) {
wfDebug( __METHOD__ . ": trying to time on negative value: " . $time );
return false;
}
$hours = floor( $time / 3600 );
$min = floor( ( $time / 60 ) % 60 );
$sec = floor( $time % 60 );
$ms = floor( $time * 1000 % 1000 );
$ms = ( $ms != 0 ) ? sprintf( '.%03d', $ms ) : '';
return sprintf( '%02d:%02d:%02d%s', $hours, $min, $sec, $ms );
}
/**
* @param string $metadata
* @return false|mixed
*/
public function unpackMetadata( $metadata ) {
AtEase::suppressWarnings();
$unser = unserialize( $metadata );
AtEase::restoreWarnings();
if ( isset( $unser['version'] ) ) {
return $unser;
}
return false;
}
/**
* @param File $image
* @param string $metadata
* @return bool
*/
public function isMetadataValid( $image, $metadata ) {
return $this->unpackMetadata( $metadata ) !== false;
}
/**
* @param string $ext
* @param string $mime
* @param null $params
* @return array
*/
public function getThumbType( $ext, $mime, $params = null ) {
return [ 'jpg', 'image/jpeg' ];
}
/**
* checks if a given file is an audio file
* @param File $file
* @return bool
*/
public function isAudio( $file ) {
return ( !$file->getWidth() && !$file->getHeight() );
}
/**
* @param File $file
* @param string $dstPath
* @param string $dstUrl
* @param array $params
* @param int $flags
* @return bool|MediaTransformError|MediaTransformOutput|TimedMediaTransformOutput
*/
public function doTransform( $file, $dstPath, $dstUrl, $params, $flags = 0 ) {
# Important or height handling is wrong.
if ( !$this->normaliseParams( $file, $params ) ) {
return new TransformParameterError( $params );
}
$options = [
'file' => $file,
'length' => $this->getLength( $file ),
'offset' => $this->getOffset( $file ),
// Default thumbnail width and height for audio files is hardcoded to match the dimensions of
// the filetype icon, see TimedMediaTransformOutput::getUrl(). Overridden for video below.
'width' => $params['width'] ?? 120,
// Height is ignored for audio files anyway, and $params['height'] might be set to 0
'height' => $params['width'] ?? 120,
'isVideo' => !$this->isAudio( $file ),
'thumbtime' => $params['thumbtime'] ?? (int)( $file->getLength() / 2 ),
'start' => $params['start'] ?? false,
'end' => $params['end'] ?? false,
'fillwindow' => $params['fillwindow'] ?? false,
'disablecontrols' => $params['disablecontrols'] ?? false,
'loop' => $params['loop'] ?? false,
'muted' => $params['muted'] ?? false,
'inline' => $params['inline'] ?? false,
];
// Allow start and end query string params on image pages (T203994)
if ( isset( $params['imagePageParams'] ) ) {
$requestParams = $params['imagePageParams'];
if ( !$options['start'] ) {
$options['start'] = $requestParams[ 'start' ] ?? false;
}
if ( !$options['end'] ) {
$options['end'] = $requestParams[ 'end' ] ?? false;
}
}
// No thumbs for audio
if ( !$options['isVideo'] ) {
return new TimedMediaTransformOutput( $options );
}
// We're dealing with a video file now, set width and height
$srcWidth = $file->getWidth();
$srcHeight = $file->getHeight();
$params['width'] = $params['width'] ?? $srcWidth;
// if height overtakes width use height as max:
$targetWidth = $params['width'];
$targetHeight = $srcWidth ? round( $params['width'] * $srcHeight / $srcWidth ) : $srcHeight;
if ( isset( $params['height'] ) && $targetHeight > $params['height'] ) {
$targetHeight = $params['height'];
$targetWidth = round( $params['height'] * $srcWidth / $srcHeight );
}
$options[ 'width' ] = $targetWidth;
$options[ 'height' ] = $targetHeight;
// Setup pointer to thumb arguments
$options[ 'thumbUrl' ] = $dstUrl;
$options[ 'dstPath' ] = $dstPath;
$options[ 'path' ] = $dstPath;
// Check if transform is deferred:
if ( $flags & self::TRANSFORM_LATER ) {
return new TimedMediaTransformOutput( $options );
}
// Generate thumb:
$thumbStatus = TimedMediaThumbnail::get( $options );
if ( $thumbStatus !== true ) {
return $thumbStatus;
}
return new TimedMediaTransformOutput( $options );
}
/**
* @param File $file
* @return bool
*/
public function mustRender( $file ) {
return true;
}
/**
* Get a stream offset time
* @param File $file
* @return int
*/
public function getOffset( $file ) {
return 0;
}
/**
* Get length of a file
* @param File $file
* @return int
*/
public function getLength( $file ) {
return $file->getLength();
}
/**
* @param File $file
* @return string
*/
public function getDimensionsString( $file ) {
global $wgLang;
if ( $file->getWidth() ) {
return wfMessage( 'video-dims', $wgLang->formatTimePeriod( $this->getLength( $file ) ) )
->numParams( $file->getWidth(), $file->getHeight() )->text();
}
return $wgLang->formatTimePeriod( $this->getLength( $file ) );
}
+ /**
+ * Return frame rate, if applicable, or 0 if no valid data.
+ * Subclasses will implement relevant metadata extraction.
+ *
+ * Note that values returned as floating point are not exact for
+ * NTSC/ATSC video with 30000/1001, 60000/1001, or 24000/1001
+ * frame rates!
+ *
+ * Note interlacing should be checked separately if relevant.
+ *
+ * @param File $file
+ * @return float
+ */
+ public function getFramerate( $file ) {
+ return 0;
+ }
+
/**
* Returns true if the file contains an interlaced video track.
* @param File $file
* @return bool
*/
public function isInterlaced( $file ) {
return false;
}
}
diff --git a/includes/TimedMediaThumbnail.php b/includes/TimedMediaThumbnail.php
index 9c46b4ce..9ec014d5 100644
--- a/includes/TimedMediaThumbnail.php
+++ b/includes/TimedMediaThumbnail.php
@@ -1,187 +1,196 @@
getWidth() &&
$options['height'] != $options['file']->getHeight()
) {
return self::resizeThumb( $options );
}
return self::tryFfmpegThumb( $options );
}
/**
* @param array $options
* @return bool|MediaTransformError
*/
private static function tryFfmpegThumb( $options ) {
global $wgFFmpegLocation, $wgMaxShellMemory;
if ( !$wgFFmpegLocation || !is_file( $wgFFmpegLocation ) ) {
return false;
}
$cmd = wfEscapeShellArg( $wgFFmpegLocation ) . ' -nostdin -threads 1 ';
+ $file = $options['file'];
+ $handler = $file->getHandler();
+
$offset = (int)self::getThumbTime( $options );
/*
This is a workaround until ffmpegs ogg demuxer properly seeks to keyframes.
Seek N seconds before offset and seek in decoded stream after that.
-ss before input seeks without decode
-ss after input seeks in decoded stream
N depends on framerate of input, keyframe interval defaults
to 64 for most encoders, seeking a bit before that
*/
- $framerate = $options['file']->getHandler()->getFramerate( $options['file'] );
+ $framerate = $handler->getFramerate( $file );
if ( $framerate > 0 ) {
$seekoffset = 1 + (int)( 64 / $framerate );
} else {
$seekoffset = 3;
}
if ( $offset > $seekoffset ) {
$cmd .= ' -ss ' . (float)( $offset - $seekoffset );
$offset = $seekoffset;
}
// try to get temporary local url to file
- $backend = $options['file']->getRepo()->getBackend();
+ $backend = $file->getRepo()->getBackend();
$src = $backend->getFileHttpUrl( [
- 'src' => $options['file']->getPath()
+ 'src' => $file->getPath()
] );
if ( $src === null ) {
- $src = $options['file']->getLocalRefPath();
+ $src = $file->getLocalRefPath();
}
$cmd .= ' -y -i ' . wfEscapeShellArg( $src );
$cmd .= ' -ss ' . $offset . ' ';
+ // Deinterlace MPEG-2 if necessary
+ if ( $handler->isInterlaced( $file ) ) {
+ // Send one frame only
+ $cmd .= ' -vf yadif=mode=0';
+ }
+
// Set the output size if set in options:
if ( isset( $options['width'] ) && isset( $options['height'] ) ) {
$cmd .= ' -s ' . (int)$options['width'] . 'x' . (int)$options['height'];
}
// MJPEG, that's the same as JPEG except it's supported by the windows build of ffmpeg
// No audio, one frame
$cmd .= ' -f mjpeg -an -vframes 1 ' .
wfEscapeShellArg( $options['dstPath'] ) . ' 2>&1';
$retval = 0;
$returnText = wfShellExec( $cmd, $retval );
// Check if it was successful
if ( !$options['file']->getHandler()->removeBadFile( $options['dstPath'], $retval ) ) {
return true;
}
$returnText = $cmd . "\nwgMaxShellMemory: $wgMaxShellMemory\n" . $returnText;
// Return error box
return new MediaTransformError(
'thumbnail_error', $options['width'], $options['height'], $returnText
);
}
/**
* @param array $options
* @return bool|MediaTransformError
*/
private static function resizeThumb( $options ) {
$file = $options['file'];
$params = [];
foreach ( [ 'start', 'thumbtime' ] as $key ) {
if ( isset( $options[ $key ] ) ) {
$params[ $key ] = $options[ $key ];
}
}
$params["width"] = $file->getWidth();
$params["height"] = $file->getHeight();
$poolKey = $file->getRepo()->getSharedCacheKey( 'file', md5( $file->getName() ) );
$posOptions = array_flip( [ 'start', 'thumbtime' ] );
$poolKey = wfAppendQuery( $poolKey, array_intersect_key( $options, $posOptions ) );
$work = new PoolCounterWorkViaCallback( 'TMHTransformFrame',
'_tmh:frame:' . $poolKey,
[ 'doWork' => static function () use ( $file, $params ) {
return $file->transform( $params, File::RENDER_NOW );
} ] );
$thumb = $work->execute();
if ( !$thumb || $thumb->isError() ) {
return $thumb;
}
$src = $thumb->getStoragePath();
if ( !$src ) {
return false;
}
$localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
$thumbFile = new UnregisteredLocalFile( $file->getTitle(),
$localRepo, $src, false );
$thumbParams = [
"width" => $options['width'],
"height" => $options['height']
];
$handler = $thumbFile->getHandler();
if ( !$handler ) {
return false;
}
$scaledThumb = $handler->doTransform(
$thumbFile,
$options['dstPath'],
$options['dstPath'],
$thumbParams
);
if ( !$scaledThumb || $scaledThumb->isError() ) {
// @phan-suppress-next-line PhanTypeMismatchReturnNullable
return $scaledThumb;
}
return true;
}
/**
* @param array $options
* @return bool|float|int
*/
private static function getThumbTime( $options ) {
$length = $options['file']->getLength();
// If start time param isset use that for the thumb:
if ( isset( $options['start'] ) ) {
$thumbtime = TimedMediaHandler::parseTimeString( $options['start'], $length );
if ( $thumbtime !== false ) {
return $thumbtime;
}
}
// else use thumbtime
if ( isset( $options['thumbtime'] ) ) {
$thumbtime = TimedMediaHandler::parseTimeString( $options['thumbtime'], $length );
if ( $thumbtime !== false ) {
return $thumbtime;
}
}
// Seek to midpoint by default, it tends to be more interesting than the start
return $length / 2;
}
}
diff --git a/includes/WebVideoTranscode/WebVideoTranscode.php b/includes/WebVideoTranscode/WebVideoTranscode.php
index 43b0efae..7bd92400 100644
--- a/includes/WebVideoTranscode/WebVideoTranscode.php
+++ b/includes/WebVideoTranscode/WebVideoTranscode.php
@@ -1,1354 +1,1294 @@
[
'maxSize' => '288x160',
- 'videoBitrate' => '128',
- 'crf' => '10',
- 'audioQuality' => '-1',
+ 'videoBitrate' => '192k',
+ 'speed' => '1',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'bufDelay' => '256',
'videoCodec' => 'vp8',
- 'slices' => '2',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'240p.webm' => [
'maxSize' => '426x240',
- 'videoBitrate' => '256',
- 'crf' => '10',
- 'audioQuality' => '1',
+ 'videoBitrate' => '394k',
+ 'speed' => '1',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'bufDelay' => '256',
'videoCodec' => 'vp8',
- 'slices' => '2',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'360p.webm' => [
'maxSize' => '640x360',
- 'videoBitrate' => '512',
- 'crf' => '10',
- 'audioQuality' => '1',
+ 'videoBitrate' => '785k',
+ 'speed' => '1',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'bufDelay' => '256',
- 'videoCodec' => 'vp8',
'slices' => '2',
+ 'videoCodec' => 'vp8',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'480p.webm' => [
'maxSize' => '854x480',
- 'videoBitrate' => '1024',
- 'crf' => '10',
- 'audioQuality' => '2',
+ 'videoBitrate' => '1280k',
+ 'speed' => '1',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'bufDelay' => '256',
+ 'slices' => '2',
'videoCodec' => 'vp8',
- 'slices' => '4',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'720p.webm' => [
'maxSize' => '1280x720',
- 'videoBitrate' => '2048',
- 'crf' => '10',
- 'audioQuality' => '3',
+ 'videoBitrate' => '2551k',
+ 'speed' => '2',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'videoCodec' => 'vp8',
'slices' => '4',
- 'speed' => '1',
+ 'videoCodec' => 'vp8',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'1080p.webm' => [
'maxSize' => '1920x1080',
- 'videoBitrate' => '4096',
- 'crf' => '10',
- 'audioQuality' => '3',
+ 'videoBitrate' => '5082k',
+ 'speed' => '2',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'videoCodec' => 'vp8',
'slices' => '4',
- 'speed' => '1',
+ 'videoCodec' => 'vp8',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'1440p.webm' => [
'maxSize' => '2560x1440',
- 'videoBitrate' => '8192',
- 'crf' => '10',
- 'audioQuality' => '3',
+ 'videoBitrate' => '8288k',
+ 'speed' => '2',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'videoCodec' => 'vp8',
'slices' => '8',
- 'speed' => '2',
+ 'videoCodec' => 'vp8',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
'2160p.webm' => [
'maxSize' => '3840x2160',
- 'videoBitrate' => '16384',
- 'crf' => '10',
- 'audioQuality' => '3',
+ 'videoBitrate' => '16513k',
+ 'speed' => '2',
'twopass' => 'true',
- 'keyframeInterval' => '240',
- 'videoCodec' => 'vp8',
'slices' => '8',
- 'speed' => '2',
+ 'videoCodec' => 'vp8',
+ 'audioCodec' => 'vorbis',
+ 'audioBitrate' => '112k',
'type' => 'video/webm; codecs="vp8, vorbis"',
],
// WebM VP9 transcode:
//
- // These configurations are meant to balance
- // * quality (try to maintain quality until size gets too large)
- // * size (aim for smaller files when possible)
- // * speed (use modest "speed" setting to gain speed at a little bandwidth/quality cost)
- //
- // A large bitrate target is used to allow preserving good quality for highly
- // detailed sources and those with high frame rates and motion, which were not
- // served well under the previous VP8 settings. The qmin is set close to the
- // crf constrained quality target to keep from adding any extra bits when not
- // needed; we're always transcoding something with its own compression artifacts
- // and there's no need to reproduce every last bit.
- //
- // This usually results in files at or significantly below target when there's
- // relatively little detail/motion, and files bigger towards the inflated target
- // (set around 4x what we'd really want as a target) are allowed to better handle
- // those high-frame-rate or high-motion/high-detail files.
- //
// Use of two-pass encoding increases runtime by 2/3 but significantly increases
- // quality through enabling auto alt reference frames. Use of 'speed' param at 2
- // instead of 0 or 2 makes things a little faster at very slight cost of bandwidth.
+ // quality and rate control. The encoding -speed parameter is used aggressively
+ // to speed up higher resolutions; alt-ref is dropped for speed at a modest cost
+ // in bitrate.
//
// Encoding speed is greatly affected by threading settings; HD videos can use up to
// 8 threads with a suitable ffmpeg/libvpx and $wgFFmpegVP9RowMT enabled ("row-mt").
// Ultra-HD can use up to 16 threads. Be sure to set $wgFFmpegThreads to a suitable
// maximum values!
//
'120p.vp9.webm' => [
'maxSize' => '213x120',
- // target 60 x 2
- 'videoBitrate' => '120',
- 'crf' => '35',
- 'qmin' => '8',
+ 'videoBitrate' => '95k',
+ 'speed' => '1',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'180p.vp9.webm' => [
'maxSize' => '320x180',
- // target 100 x 2
- 'videoBitrate' => '200',
- 'crf' => '35',
- 'qmin' => '9',
+ 'videoBitrate' => '189k',
+ 'speed' => '1',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'240p.vp9.webm' => [
'maxSize' => '426x240',
- // target 160 x 2
- 'videoBitrate' => '320',
- 'crf' => '35',
- 'qmin' => '11',
+ 'videoBitrate' => '308k',
+ 'speed' => '1',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'samplerate' => '48000',
- 'audioBitrate' => '96',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'360p.vp9.webm' => [
'maxSize' => '640x360',
- // target 320 x 2
- 'videoBitrate' => '640',
- 'crf' => '35',
- 'qmin' => '12',
+ 'videoBitrate' => '613k',
+ 'speed' => '1',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
+ 'tileColumns' => '1',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
- 'tileColumns' => '1',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
- ],
+ ],
'480p.vp9.webm' => [
'maxSize' => '854x480',
- // target 640 x 2
- 'videoBitrate' => '1280',
- 'crf' => '33',
- 'qmin' => '10',
- 'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
+ 'videoBitrate' => '1000k',
'speed' => '2',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
+ 'twopass' => 'true',
+ 'tileColumns' => '1',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
- 'tileColumns' => '1',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'720p.vp9.webm' => [
'maxSize' => '1280x720',
- // target 1280 x 2
- 'videoBitrate' => '2560',
- 'crf' => '32',
- 'qmin' => '10',
+ 'videoBitrate' => '1993k',
+ 'speed' => '3',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
+ 'tileColumns' => '2',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
- 'tileColumns' => '2',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'1080p.vp9.webm' => [
'maxSize' => '1920x1080',
- // target 2560 x 2
- 'videoBitrate' => '5120',
- 'crf' => '31',
- 'qmin' => '9',
+ 'videoBitrate' => '3971k',
+ 'speed' => '3',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
+ 'tileColumns' => '2',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
- 'tileColumns' => '4',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'1440p.vp9.webm' => [
'maxSize' => '2560x1440',
- // target 5120 x 2
- 'videoBitrate' => '10240',
- 'crf' => '24',
- 'qmin' => '8',
+ 'videoBitrate' => '6475k',
+ 'speed' => '4',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
+ 'tileColumns' => '3',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
- 'tileColumns' => '4',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
'2160p.vp9.webm' => [
'maxSize' => '3840x2160',
- // target 10240 x 2
- 'videoBitrate' => '20480',
- 'crf' => '24',
- 'qmin' => '8',
+ 'videoBitrate' => '12900k',
+ 'speed' => '4',
+ 'altref' => '0',
+ 'lagInFrames' => '0',
'twopass' => 'true',
- 'altref' => 'true',
- 'keyframeInterval' => '240',
- 'speed' => '2',
+ 'tileColumns' => '3',
'videoCodec' => 'vp9',
'audioCodec' => 'opus',
- 'audioBitrate' => '96',
- 'samplerate' => '48000',
- 'tileColumns' => '4',
+ 'audioBitrate' => '96k',
'type' => 'video/webm; codecs="vp9, opus"',
],
// Loosely defined per PCF guide to mp4 profiles:
// https://develop.participatoryculture.org/index.php/ConversionMatrix
// and apple HLS profile guide:
// https://developer.apple.com/library/ios/#documentation/networkinginternet/conceptual/streamingmediaguide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-DontLinkElementID_24
+ // high profile
+ // level 2 needed for 160p60
+ // level 2.1 needed for 240p60
+ // level 3 needed for 360p60, 480p60
+ // level 4 needed for 720p60, 1080p30
+ // level 4.1 needed for 1080p60
+ // level 5 needed for 1440p60, 2160p30
+ // level 5.1 needed for 2160p60
+
+ // deprecated
'160p.mp4' => [
'maxSize' => '288x160',
'videoCodec' => 'h264',
- 'videoBitrate' => '160k',
+ 'videoBitrate' => '192k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '40k',
- 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'audioBitrate' => '112k',
+ 'type' => 'video/mp4; codecs="avc1.640014, mp4a.40.2"',
],
'240p.mp4' => [
'maxSize' => '426x240',
'videoCodec' => 'h264',
- 'videoBitrate' => '256k',
+ 'videoBitrate' => '394k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '40k',
- 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'audioBitrate' => '112k',
+ 'type' => 'video/mp4; codecs="avc1.42E015, mp4a.40.2"',
],
+ // deprecated
'320p.mp4' => [
'maxSize' => '480x320',
'videoCodec' => 'h264',
- 'videoBitrate' => '400k',
+ 'videoBitrate' => '512k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '40k',
+ 'audioBitrate' => '112k',
'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
],
'360p.mp4' => [
'maxSize' => '640x360',
'videoCodec' => 'h264',
- 'videoBitrate' => '512k',
+ 'videoBitrate' => '785k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '64k',
+ 'audioBitrate' => '112k',
'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
],
-
'480p.mp4' => [
'maxSize' => '854x480',
'videoCodec' => 'h264',
- 'videoBitrate' => '1200k',
+ 'videoBitrate' => '1280k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '64k',
+ 'audioBitrate' => '112k',
'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
],
'720p.mp4' => [
'maxSize' => '1280x720',
'videoCodec' => 'h264',
- 'videoBitrate' => '2500k',
+ 'videoBitrate' => '2551k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '128k',
- 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'audioBitrate' => '112k',
+ 'type' => 'video/mp4; codecs="avc1.42E028, mp4a.40.2"',
],
'1080p.mp4' => [
'maxSize' => '1920x1080',
'videoCodec' => 'h264',
- 'videoBitrate' => '5000k',
+ 'videoBitrate' => '5082k',
'audioCodec' => 'aac',
- 'channels' => '2',
'audioBitrate' => '128k',
- 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'type' => 'video/mp4; codecs="avc1.640029, mp4a.40.2"',
],
+ // Recommend against due to size
'1440p.mp4' => [
'maxSize' => '2560x1440',
'videoCodec' => 'h264',
- 'videoBitrate' => '16384k',
+ 'videoBitrate' => '8288k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '128k',
- 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'audioBitrate' => '112k',
+ 'type' => 'video/mp4; codecs="avc1.42E032, mp4a.40.2"',
],
+ // Recommend against due to size
'2160p.mp4' => [
'maxSize' => '4096x2160',
'videoCodec' => 'h264',
- 'videoBitrate' => '16384k',
+ 'videoBitrate' => '16513k',
'audioCodec' => 'aac',
- 'channels' => '2',
- 'audioBitrate' => '128k',
- 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'audioBitrate' => '112k',
+ 'type' => 'video/mp4; codecs="avc1.42E033, mp4a.40.2"',
],
// Audio profiles
'ogg' => [
'audioCodec' => 'vorbis',
'audioQuality' => '3',
'samplerate' => '44100',
- 'channels' => '2',
'noUpscaling' => 'true',
'novideo' => 'true',
'type' => 'audio/ogg; codecs="vorbis"',
],
'opus' => [
'audioCodec' => 'opus',
'audioQuality' => '1',
- 'samplerate' => '44100',
- 'channels' => '2',
+ 'samplerate' => '48000',
'noUpscaling' => 'true',
'novideo' => 'true',
'type' => 'audio/ogg; codecs="opus"',
],
'mp3' => [
'audioCodec' => 'mp3',
'audioQuality' => '1',
'samplerate' => '44100',
'channels' => '2',
'noUpscaling' => 'true',
'novideo' => 'true',
'type' => 'audio/mpeg',
],
'm4a' => [
'audioCodec' => 'aac',
'audioQuality' => '1',
'samplerate' => '44100',
- 'channels' => '2',
'noUpscaling' => 'true',
'novideo' => 'true',
'type' => 'audio/mp4; codecs="mp4a.40.5"',
],
];
/**
* @param File $file
* @param string $transcodeKey
* @return string
*/
public static function getDerivativeFilePath( $file, $transcodeKey ) {
return $file->getTranscodedPath( static::getTranscodeFileBaseName( $file, $transcodeKey ) );
}
/**
* Get the name to use as the base name for the transcode.
*
* Swift has problems where the url-encoded version of
* the path (ie '0/00/filename.ogv/filename.ogv.720p.webm' )
* is greater than > 1024 bytes, so shorten in that case.
*
* Future versions might respect FileRepo::$abbrvThreshold.
*
* @param File $file
* @param string $suffix Optional suffix (e.g. transcode key).
* @return string File name, or the string transcode.
*/
public static function getTranscodeFileBaseName( $file, $suffix = '' ) {
$name = $file->getName();
$length = strlen( urlencode( '0/00/' . $name . '/' . $name . '.' . $suffix ) );
if ( $length > 1024 ) {
return 'transcode' . '.' . $suffix;
}
return $name . '.' . $suffix;
}
/**
* Get url for a transcode.
*
* @param File $file
* @param string $suffix Transcode key
* @return string
*/
public static function getTranscodedUrlForFile( $file, $suffix = '' ) {
return $file->getTranscodedUrl( static::getTranscodeFileBaseName( $file, $suffix ) );
}
/**
* Get temp file at target path for video encode
*
* @param File $file
* @param string $transcodeKey
*
* @return TempFSFile|false at target encode path
*/
public static function getTargetEncodeFile( $file, $transcodeKey ) {
$filePath = static::getDerivativeFilePath( $file, $transcodeKey );
$ext = strtolower( pathinfo( $filePath, PATHINFO_EXTENSION ) );
// Create a temp FS file with the same extension
$tmpFileFactory = new TempFSFileFactory();
$tmpFile = $tmpFileFactory->newTempFSFile( 'transcode_' . $transcodeKey, $ext );
if ( !$tmpFile ) {
return false;
}
return $tmpFile;
}
/**
* Get the max size of the web stream ( constant bitrate )
* @return int
*/
public static function getMaxSizeWebStream() {
$maxSize = 0;
foreach ( static::enabledVideoTranscodes() as $transcodeKey ) {
if ( isset( static::$derivativeSettings[$transcodeKey]['videoBitrate'] ) ) {
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$currentSize = static::$derivativeSettings[$transcodeKey]['maxSize'];
if ( $currentSize > $maxSize ) {
$maxSize = $currentSize;
}
}
}
return $maxSize;
}
/**
* Give a rough estimate on file size
* Note this is not always accurate.. especially with variable bitrate codecs ;)
* @param File $file
* @param string $transcodeKey
* @suppress PhanTypePossiblyInvalidDimOffset
* @return int
*/
public static function getProjectedFileSize( $file, $transcodeKey ) {
$settings = static::$derivativeSettings[$transcodeKey];
// FIXME broken, as bitrate settings can contain units (64k)
if ( $settings[ 'videoBitrate' ] && $settings['audioBitrate'] ) {
return $file->getLength() * 8 * (
(int)$settings['videoBitrate']
+
(int)$settings['audioBitrate']
);
}
// Else just return the size of the source video
// ( we have no idea how large the actual derivative size will be )
/** @var ID3Handler $handler */
$handler = $file->getHandler();
'@phan-var ID3Handler $handler';
return $file->getLength() * $handler->getBitrate( $file ) * 8;
}
/**
* Static function to get the set of video assets
* Checks if the file is local or remote and grabs respective sources
* @param File &$file
* @param array $options
* @return array|mixed
*/
public static function getSources( &$file, $options = [] ) {
if ( $file->isLocal() || $file->repo instanceof ForeignDBViaLBRepo ) {
return static::getLocalSources( $file, $options );
}
if ( $file->getRepo() instanceof IForeignRepoWithMWApi ) {
return static::getRemoteSources( $file, $options );
}
return [];
}
/**
* Grabs sources from the remote repo via ApiQueryVideoInfo.php entry point.
*
* TODO: This method could use some rethinking. See comments on PS1 of
*
*
* Because this works with commons regardless of whether TimedMediaHandler is installed or not
* @param File $file The File must belong to a repo that is an instance of IForeignRepoWithMWApi
* @param array $options
* @return array|mixed
*/
public static function getRemoteSources( $file, $options = [] ) {
$regenerator = static function () use ( $file, $options ) {
// Setup source attribute options
$dataPrefix = in_array( 'nodata', $options, true ) ? '' : 'data-';
wfDebug( "Get Video sources from remote api for " . $file->getName() . "\n" );
$namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
$query = [
'action' => 'query',
'prop' => 'videoinfo',
'viprop' => 'derivatives',
'titles' => $namespaceInfo->getCanonicalName( NS_FILE ) . ':' . $file->getTitle()->getText()
];
/** @var IForeignRepoWithMWApi $repo */
$repo = $file->getRepo();
'@phan-var IForeignRepoWithMWApi $repo';
$data = $repo->fetchImageQuery( $query );
if ( isset( $data['warnings']['query'] ) &&
$data['warnings']['query']['*'] === "Unrecognized value for parameter 'prop': videoinfo"
) {
// The target wiki doesn't have TimedMediaHandler.
// Use the normal file repo system single source:
return [ static::getPrimarySourceAttributes( $file, [ $dataPrefix ] ) ];
}
$sources = [];
// Generate the source list from the data response:
if ( isset( $data['query']['pages'] ) ) {
$vidResult = array_shift( $data['query']['pages'] );
if ( isset( $vidResult['videoinfo'] ) ) {
$derResult = array_shift( $vidResult['videoinfo'] );
$derivatives = $derResult['derivatives'];
foreach ( $derivatives as $derivativeSource ) {
$sources[] = $derivativeSource;
}
}
}
return $sources;
};
$repoInfo = $file->getRepo()->getInfo();
$cacheTTL = $repoInfo['descriptionCacheExpiry'] ?? 0;
if ( $cacheTTL > 0 ) {
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$sources = $cache->getWithSetCallback(
$cache->makeKey( 'WebVideoSources-url', $file->getRepoName(), $file->getName() ),
$cacheTTL,
$regenerator
);
} else {
$sources = $regenerator();
}
return $sources;
}
/**
* Based on the $wgEnabledTranscodeSet set of enabled derivatives we
* return sources that are ready.
*
* This will not automatically update or queue anything!
*
* @param File &$file File object
* @param array $options Options, a set of options:
* 'nodata' Strips the data- attribute, useful when your output is not html
* @return array an associative array of sources suitable for tag output
*/
public static function getLocalSources( &$file, $options = [] ) {
global $wgEnableTranscode;
$sources = [];
// Add the original file:
$sources[] = static::getPrimarySourceAttributes( $file, $options );
// If $wgEnableTranscode is false don't look for or add other local sources:
if ( $wgEnableTranscode === false &&
!( $file->repo instanceof ForeignDBViaLBRepo ) ) {
return $sources;
}
// If an "oldFile" don't look for other sources:
if ( $file->isOld() ) {
return $sources;
}
/** @var ID3Handler $handler */
$handler = $file->getHandler();
'@phan-var ID3Handler $handler';
// Now Check for derivatives
if ( $handler->isAudio( $file ) ) {
$transcodeSet = static::enabledAudioTranscodes();
} else {
$transcodeSet = static::enabledVideoTranscodes();
}
foreach ( $transcodeSet as $transcodeKey ) {
if ( static::isTranscodeEnabled( $file, $transcodeKey ) ) {
// Try and add the source
static::addSourceIfReady( $file, $sources, $transcodeKey, $options );
}
}
return $sources;
}
/**
* Get the transcode state for a given filename and transcodeKey
*
* @param File $file
* @param string $transcodeKey
* @return bool
*/
public static function isTranscodeReady( $file, $transcodeKey ) {
// Check if we need to populate the transcodeState cache:
$transcodeState = static::getTranscodeState( $file );
// If no state is found the cache for this file is false:
if ( !isset( $transcodeState[ $transcodeKey ] ) ) {
return false;
}
// Else return boolean ready state ( if not null, then ready ):
return ( $transcodeState[ $transcodeKey ]['time_success'] ) !== null;
}
/**
* Clear the transcode state cache:
* @param string|null $fileName Optional fileName to clear transcode cache for
*/
public static function clearTranscodeCache( $fileName = null ) {
if ( $fileName ) {
unset( static::$transcodeState[ $fileName ] );
} else {
static::$transcodeState = [];
}
}
/**
* Populates the transcode table with the current DB state of transcodes
* if transcodes are not found in the database their state is set to "false"
*
* @param File $file File object
* @param IDatabase|false $db
* @return array[]
*/
public static function getTranscodeState( $file, $db = false ) {
global $wgTranscodeBackgroundTimeLimit;
$fileName = $file->getName();
if ( !isset( static::$transcodeState[$fileName] ) ) {
if ( $db === false ) {
$db = $file->repo->getReplicaDB();
}
// initialize the transcode state array
static::$transcodeState[ $fileName ] = [];
$res = $db->select( 'transcode',
'*',
[ 'transcode_image_name' => $fileName ],
__METHOD__,
[ 'LIMIT' => 100 ]
);
$overTimeout = [];
$over = time() - ( 2 * $wgTranscodeBackgroundTimeLimit );
// Populate the per transcode state cache
foreach ( $res as $row ) {
// strip the out the "transcode_" from keys
$transcodeState = [];
foreach ( $row as $k => $v ) {
$transcodeState[ str_replace( 'transcode_', '', $k ) ] = $v;
}
static::$transcodeState[ $fileName ][ $row->transcode_key ] = $transcodeState;
if ( $row->transcode_time_startwork !== null
&& wfTimestamp( TS_UNIX, $row->transcode_time_startwork ) < $over
&& $row->transcode_time_success === null
&& $row->transcode_time_error === null
) {
$overTimeout[] = $row->transcode_key;
}
}
if ( $overTimeout ) {
$dbw = wfGetDB( DB_PRIMARY );
$dbw->update(
'transcode',
[
'transcode_time_error' => $dbw->timestamp(),
'transcode_error' => 'timeout'
],
[
'transcode_image_name' => $fileName,
'transcode_key' => $overTimeout
],
__METHOD__
);
}
}
$sorted = static::$transcodeState[ $fileName ];
uksort( $sorted, 'strnatcmp' );
return $sorted;
}
/**
* Remove any transcode files and db states associated with a given $file
* Note that if you want to see them again, you must re-queue them by calling
* startJobQueue() or updateJobQueue().
*
* also remove the transcode files:
* @param File $file File Object
* @param string|false $transcodeKey Optional transcode key to remove only this key
*/
public static function removeTranscodes( $file, $transcodeKey = false ) {
// if transcode key is non-false, non-null:
if ( $transcodeKey ) {
// only remove the requested $transcodeKey
$removeKeys = [ $transcodeKey ];
} else {
// Remove any existing files ( regardless of their state )
$res = $file->repo->getPrimaryDB()->select( 'transcode',
[ 'transcode_key' ],
[ 'transcode_image_name' => $file->getName() ],
__METHOD__
);
$removeKeys = [];
foreach ( $res as $transcodeRow ) {
$removeKeys[] = $transcodeRow->transcode_key;
}
}
// Remove files by key:
$urlsToPurge = [];
foreach ( $removeKeys as $tKey ) {
$urlsToPurge[] = static::getTranscodedUrlForFile( $file, $tKey );
$filePath = static::getDerivativeFilePath( $file, $tKey );
if ( $file->repo->fileExists( $filePath ) ) {
$res = $file->repo->quickPurge( $filePath );
if ( !$res ) {
wfDebug( "Could not delete file $filePath\n" );
}
}
}
$update = new CdnCacheUpdate( $urlsToPurge );
DeferredUpdates::addUpdate( $update );
// Build the sql query:
$dbw = wfGetDB( DB_PRIMARY );
$deleteWhere = [ 'transcode_image_name' => $file->getName() ];
// Check if we are removing a specific transcode key
if ( $transcodeKey !== false ) {
$deleteWhere['transcode_key'] = $transcodeKey;
}
// Remove the db entries
$dbw->delete( 'transcode', $deleteWhere, __METHOD__ );
// Purge the cache for pages that include this video:
$titleObj = $file->getTitle();
static::invalidatePagesWithFile( $titleObj );
// Remove from local WebVideoTranscode cache:
static::clearTranscodeCache( $titleObj->getDBkey() );
}
/**
* @param Title $titleObj
*/
public static function invalidatePagesWithFile( $titleObj ) {
wfDebug( "WebVideoTranscode:: Invalidate pages that include: " . $titleObj->getDBkey() . "\n" );
// Purge the main image page:
$titleObj->invalidateCache();
// TODO if the video is used in over 500 pages add to 'job queue'
// TODO interwiki invalidation ?
$limit = 500;
$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select(
[ 'imagelinks', 'page' ],
[ 'page_namespace', 'page_title' ],
[ 'il_to' => $titleObj->getDBkey(), 'il_from = page_id' ],
__METHOD__,
[ 'LIMIT' => $limit + 1 ]
);
foreach ( $res as $page ) {
$title = Title::makeTitle( $page->page_namespace, $page->page_title );
$title->invalidateCache();
}
}
/**
* Add a source to the sources list if the transcode job is ready
*
* If the source is not found, it will not be used yet...
* Missing transcodes should be added by write tasks, not read tasks!
* @param File $file
* @param array &$sources
* @param string $transcodeKey
* @param array $dataPrefix
*/
public static function addSourceIfReady( $file, &$sources, $transcodeKey, $dataPrefix ) {
// Check if the transcode is ready:
if ( static::isTranscodeReady( $file, $transcodeKey ) ) {
$sources[] = static::getDerivativeSourceAttributes( $file, $transcodeKey, $dataPrefix );
}
}
/**
* Get the primary "source" asset used for other derivatives
* @param File $file
* @param array $options
* @return array
*/
public static function getPrimarySourceAttributes( $file, $options = [] ) {
global $wgLang;
$src = in_array( 'fullurl', $options, true ) ? wfExpandUrl( $file->getUrl() ) : $file->getUrl();
/** @var FLACHandler|MidiHandler|Mp3Handler|Mp4Handler|OggHandler|WAVHandler $handler */
$handler = $file->getHandler();
'@phan-var FLACHandler|MidiHandler|Mp3Handler|Mp4Handler|OggHandler|WAVHandler $handler';
$bitrate = $handler->getBitrate( $file );
$metadataType = $handler->getMetadataType( $file );
// Give grep a chance to find the usages: timedmedia-ogg, timedmedia-webm,
// timedmedia-mp4, timedmedia-flac, timedmedia-wav
if ( $handler->isAudio( $file ) ) {
$title = wfMessage( 'timedmedia-source-audio-file-desc',
wfMessage( 'timedmedia-' . $metadataType )->text() )
->params( $wgLang->formatBitrate( $bitrate ) )->text();
} else {
$title = wfMessage( 'timedmedia-source-file-desc',
wfMessage( 'timedmedia-' . $metadataType )->text() )
->numParams( $file->getWidth(), $file->getHeight() )
->params( $wgLang->formatBitrate( $bitrate ) )->text();
}
// Give grep a chance to find the usages: timedmedia-ogg, timedmedia-webm,
// timedmedia-mp4, timedmedia-flac, timedmedia-wav
$source = [
'src' => $src,
'type' => $handler->getWebType( $file ),
'title' => $title,
"shorttitle" => wfMessage(
'timedmedia-source-file',
wfMessage( 'timedmedia-' . $metadataType )->text()
)->text(),
"width" => (int)$file->getWidth(),
"height" => (int)$file->getHeight(),
];
if ( $bitrate ) {
$source["bandwidth"] = round( $bitrate );
}
// For video include framerate:
if ( !$handler->isAudio( $file ) ) {
$framerate = $handler->getFramerate( $file );
if ( $framerate ) {
$source[ "framerate" ] = (float)$framerate;
}
}
return $source;
}
/**
* Get derivative "source" attributes
* @param File $file
* @param string $transcodeKey
* @param array $options
* @return array
* @suppress PhanTypePossiblyInvalidDimOffset
*/
public static function getDerivativeSourceAttributes( $file, $transcodeKey, $options = [] ) {
$fileName = $file->getTitle()->getDBkey();
$src = static::getTranscodedUrlForFile( $file, $transcodeKey );
/** @var ID3Handler $handler */
$handler = $file->getHandler();
'@phan-var ID3Handler $handler';
if ( $handler->isAudio( $file ) ) {
$width = $height = 0;
} else {
[ $width, $height ] = static::getMaxSizeTransform(
$file,
static::$derivativeSettings[$transcodeKey]['maxSize']
);
}
$framerate = static::$derivativeSettings[$transcodeKey]['framerate']
?? $handler->getFramerate( $file );
// Setup the url src:
$src = in_array( 'fullurl', $options, true ) ? wfExpandUrl( $src ) : $src;
$fields = [
'src' => $src,
'title' => wfMessage( 'timedmedia-derivative-desc-' . $transcodeKey )->text(),
'type' => static::$derivativeSettings[ $transcodeKey ][ 'type' ],
"shorttitle" => wfMessage( 'timedmedia-derivative-' . $transcodeKey )->text(),
"transcodekey" => $transcodeKey,
// Add data attributes per emerging DASH / webTV adaptive streaming attributes
// eventually we will define a manifest xml entry point.
"width" => (int)$width,
"height" => (int)$height,
];
// a "ready" transcode should have a bitrate:
if ( isset( static::$transcodeState[$fileName] ) ) {
$fields["bandwidth"] = (int)static::$transcodeState[$fileName][$transcodeKey]['final_bitrate'];
}
if ( !$handler->isAudio( $file ) ) {
$fields += [ "framerate" => (float)$framerate ];
}
return $fields;
}
/**
* Queue up all enabled transcodes if missing.
* @param File $file File object
*/
public static function startJobQueue( File $file ) {
$keys = static::enabledTranscodes();
// 'Natural sort' puts the transcodes in ascending order by resolution,
// which roughly gives us fastest-to-slowest order.
natsort( $keys );
foreach ( $keys as $tKey ) {
// Note the job queue will de-duplicate and handle various errors, so we
// can just blast out the full list here.
static::updateJobQueue( $file, $tKey );
}
}
/**
* Make sure all relevant transcodes for the given file are tracked in the
* transcodes table; add entries for any missing ones.
*
* @param File $file File object
*/
public static function cleanupTranscodes( File $file ) {
$fileName = $file->getTitle()->getDBkey();
$db = $file->repo->getPrimaryDB();
$transcodeState = static::getTranscodeState( $file, $db );
$keys = static::enabledTranscodes();
foreach ( $keys as $transcodeKey ) {
if ( !static::isTranscodeEnabled( $file, $transcodeKey ) ) {
// This transcode is no longer enabled or erroneously included...
// Leave it in place, allowing it to be removed manually;
// it won't be used in playback and should be doing no harm.
continue;
}
if ( !isset( $transcodeState[ $transcodeKey ] ) ) {
$db->insert(
'transcode',
[
'transcode_image_name' => $fileName,
'transcode_key' => $transcodeKey,
'transcode_time_addjob' => null,
'transcode_error' => "",
'transcode_final_bitrate' => 0
],
__METHOD__,
[ 'IGNORE' ]
);
}
}
// Remove from local WebVideoTranscode cache:
static::clearTranscodeCache( $fileName );
}
/**
* Check if the given transcode key is appropriate for the file.
*
* @param File $file File object
* @param string $transcodeKey transcode key
* @return bool
* @suppress PhanTypePossiblyInvalidDimOffset
*/
public static function isTranscodeEnabled( File $file, $transcodeKey ) {
/** @var FLACHandler|MidiHandler|Mp3Handler|Mp4Handler|OggHandler|WAVHandler $handler */
$handler = $file->getHandler();
'@phan-var FLACHandler|MidiHandler|Mp3Handler|Mp4Handler|OggHandler|WAVHandler $handler';
$audio = $handler->isAudio( $file );
if ( $audio ) {
$keys = static::enabledAudioTranscodes();
} else {
$keys = static::enabledVideoTranscodes();
}
if ( in_array( $transcodeKey, $keys, true ) ) {
$settings = static::$derivativeSettings[$transcodeKey];
if ( $audio ) {
$sourceCodecs = $handler->getStreamTypes( $file );
$sourceCodec = $sourceCodecs ? strtolower( $sourceCodecs[0] ) : '';
return ( $sourceCodec !== $settings['audioCodec'] );
}
if ( static::isTargetLargerThanFile( $file, $settings['maxSize'] ) ) {
// Are we the smallest enabled transcode for this type?
// Then go ahead and make a wee little transcode for compat.
return static::isSmallestTranscodeForCodec( $transcodeKey );
}
return true;
}
// Transcode key is invalid or has been disabled.
return false;
}
/**
* Update the job queue if the file is not already in the job queue:
* @param File &$file File object
* @param string $transcodeKey transcode key
+ * @param bool $manualOverride permission to override soft limits on output size
*/
- public static function updateJobQueue( &$file, $transcodeKey ) {
+ public static function updateJobQueue( &$file, $transcodeKey, $manualOverride = false ) {
$fileName = $file->getTitle()->getDBkey();
$db = $file->repo->getPrimaryDB();
$transcodeState = static::getTranscodeState( $file, $db );
if ( !static::isTranscodeEnabled( $file, $transcodeKey ) ) {
return;
}
// If the job hasn't been added yet, attempt to do so
if ( !isset( $transcodeState[ $transcodeKey ] ) ) {
$db->insert(
'transcode',
[
'transcode_image_name' => $fileName,
'transcode_key' => $transcodeKey,
'transcode_time_addjob' => $db->timestamp(),
'transcode_error' => "",
'transcode_final_bitrate' => 0
],
__METHOD__,
[ 'IGNORE' ]
);
if ( !$db->affectedRows() ) {
// There is already a row for that job added by another request, no need to continue
return;
}
// Set the priority
$prioritized = static::isTranscodePrioritized( $file, $transcodeKey );
$job = new WebVideoTranscodeJob( $file->getTitle(), [
'transcodeMode' => 'derivative',
'transcodeKey' => $transcodeKey,
- 'prioritized' => $prioritized
+ 'prioritized' => $prioritized,
+ 'manualOverride' => $manualOverride,
] );
try {
MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup()->push( $job );
// Clear the state cache ( now that we have updated the page )
static::clearTranscodeCache( $fileName );
} catch ( Exception $ex ) {
// Adding job failed, update transcode row
$db->update(
'transcode',
[
'transcode_time_error' => $db->timestamp(),
'transcode_error' => "Failed to insert Job."
],
[
'transcode_image_name' => $fileName,
'transcode_key' => $transcodeKey,
],
__METHOD__
);
}
}
}
/**
* Check if this transcode belongs to the high-priority queue.
* @param File $file
* @param string $transcodeKey
* @return bool
*/
public static function isTranscodePrioritized( File $file, $transcodeKey ) {
global $wgTmhPriorityResolutionThreshold, $wgTmhPriorityLengthThreshold;
$transcodeHeight = 0;
$matches = [];
if ( preg_match( '/^(\d+)p/', $transcodeKey, $matches ) ) {
$transcodeHeight = (int)$matches[0];
}
return ( $transcodeHeight <= $wgTmhPriorityResolutionThreshold )
&& ( $file->getLength() <= $wgTmhPriorityLengthThreshold );
}
/**
* Return job queue length for the queue that will run this transcode.
* @param File $file
* @param string $transcodeKey
* @return int
*/
public static function getQueueSize( File $file, $transcodeKey ) {
// Warning: this won't treat the prioritized queue separately.
$db = $file->repo->getPrimaryDB();
$count = $db->selectField( 'transcode',
'COUNT(*)',
[
'transcode_time_addjob IS NOT NULL',
'transcode_time_startwork IS NULL',
'transcode_time_success IS NULL',
'transcode_time_error IS NULL',
],
__METHOD__
);
return (int)$count;
}
/**
* Transforms the size per a given "maxSize"
* if maxSize is > file, file size is used
* @param File $file
* @param string $targetMaxSize
* @return int[]
*/
public static function getMaxSizeTransform( $file, $targetMaxSize ) {
$maxSize = static::getMaxSize( $targetMaxSize );
$sourceWidth = (int)$file->getWidth();
$sourceHeight = (int)$file->getHeight();
if ( $sourceHeight === 0 ) {
// Audio file
return [ 0, 0 ];
}
$sourceAspect = $sourceWidth / $sourceHeight;
$targetWidth = $sourceWidth;
$targetHeight = $sourceHeight;
if ( $sourceAspect <= $maxSize['aspect'] ) {
if ( $sourceHeight > $maxSize['height'] ) {
$targetHeight = $maxSize['height'];
$targetWidth = (int)( $targetHeight * $sourceAspect );
}
} else {
if ( $sourceWidth > $maxSize['width'] ) {
$targetWidth = $maxSize['width'];
$targetHeight = (int)( $targetWidth / $sourceAspect );
// some players do not like uneven frame sizes
}
}
// some players do not like uneven frame sizes
$targetWidth += $targetWidth % 2;
$targetHeight += $targetHeight % 2;
return [ $targetWidth, $targetHeight ];
}
/**
* Test if a given transcode target is larger than the source file
*
* @param File &$file File object
* @param string $targetMaxSize
* @return bool
*/
public static function isTargetLargerThanFile( &$file, $targetMaxSize ) {
$maxSize = static::getMaxSize( $targetMaxSize );
$sourceWidth = $file->getWidth();
$sourceHeight = $file->getHeight();
$sourceAspect = (int)$sourceWidth / (int)$sourceHeight;
if ( $sourceAspect <= $maxSize['aspect'] ) {
return ( $maxSize['height'] > $sourceHeight );
}
return ( $maxSize['width'] > $sourceWidth );
}
/**
* Is the given transcode key the smallest configured transcode for
* its video codec?
* @param string $transcodeKey
* @return bool
* @suppress PhanTypePossiblyInvalidDimOffset
*/
public static function isSmallestTranscodeForCodec( $transcodeKey ) {
$settings = static::$derivativeSettings[$transcodeKey];
$vcodec = $settings['videoCodec'];
$maxSize = static::getMaxSize( $settings['maxSize'] );
foreach ( static::enabledVideoTranscodes() as $tKey ) {
$tsettings = static::$derivativeSettings[$tKey];
if ( $tsettings['videoCodec'] === $vcodec ) {
$tmaxSize = static::getMaxSize( $tsettings['maxSize'] );
if ( $tmaxSize['width'] < $maxSize['width'] ) {
return false;
}
if ( $tmaxSize['height'] < $maxSize['height'] ) {
return false;
}
}
}
return true;
}
/**
* Return maxSize array for given maxSize setting
*
* @param string $targetMaxSize
* @return array
*/
public static function getMaxSize( $targetMaxSize ) {
$maxSize = [];
$targetMaxSize = explode( 'x', $targetMaxSize, 2 );
$maxSize['width'] = (int)$targetMaxSize[0];
if ( count( $targetMaxSize ) === 1 ) {
$maxSize['height'] = (int)$targetMaxSize[0];
} else {
$maxSize['height'] = (int)$targetMaxSize[1];
}
// check for zero size ( audio )
if ( $maxSize['width'] === 0 || $maxSize['height'] === 0 ) {
$maxSize['aspect'] = 0;
} else {
$maxSize['aspect'] = $maxSize['width'] / $maxSize['height'];
}
return $maxSize;
}
/**
* @param array $set
*
* @return array
*/
private static function filterAndSort( array $set ) {
$keys = array_keys( array_filter( $set ) );
natsort( $keys );
return $keys;
}
public static function enabledTranscodes() {
global $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet;
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable These globals are arrays
return static::filterAndSort( array_merge( $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet ) );
}
public static function enabledVideoTranscodes() {
global $wgEnabledTranscodeSet;
return static::filterAndSort( $wgEnabledTranscodeSet );
}
public static function enabledAudioTranscodes() {
global $wgEnabledAudioTranscodeSet;
return static::filterAndSort( $wgEnabledAudioTranscodeSet );
}
public static function validateTranscodeConfiguration() {
foreach ( static::enabledTranscodes() as $transcodeKey ) {
if ( !isset( static::$derivativeSettings[ $transcodeKey ] ) ) {
throw new MWException(
__METHOD__ . ": Invalid key '$transcodeKey' specified in"
. " wgEnabledTranscodeSet or wgEnabledAudioTranscodeSet."
);
}
}
}
}
diff --git a/includes/WebVideoTranscode/WebVideoTranscodeJob.php b/includes/WebVideoTranscode/WebVideoTranscodeJob.php
index df24861a..86c1cc6f 100644
--- a/includes/WebVideoTranscode/WebVideoTranscodeJob.php
+++ b/includes/WebVideoTranscode/WebVideoTranscodeJob.php
@@ -1,1040 +1,1241 @@
removeDuplicates = true;
}
/**
* Wrapper around debug logger
* @param string $msg
*/
private function output( $msg ) {
LoggerFactory::getInstance( 'WebVideoTranscodeJob' )->debug( $msg );
}
/**
* @return File
*/
private function getFile() {
if ( !$this->file ) {
$this->file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
->newFile( $this->title );
}
return $this->file;
}
/**
* @return string
*/
private function getTargetEncodePath() {
if ( !$this->targetEncodeFile ) {
$file = $this->getFile();
$transcodeKey = $this->params[ 'transcodeKey' ];
$this->targetEncodeFile = WebVideoTranscode::getTargetEncodeFile( $file, $transcodeKey );
$this->targetEncodeFile->bind( $this );
}
return $this->targetEncodeFile->getPath();
}
/**
* purge temporary encode target
*/
private function purgeTargetEncodeFile() {
if ( $this->targetEncodeFile ) {
$this->targetEncodeFile->purge();
$this->targetEncodeFile = null;
}
}
/**
* @return string|false
*/
private function getSourceFilePath() {
if ( !$this->sourceFilePath ) {
$file = $this->getFile();
$this->source = $file->repo->getLocalReference( $file->getPath() );
if ( !$this->source ) {
$this->sourceFilePath = false;
} else {
$this->sourceFilePath = $this->source->getPath();
}
}
return $this->sourceFilePath;
}
/**
* Update the transcode table with failure time and error
* @param string $transcodeKey
* @param string $error
*
*/
private function setTranscodeError( $transcodeKey, $error ) {
$dbw = wfGetDB( DB_PRIMARY );
$dbw->update(
'transcode',
[
'transcode_time_error' => $dbw->timestamp(),
'transcode_error' => $error
],
[
'transcode_image_name' => $this->getFile()->getName(),
'transcode_key' => $transcodeKey
],
__METHOD__
);
$this->setLastError( $error );
}
/**
* Run the transcode request
* @return bool success
*/
public function run() {
// get a local pointer to the file
$file = $this->getFile();
// Validate the file exists:
if ( !$file ) {
$this->output( $this->title . ': File not found ' );
return false;
}
// Validate the transcode key param:
$transcodeKey = $this->params['transcodeKey'];
// Build the destination target
if ( !isset( WebVideoTranscode::$derivativeSettings[ $transcodeKey ] ) ) {
$error = "Transcode key $transcodeKey not found, skipping";
$this->output( $error );
$this->setLastError( $error );
return false;
}
// Validate the source exists:
if ( !$this->getSourceFilePath() || !is_file( $this->getSourceFilePath() ) ) {
$status = $this->title . ': Source not found ' . $this->getSourceFilePath();
$this->output( $status );
$this->setTranscodeError( $transcodeKey, $status );
return false;
}
$options = WebVideoTranscode::$derivativeSettings[ $transcodeKey ];
if ( isset( $options[ 'novideo' ] ) ) {
- // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$this->output( "Encoding to audio codec: " . $options['audioCodec'] );
} else {
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$this->output( "Encoding to codec: " . $options['videoCodec'] );
}
$dbw = wfGetDB( DB_PRIMARY );
// Check if we have "already started" the transcode ( possible error )
$dbStartTime = $dbw->selectField( 'transcode', 'transcode_time_startwork',
[
'transcode_image_name' => $this->getFile()->getName(),
'transcode_key' => $transcodeKey
],
__METHOD__
);
if ( $dbStartTime !== null ) {
$error = 'Error, running transcode job, for job that has already started';
$this->output( $error );
return true;
}
// Update the transcode table letting it know we have "started work":
$jobStartTimeCache = wfTimestamp( TS_UNIX );
$dbw->update(
'transcode',
[ 'transcode_time_startwork' => $dbw->timestamp( $jobStartTimeCache ) ],
[
'transcode_image_name' => $this->getFile()->getName(),
'transcode_key' => $transcodeKey
],
__METHOD__
);
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
// Avoid contention and "server has gone away" errors as
// the transcode will take a very long time in some cases
$lbFactory->commitAll( __METHOD__ );
// We can't just leave the connection open either or it will
// eat up resources and block new connections, so make sure
// everything is dead and gone.
$lbFactory->closeAll();
// Check the codec see which encode method to call;
$videoCodec = $options['videoCodec'] ?? '';
+ $codecs = [ 'vp8', 'vp9', 'h264' ];
if ( isset( $options[ 'novideo' ] ) ) {
if ( $file->getMimeType() === 'audio/midi' ) {
$status = $this->midiToAudioEncode( $options );
} else {
$status = $this->ffmpegEncode( $options );
}
- } elseif (
- $videoCodec === 'vp8' || $videoCodec === 'vp9' ||
- $videoCodec === 'h264'
- ) {
+ } elseif ( in_array( $videoCodec, $codecs ) ) {
// Check for twopass:
if ( isset( $options['twopass'] ) ) {
// ffmpeg requires manual two pass
$status = $this->ffmpegEncode( $options, 1 );
if ( $status && !is_string( $status ) ) {
$status = $this->ffmpegEncode( $options, 2 );
}
} else {
$status = $this->ffmpegEncode( $options );
}
} else {
wfDebug( 'Error unknown codec:' . $videoCodec );
$status = 'Error unknown target encode codec:' . $videoCodec;
}
// Remove any log files,
// all useful info should be in status and or we are done with 2 pass encoding
$this->removeFfmpegLogFiles();
// Reconnect to the database...
$dbw = wfGetDB( DB_PRIMARY );
// Do a quick check to confirm the job was not restarted or removed while we were transcoding
// Confirm that the in memory $jobStartTimeCache matches db start time
$dbStartTime = $dbw->selectField( 'transcode', 'transcode_time_startwork',
[
'transcode_image_name' => $this->getFile()->getName(),
'transcode_key' => $transcodeKey
],
__METHOD__
);
// Check for ( hopefully rare ) issue of or job restarted while transcode in progress
if ( $dbStartTime === null || $jobStartTimeCache !== wfTimestamp( TS_UNIX, $dbStartTime ) ) {
$this->output(
'Possible Error,
transcode task restarted, removed, or completed while transcode was in progress'
);
// if an error; just error out,
// we can't remove temp files or update states, because the new job may be doing stuff.
if ( $status !== true ) {
$this->setTranscodeError( $transcodeKey, $status );
return false;
}
// else just continue with db updates,
// and when the new job comes around it won't start because it will see
// that the job has already been started.
}
// If status is oky and target does not exist, reset status
if ( $status === true && !is_file( $this->getTargetEncodePath() ) ) {
$status = 'Target does not exist: ' . $this->getTargetEncodePath();
}
// If status is ok and target is larger than 0 bytes
if ( $status === true && filesize( $this->getTargetEncodePath() ) > 0 ) {
$file = $this->getFile();
$storeOptions = null;
if (
strpos( $options['type'], '/ogg' ) !== false &&
$file->getLength()
) {
$storeOptions = [];
// Ogg files need a duration header for firefox
$storeOptions['headers']['X-Content-Duration'] = (float)$file->getLength();
}
// Avoid "server has gone away" errors as copying can be slow
$lbFactory->commitAll( __METHOD__ );
$lbFactory->closeAll();
// Copy derivative from the FS into storage at $finalDerivativeFilePath
$result = $file->getRepo()->quickImport(
// temp file
$this->getTargetEncodePath(),
// storage
WebVideoTranscode::getDerivativeFilePath( $file, $transcodeKey ),
$storeOptions
);
if ( !$result->isOK() ) {
// no need to invalidate all pages with video.
// Because all pages remain valid ( no $transcodeKey derivative )
// just clear the file page ( so that the transcode table shows the error )
$this->title->invalidateCache();
$this->setTranscodeError( $transcodeKey, $result->getWikiText() );
$status = false;
} else {
$bitrate = round(
(int)( filesize( $this->getTargetEncodePath() ) / $file->getLength() ) * 8
);
// Wikimedia\restoreWarnings();
// Reconnect to the database...
$dbw = wfGetDB( DB_PRIMARY );
// Update the transcode table with success time:
$dbw->update(
'transcode',
[
'transcode_error' => '',
'transcode_time_error' => null,
'transcode_time_success' => $dbw->timestamp(),
'transcode_final_bitrate' => $bitrate
],
[
'transcode_image_name' => $this->getFile()->getName(),
'transcode_key' => $transcodeKey,
],
__METHOD__
);
// Commit to reduce contention
$dbw->commit( __METHOD__, 'flush' );
WebVideoTranscode::invalidatePagesWithFile( $this->title );
}
} else {
// Update the transcode table with failure time and error
$this->setTranscodeError( $transcodeKey, $status );
// no need to invalidate all pages with video.
// Because all pages remain valid ( no $transcodeKey derivative )
// just clear the file page ( so that the transcode table shows the error )
$this->title->invalidateCache();
}
// done with encoding target, clean up
$this->purgeTargetEncodeFile();
// Clear the webVideoTranscode cache ( so we don't keep out dated table cache around )
WebVideoTranscode::clearTranscodeCache( $this->title->getDBkey() );
$url = WebVideoTranscode::getTranscodedUrlForFile( $file, $transcodeKey );
$update = new CdnCacheUpdate( [ $url ] );
$update->doUpdate();
if ( $status !== true ) {
$this->setLastError( $status );
}
return $status === true;
}
private function removeFfmpegLogFiles() {
$path = $this->getTargetEncodePath();
$dir = dirname( $path );
if ( is_dir( $dir ) ) {
$dh = opendir( $dir );
if ( $dh ) {
$file = readdir( $dh );
while ( $file !== false ) {
$log_path = "$dir/$file";
$ext = strtolower( pathinfo( $log_path, PATHINFO_EXTENSION ) );
if ( $ext === 'log' && strpos( $log_path, $path ) === 0 ) {
AtEase::suppressWarnings();
unlink( $log_path );
AtEase::restoreWarnings();
}
$file = readdir( $dh );
}
closedir( $dh );
}
}
}
/**
* Utility helper for ffmpeg mapping
* @param array $options
* @param int $pass
* @return true|string
*/
private function ffmpegEncode( $options, $pass = 0 ) {
global $wgFFmpegLocation, $wgTranscodeBackgroundMemoryLimit;
+ global $wgTranscodeBackgroundSizeLimit, $wgTranscodeSoftSizeLimit;
if ( !is_file( $this->getSourceFilePath() ) ) {
return "source file is missing, " . $this->getSourceFilePath() . ". Encoding failed.";
}
// Set up the base command
$cmd = wfEscapeShellArg(
$wgFFmpegLocation
) . ' -nostdin -y -i ' . wfEscapeShellArg( $this->getSourceFilePath() );
if ( isset( $options['vpre'] ) ) {
$cmd .= ' -vpre ' . wfEscapeShellArg( $options['vpre'] );
}
+ if ( isset( $options['framerate'] ) ) {
+ $cmd .= " -r " . wfEscapeShellArg( $options['framerate'] );
+ } elseif ( isset( $options['fpsmax'] ) ) {
+ $cmd .= " -fpsmax " . wfEscapeShellArg( $options['fpsmax'] );
+ } else {
+ $cmd .= " -fpsmax " . self::MAX_FPS;
+ }
+ $fps = $this->effectiveFrameRate( $options );
+
if ( isset( $options['novideo'] ) ) {
$cmd .= " -vn ";
} elseif ( $options['videoCodec'] === 'vp8' || $options['videoCodec'] === 'vp9' ) {
$cmd .= $this->ffmpegAddWebmVideoOptions( $options, $pass );
} elseif ( $options['videoCodec'] === 'h264' ) {
$cmd .= $this->ffmpegAddH264VideoOptions( $options, $pass );
+ } elseif ( $options['videoCodec'] ) {
+ $cmd .= ' -vcodec ' . wfEscapeShellArg( $options['videoCodec'] );
+ }
+
+ // Check for keyframeInterval
+ $keyframeInterval = $options['keyframeInterval'] ?? '240';
+ $cmd .= ' -g ' . wfEscapeShellArg( $keyframeInterval );
+ if ( isset( $options['keyframeIntervalMin'] ) ) {
+ $cmd .= ' -keyint_min ' . wfEscapeShellArg( $options['keyframeIntervalMin'] );
+ }
+
+ if ( isset( $options['videoBitrate'] ) ) {
+ $base = $this->expandRate( $options['videoBitrate'] );
+ $bitrate = $this->scaleRate( $options, $base );
+ $cmd .= " -b:v $bitrate";
+
+ // Estimate the output file size in KiB and bail out early
+ // if it's potentially very large. Could be a denial of
+ // service, or just a large file that probably is poorly
+ // compressed.
+ $duration = (float)$this->file->getLength();
+ $estimatedSize = round( ( $bitrate / 8 ) * $duration / 1024 );
+ if ( $wgTranscodeBackgroundSizeLimit > 0 && $estimatedSize > $wgTranscodeBackgroundSizeLimit ) {
+ // This hard limit cannot be overridden by admins, except by raising the limit in config.
+ // @todo return an error code that can be localized later
+ return "estimated file size $estimatedSize KiB over hard limit $wgTranscodeBackgroundSizeLimit KiB";
+ }
+
+ if ( $wgTranscodeSoftSizeLimit > 0 && $estimatedSize > $wgTranscodeSoftSizeLimit ) {
+ // This soft limit can be overridden when a transcode is reset by hand via the web UI
+ // or API, or requeueTranscodes.php with --manual-override option.
+ $manualOverride = $this->params['manualOverride'] ?? false;
+ if ( !$manualOverride ) {
+ // @todo return an error code that can be localized later
+ return "estimated file size $estimatedSize KiB over soft limit $wgTranscodeSoftSizeLimit KiB";
+ }
+ }
+
+ if ( isset( $options['minrate'] ) ) {
+ $minrate = $this->scaleRate( $options, $options['minrate'] );
+ $cmd .= " -minrate $minrate";
+ }
+ if ( isset( $options['maxrate'] ) ) {
+ $maxrate = $this->scaleRate( $options, $options['maxrate'] );
+ $cmd .= " -maxrate $maxrate";
+ }
+ if ( isset( $options['bufsize'] ) ) {
+ $bufsize = $this->scaleRate( $options, $options['bufsize'] );
+ $cmd .= " -bufsize $bufsize";
+ }
}
+
// If necessary, add deinterlacing options
$cmd .= $this->ffmpegAddDeinterlaceOptions( $options );
// Add size options:
$cmd .= $this->ffmpegAddVideoSizeOptions( $options );
if ( !MediaWikiServices::getInstance()->getMainConfig()->get( 'UseFFmpeg2' ) ) {
// Work around https://trac.ffmpeg.org/ticket/6375 in ffmpeg 3.4/4.0
// Sometimes caused transcode failures saying things like:
// "1 frames left in the queue on closing"
$cmd .= ' -max_muxing_queue_size 1024';
}
// Check for start time
if ( isset( $options['starttime'] ) ) {
$cmd .= ' -ss ' . wfEscapeShellArg( $options['starttime'] );
} else {
$options['starttime'] = 0;
}
// Check for end time:
if ( isset( $options['endtime'] ) ) {
$duration = (int)$options['endtime'] - (int)$options['starttime'];
$cmd .= ' -t ' . $duration;
}
if ( $pass === 1 || isset( $options['noaudio'] ) ) {
$cmd .= ' -an';
} else {
$cmd .= $this->ffmpegAddAudioOptions( $options, $pass );
}
if ( $pass !== 0 ) {
$cmd .= " -pass " . wfEscapeShellArg( (string)$pass );
$cmd .= " -passlogfile " . wfEscapeShellArg( $this->getTargetEncodePath() . '.log' );
}
// And the output target:
if ( $pass === 1 ) {
$cmd .= ' /dev/null';
} else {
$cmd .= " " . wfEscapeShellArg( $this->getTargetEncodePath() );
}
$this->output( "Running cmd: \n\n" . $cmd . "\n" );
// Right before we output remove the old file
$shellOutput = $this->runShellExec( $cmd, $retval );
if ( $retval !== 0 ) {
return $cmd .
"\n\nExitcode: $retval\nMemory: $wgTranscodeBackgroundMemoryLimit\n\n" .
$shellOutput;
}
return true;
}
+ // Bitrates and keyframe distances are specified for this
+ // common frame rate (30), and scaled accordingly to accomodate
+ // higher frame rates.
+ private const DEFAULT_FPS = 30;
+ private const MAX_FPS = 60;
+ private const MIN_FPS = 24;
+
+ /**
+ * Scale a bitrate or frame count according to the frame rate
+ * of the file versus the default frame rate. This is not a
+ * straight linear multiplication; it's biased to reduce impact
+ * beyond 30 fps, to 1.5x base at 60 fps.
+ *
+ * @param array $options
+ * @param string|int $rate
+ * @return int
+ */
+ private function scaleRate( $options, $rate ) {
+ $fps = $this->effectiveFrameRate( $options );
+ $base = $this->expandRate( $rate );
+
+ $lofps = min( $fps, self::DEFAULT_FPS );
+ $hifps = $fps - $lofps;
+ $scaled = $base * $lofps / self::DEFAULT_FPS +
+ 0.5 * $base * $hifps / self::DEFAULT_FPS;
+ return (int)$scaled;
+ }
+
+ /**
+ * Expand a bitrate that may have a k/m/g suffix
+ *
+ * @param string|int $rate
+ * @return int
+ */
+ private function expandRate( $rate ) {
+ if ( is_int( $rate ) ) {
+ return $rate;
+ }
+ $matches = [];
+ if ( preg_match( '/^(\d+)([kmg])$/', strtolower( $rate ), $matches ) ) {
+ $n = (int)$matches[1];
+ switch ( $matches[2] ) {
+ case 'g':
+ $n *= 1000;
+ // fall through
+ case 'm':
+ $n *= 1000;
+ // fall through
+ case 'k':
+ $n *= 1000;
+ // fall through
+ }
+ return $n;
+ } else {
+ return (int)$rate;
+ }
+ }
+
+ /**
+ * Grab the frame rate from the file, bounded by
+ * format-specific or generic limitations.
+ * Suitable for scaling linear parameters like the
+ * target bit rate.
+ *
+ * @param array $options
+ * @return float
+ */
+ private function effectiveFrameRate( $options ) {
+ if ( isset( $options['framerate'] ) ) {
+ // fixed framerate
+ $fps = $this->fractionToFloat( $options['framerate'] );
+ } else {
+ // @todo getid3 gets this wrong on some WebM input files
+ // consider reading from ffmpeg or ffprobe...
+ // We cap it, but this can cause a 29.97fps file to use
+ // the 60fps bitrate. Worst case it's a bloated file.
+ $fps = $this->frameRate();
+ }
+ if ( $this->shouldFrameDouble( $options ) ) {
+ $fps *= 2;
+ }
+
+ if ( $fps < self::MIN_FPS ) {
+ return self::MIN_FPS;
+ }
+ if ( isset( $options['fpsmax'] ) ) {
+ $max = $this->fractionToFloat( $options['fpsmax'] );
+ } else {
+ $max = self::MAX_FPS;
+ }
+ if ( $fps > $max ) {
+ return $max;
+ }
+ return $fps;
+ }
+
+ /**
+ * @param string $str
+ * @return float
+ */
+ private function fractionToFloat( $str ) {
+ $fraction = explode( '/', $str, 2 );
+ if ( count( $fraction ) > 1 ) {
+ return (float)$fraction[0] / (float)$fraction[1];
+ }
+ return (float)$str;
+ }
+
+ /**
+ * Return the actual frame rate of the file, or the default
+ * if can't retrieve it.
+ *
+ * @return float
+ */
+ private function frameRate() {
+ $file = $this->getFile();
+ $handler = $file->getHandler();
+ if ( $handler instanceof TimedMediaHandler ) {
+ $fps = $handler->getFrameRate( $file );
+ if ( $fps ) {
+ return $fps;
+ }
+ }
+ return self::DEFAULT_FPS;
+ }
+
/**
* Adds ffmpeg shell options for h264
*
* @param array $options
* @param int $pass
* @return string
*/
public function ffmpegAddH264VideoOptions( $options, $pass ) {
global $wgFFmpegThreads;
// Set the codec:
$cmd = " -threads " . (int)$wgFFmpegThreads . " -vcodec libx264";
// Check for presets:
if ( isset( $options['preset'] ) ) {
// Add the two vpre types:
switch ( $options['preset'] ) {
case 'ipod320':
// phpcs:ignore Generic.Files.LineLength.TooLong
$cmd .= " -profile:v baseline -preset slow -coder 0 -bf 0 -weightb 1 -level 13 -maxrate 768k -bufsize 3M";
break;
case '720p':
case 'ipod640':
// phpcs:ignore Generic.Files.LineLength.TooLong
$cmd .= " -profile:v baseline -preset slow -coder 0 -bf 0 -refs 1 -weightb 1 -level 31 -maxrate 10M -bufsize 10M";
break;
default:
// in the default case just pass along the preset to ffmpeg
$cmd .= " -vpre " . wfEscapeShellArg( $options['preset'] );
break;
}
}
- if ( isset( $options['videoBitrate'] ) ) {
- $cmd .= " -b " . wfEscapeShellArg( $options['videoBitrate'] );
- }
+
+ $cmd .= ' -pix_fmt yuv420p';
+ $cmd .= ' -rc-lookahead 16';
+ $cmd .= ' -movflags +faststart';
+
// Output mp4
$cmd .= " -f mp4";
return $cmd;
}
/**
* @param array $options
*
* @return string
*/
private function ffmpegAddVideoSizeOptions( $options ) {
$cmd = '';
// Get a local pointer to the file object
$file = $this->getFile();
- // Check for aspect ratio ( we don't do anything with this right now)
+ // Check for aspect ratio
$aspectRatio = $options['aspect'] ?? $file->getWidth() . ':' . $file->getHeight();
- if ( isset( $options['maxSize'] ) ) {
- // Get size transform ( if maxSize is > file, file size is used:
-
- [ $width, $height ] = WebVideoTranscode::getMaxSizeTransform( $file, $options['maxSize'] );
- $cmd .= ' -s ' . (int)$width . 'x' . (int)$height;
- } elseif (
- ( isset( $options['width'] ) && $options['width'] > 0 )
+ if ( ( isset( $options['width'] ) && $options['width'] > 0 )
&&
( isset( $options['height'] ) && $options['height'] > 0 )
) {
$cmd .= ' -s ' . (int)$options['width'] . 'x' . (int)$options['height'];
+ $cmd .= ' -aspect ' . wfEscapeShellArg( $aspectRatio );
+ } elseif ( isset( $options['maxSize'] ) ) {
+ // Get size transform ( if maxSize is > file, file size is used:
+
+ [ $width, $height ] = WebVideoTranscode::getMaxSizeTransform( $file, $options['maxSize'] );
+ $cmd .= ' -s ' . (int)$width . 'x' . (int)$height;
}
// Handle crop:
$optionMap = [
'cropTop' => '-croptop',
'cropBottom' => '-cropbottom',
'cropLeft' => '-cropleft',
'cropRight' => '-cropright'
];
foreach ( $optionMap as $name => $cmdArg ) {
if ( isset( $options[$name] ) ) {
$cmd .= " $cmdArg " . wfEscapeShellArg( $options[$name] );
}
}
return $cmd;
}
/**
* Adds ffmpeg shell options for webm
*
* @param array $options
* @param int $pass
* @return string
*/
private function ffmpegAddWebmVideoOptions( $options, $pass ) {
global $wgFFmpegThreads, $wgFFmpegVP9RowMT;
// Get a local pointer to the file object
$file = $this->getFile();
$cmd = ' -threads ' . (int)$wgFFmpegThreads;
if ( $wgFFmpegVP9RowMT && $options['videoCodec'] === 'vp9' ) {
// Macroblock row multithreading allows using more CPU cores
// for VP9 encoding. This is not yet the default, and the option
// will fail on a version of ffmpeg that is too old or is built
// against a libvpx that is too old, so we have to enable it
// conditionally for now.
//
// Requires libvpx 1.7 and ffmpeg 3.3.
$cmd .= ' -row-mt 1';
}
- // check for presets:
- if ( isset( $options['preset'] ) ) {
- if ( $options['preset'] === "360p" ) {
- $cmd .= " -vpre libvpx-360p";
- } elseif ( $options['preset'] === "720p" ) {
- $cmd .= " -vpre libvpx-720p";
- } elseif ( $options['preset'] === "1080p" ) {
- $cmd .= " -vpre libvpx-1080p";
- }
- }
-
// Force to 4:2:0 chroma subsampling. Others are supported in Theora
// and in VP9 profile 1, but Chrome and Edge don't grok them.
$cmd .= ' -pix_fmt yuv420p';
// Check for video quality:
if ( isset( $options['videoQuality'] ) && $options['videoQuality'] >= 0 ) {
// Map 0-10 to 63-0, higher values worse quality
$quality = 63 - (int)( (int)$options['videoQuality'] / 10 * 63 );
- $cmd .= " -qmin " . wfEscapeShellArg( (string)$quality );
- $cmd .= " -qmax " . wfEscapeShellArg( (string)$quality );
+ $options['qmax'] = (string)$quality;
+ $options['qmin'] = (string)$quality;
+ }
+ if ( isset( $options['qmin'] ) ) {
+ $cmd .= " -qmin " . wfEscapeShellArg( $options['qmin'] );
+ }
+ if ( isset( $options['qmax'] ) ) {
+ $cmd .= " -qmax " . wfEscapeShellArg( $options['qmax'] );
}
// libvpx-specific constant quality or constrained quality
// note the range is different between VP8 and VP9
if ( isset( $options['crf'] ) ) {
$cmd .= " -crf " . wfEscapeShellArg( $options['crf'] );
}
- // Check for video bitrate:
- if ( isset( $options['videoBitrate'] ) ) {
- $qmin = $options['qmin'] ?? 1;
- $qmax = $options['qmax'] ?? 51;
- $cmd .= " -qmin " . wfEscapeShellArg( (string)$qmin );
- $cmd .= " -qmax " . wfEscapeShellArg( (string)$qmax );
-
- $cmd .= " -vb " . wfEscapeShellArg( (string)( $options['videoBitrate'] * 1000 ) );
- if ( isset( $options['minrate'] ) ) {
- $cmd .= " -minrate " . wfEscapeShellArg( (string)( $options['minrate'] * 1000 ) );
- }
- if ( isset( $options['maxrate'] ) ) {
- $cmd .= " -maxrate " . wfEscapeShellArg( (string)( $options['maxrate'] * 1000 ) );
- }
- }
// Set the codec:
if ( $options['videoCodec'] === 'vp9' ) {
$cmd .= " -vcodec libvpx-vp9";
if ( isset( $options['tileColumns'] ) ) {
$cmd .= ' -tile-columns ' . wfEscapeShellArg( $options['tileColumns'] );
}
+ if ( isset( $options['tileRows'] ) ) {
+ $cmd .= ' -tile-rows ' . wfEscapeShellArg( $options['tileRows'] );
+ }
} else {
$cmd .= " -vcodec libvpx";
if ( isset( $options['slices'] ) ) {
$cmd .= ' -slices ' . wfEscapeShellArg( $options['slices'] );
}
}
if ( isset( $options['altref'] ) ) {
- $cmd .= ' -auto-alt-ref 1';
- $cmd .= ' -lag-in-frames 25';
- }
-
- // Check for keyframeInterval
- if ( isset( $options['keyframeInterval'] ) ) {
- $cmd .= ' -g ' . wfEscapeShellArg( $options['keyframeInterval'] );
+ if ( $options['altref'] === '0' ) {
+ $cmd .= ' -auto-alt-ref 0';
+ } else {
+ $cmd .= ' -auto-alt-ref 1';
+ }
}
- if ( isset( $options['keyframeIntervalMin'] ) ) {
- $cmd .= ' -keyint_min ' . wfEscapeShellArg( $options['keyframeIntervalMin'] );
+ if ( isset( $options['lagInFrames'] ) ) {
+ $cmd .= ' -lag-in-frames ' . wfEscapeShellArg( $options['lagInFrames'] );
}
- if ( isset( $options['deinterlace'] ) ) {
- $cmd .= ' -deinterlace';
+
+ if ( isset( $options['quality'] ) ) {
+ $cmd .= ' -quality ' . wfEscapeShellArg( $options['quality'] );
+ } else {
+ $cmd .= ' -quality good';
}
+
if ( $pass === 1 ) {
// Make first pass faster...
$cmd .= ' -speed 4';
} elseif ( isset( $options['speed'] ) ) {
$cmd .= ' -speed ' . wfEscapeShellArg( $options['speed'] );
}
// Output WebM
$cmd .= " -f webm";
return $cmd;
}
+ /**
+ * @return bool
+ */
+ private function isInterlaced() {
+ $handler = $this->file->getHandler();
+ return ( $handler instanceof TimedMediaHandler && $handler->isInterlaced( $this->file ) );
+ }
+
+ /**
+ * Whether to produce one frame per field when deinterlacing.
+ * This will double the output frame rate.
+ *
+ * @param array $options
+ * @return bool
+ */
+ private function shouldFrameDouble( $options ) {
+ if ( $this->isInterlaced() ) {
+ if ( isset( $options['framerate'] ) ) {
+ // Fixed framerate, don't mess with it.
+ return false;
+ }
+ if ( isset( $options['fpsmax'] ) && $this->fractionToFloat( $options['fpsmax'] ) < 60 ) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
/**
* @param array $options
* @return string
*/
private function ffmpegAddDeinterlaceOptions( $options ) {
- $cmd = '';
-
- $handler = $this->file->getHandler();
- if ( $handler instanceof TimedMediaHandler && $handler->isInterlaced( $this->file ) ) {
- $cmd .= ' -vf yadif=0';
+ if ( $this->isInterlaced() ) {
+ if ( $this->shouldFrameDouble( $options ) ) {
+ // Send one frame per field for full motion smoothness.
+ return ' -vf yadif=1';
+ }
+ // Send one frame per field
+ return ' -vf yadif=0';
}
-
- return $cmd;
+ return '';
}
/**
* @param array $options
* @param int $pass
* @return string
*/
private function ffmpegAddAudioOptions( $options, $pass ) {
$cmd = '';
if ( isset( $options['audioQuality'] ) ) {
$cmd .= " -aq " . wfEscapeShellArg( $options['audioQuality'] );
}
if ( isset( $options['audioBitrate'] ) ) {
- $cmd .= ' -ab ' . (int)$options['audioBitrate'] * 1000;
+ $cmd .= " -ab " . $this->expandRate( $options['audioBitrate'] );
}
if ( isset( $options['samplerate'] ) ) {
$cmd .= " -ar " . wfEscapeShellArg( $options['samplerate'] );
}
if ( isset( $options['channels'] ) ) {
$cmd .= " -ac " . wfEscapeShellArg( $options['channels'] );
}
if ( isset( $options['audioCodec'] ) ) {
$encoders = [
'vorbis' => 'libvorbis',
'opus' => 'libopus',
'mp3' => 'libmp3lame',
];
$codec = $encoders[$options['audioCodec']] ?? $options['audioCodec'];
$cmd .= " -acodec " . wfEscapeShellArg( $codec );
if ( $codec === 'aac' ) {
// the aac encoder is currently "experimental" in libav 9? :P
$cmd .= ' -strict experimental';
}
} else {
// if no audio codec set use vorbis :
$cmd .= " -acodec libvorbis ";
}
return $cmd;
}
/**
* Utility helper for midi to an audio format conversion
* @param array $options
* @return true|string
*/
private function midiToAudioEncode( $options ) {
global $wgTmhFluidsynthLocation, $wgFFmpegLocation, $wgTmhSoundfontLocation,
$wgTranscodeBackgroundMemoryLimit;
if ( !is_file( $this->getSourceFilePath() ) ) {
return "source file is missing, " . $this->getSourceFilePath() . ". Encoding failed.";
}
$outputFileExt = $options['audioCodec'] === 'vorbis' ? '' : '.wav';
// Set up the base command
$cmdArgs = [
wfEscapeShellArg( $wgTmhFluidsynthLocation ),
'-T',
// wav for mp3
$options['audioCodec'] === 'vorbis' ? 'oga' : 'wav',
wfEscapeShellArg( $wgTmhSoundfontLocation ),
wfEscapeShellArg( $this->getSourceFilePath() ),
'-F',
wfEscapeShellArg( $this->getTargetEncodePath() . $outputFileExt )
];
$cmdString = implode( " ", $cmdArgs );
$shellOutput = $this->runShellExec( $cmdString, $retval );
'@phan-var int $retval';
// Fluidsynth doesn't give error codes - $retval always stays 0
if ( strpos( $shellOutput, "fluidsynth: error:" ) !== false ) {
return $cmdString .
"\n\nExitcode: " . $retval . "\nMemory: $wgTranscodeBackgroundMemoryLimit\n\n" .
$shellOutput;
}
if ( $options['audioCodec'] === 'vorbis' ) {
return true;
}
// For mp3, convert wav (previous command) to mp3 with ffmpeg
$lameCmdArgs = [
wfEscapeShellArg( $wgFFmpegLocation ),
'-y',
'-i',
wfEscapeShellArg( $this->getTargetEncodePath() . $outputFileExt ),
'-ss',
wfEscapeShellArg( $options['starttime'] ?? '0' ),
];
if ( isset( $options['audioQuality'] ) ) {
array_push( $lameCmdArgs, "-aq", wfEscapeShellArg( $options['audioQuality'] ) );
}
if ( isset( $options['audioBitrate'] ) ) {
- array_push( $lameCmdArgs, "-ab", (int)$options['audioBitrate'] * 1000 );
+ array_push( $lameCmdArgs, "-ab", $this->expandRate( $options['audioBitrate'] ) );
}
if ( isset( $options['samplerate'] ) ) {
array_push( $lameCmdArgs, "-ar", wfEscapeShellArg( $options['samplerate'] ) );
}
if ( isset( $options['channels'] ) ) {
array_push( $lameCmdArgs, "-ac", wfEscapeShellArg( $options['channels'] ) );
}
array_push(
$lameCmdArgs,
"-acodec",
"libmp3lame",
wfEscapeShellArg( $this->getTargetEncodePath() )
);
$lameCmdString = implode( " ", $lameCmdArgs );
$shellOutput = $this->runShellExec( $lameCmdString, $retval );
// Retval from fluidsynth command
if ( $retval !== 0 ) {
return $lameCmdString .
"\n\nExitcode: $retval\nMemory: $wgTranscodeBackgroundMemoryLimit\n\n" .
$shellOutput;
}
return true;
}
/**
* Runs the shell exec command.
* if $wgEnableBackgroundTranscodeJobs is enabled will mannage a background transcode task
* else it just directly passes off to wfShellExec
*
* @param string $cmd Command to be run
* @param int &$retval reference variable to return the exit code
* @return string
*/
public function runShellExec( $cmd, &$retval ) {
global $wgTranscodeBackgroundTimeLimit,
$wgTranscodeBackgroundMemoryLimit,
$wgTranscodeBackgroundSizeLimit,
$wgEnableNiceBackgroundTranscodeJobs;
// For profiling
$caller = wfGetCaller();
// Check if background tasks are enabled
if ( $wgEnableNiceBackgroundTranscodeJobs === false ) {
// Directly execute the shell command:
$limits = [
"filesize" => $wgTranscodeBackgroundSizeLimit,
"memory" => $wgTranscodeBackgroundMemoryLimit,
"time" => $wgTranscodeBackgroundTimeLimit
];
return wfShellExec( $cmd . ' 2>&1', $retval, [], $limits,
[ 'profileMethod' => $caller ] );
}
$encodingLog = $this->getTargetEncodePath() . '.stdout.log';
$retvalLog = $this->getTargetEncodePath() . '.retval.log';
// Check that we can actually write to these files
// ( no point in running the encode if we can't write )
AtEase::suppressWarnings();
if ( !touch( $encodingLog ) || !touch( $retvalLog ) ) {
AtEase::restoreWarnings();
$retval = 1;
return "Error could not write to target location";
}
AtEase::restoreWarnings();
// Fork out a process for running the transcode
$pid = pcntl_fork();
if ( $pid === -1 ) {
$errorMsg = '$wgEnableNiceBackgroundTranscodeJobs enabled but failed pcntl_fork';
$retval = 1;
$this->output( $errorMsg );
return $errorMsg;
}
if ( $pid === 0 ) {
// we are the child
$this->runChildCmd( $cmd, $retval, $encodingLog, $retvalLog, $caller );
// dont remove any temp files in the child process, this is done
// once the parent is finished
$this->targetEncodeFile->preserve();
if ( $this->source instanceof TempFSFile ) {
$this->source->preserve();
}
// exit with the same code as the transcode:
exit( $retval );
}
// we are the parent monitor and return status
return $this->monitorTranscode( $pid, $retval, $encodingLog, $retvalLog );
}
/**
* @param string $cmd
* @param int &$retval
* @param string $encodingLog
* @param string $retvalLog
* @param string $caller The calling method
*/
public function runChildCmd( $cmd, &$retval, $encodingLog, $retvalLog, $caller ) {
global $wgTranscodeBackgroundTimeLimit, $wgTranscodeBackgroundMemoryLimit,
$wgTranscodeBackgroundSizeLimit;
// In theory we should use pcntl_exec but not sure how to get the stdout, ensure
// we don't max php memory with the same protections provided by wfShellExec.
// pcntl_exec requires a direct path to the exe and arguments as an array:
// $cmd = explode(' ', $cmd );
// $baseCmd = array_shift( $cmd );
// print "run:" . $baseCmd . " args: " . print_r( $cmd, true );
// $status = pcntl_exec($baseCmd , $cmd );
// Directly execute the shell command:
// global $wgTranscodeBackgroundPriority;
// $status =
// wfShellExec( 'nice -n ' . $wgTranscodeBackgroundPriority . ' '. $cmd . ' 2>&1', $retval );
$limits = [
"filesize" => $wgTranscodeBackgroundSizeLimit,
"memory" => $wgTranscodeBackgroundMemoryLimit,
"time" => $wgTranscodeBackgroundTimeLimit
];
$status = wfShellExec( $cmd . ' 2>&1', $retval, [], $limits,
[ 'profileMethod' => $caller ] );
// Output the status:
AtEase::suppressWarnings();
file_put_contents( $encodingLog, $status );
// Output the retVal to the $retvalLog
file_put_contents( $retvalLog, $retval );
AtEase::restoreWarnings();
}
/**
* @param int $pid
* @param int &$retval
* @param string $encodingLog
* @param string $retvalLog
* @return string
*/
public function monitorTranscode( $pid, &$retval, $encodingLog, $retvalLog ) {
global $wgTranscodeBackgroundTimeLimit, $wgLang;
$errorMsg = '';
$loopCount = 0;
$oldFileSize = 0;
$startTime = time();
$fileIsNotGrowing = false;
$this->output( "Encoding with pid: $pid \npcntl_waitpid: " .
pcntl_waitpid( $pid, $status, WNOHANG | WUNTRACED ) .
"\nisProcessRunning: " . ( self::isProcessRunningKillZombie( $pid ) ? 'true' : 'false' ) .
"\n" );
// Check that the child process is still running
// ( note this does not work well with pcntl_waitpid for some reason :( )
while ( self::isProcessRunningKillZombie( $pid ) ) {
// $this->output( "$pid is running" );
// Check that the target file is growing ( every 5 seconds )
if ( $loopCount === 10 ) {
// only run check if we are outputing to target file
// ( two pass encoding does not output to target on first pass )
clearstatcache();
$newFileSize = is_file(
$this->getTargetEncodePath()
) ? filesize( $this->getTargetEncodePath() ) : 0;
// Don't start checking for file growth until we have an initial positive file size:
if ( $newFileSize > 0 ) {
$this->output( $wgLang->formatSize( $newFileSize ) . ' Total size, encoding ' .
$wgLang->formatSize( ( $newFileSize - $oldFileSize ) / 5 ) . ' per second' );
if ( $newFileSize === $oldFileSize ) {
if ( $fileIsNotGrowing ) {
$errorMsg = "Target File is not increasing in size, kill process.";
$this->output( $errorMsg );
// file is not growing in size, kill proccess
$retval = 1;
// posix_kill( $pid, 9);
self::killProcess( $pid );
break;
}
// Wait an additional 5 seconds of the file not growing to confirm
// the transcode is frozen.
$fileIsNotGrowing = true;
} else {
$fileIsNotGrowing = false;
}
$oldFileSize = $newFileSize;
}
// reset the loop counter
$loopCount = 0;
}
// Check if we have global job run-time has been exceeded:
if (
$wgTranscodeBackgroundTimeLimit && time() - $startTime > $wgTranscodeBackgroundTimeLimit
) {
$errorMsg = "Encoding exceeded max job run time ( "
. TimedMediaHandler::seconds2npt( $wgTranscodeBackgroundTimeLimit ) . " ), kill process.";
$this->output( $errorMsg );
// File is not growing in size, kill proccess
$retval = 1;
// posix_kill( $pid, 9);
self::killProcess( $pid );
break;
}
// Sleep for one second before repeating loop
$loopCount++;
sleep( 1 );
}
$returnPcntl = pcntl_wexitstatus( $status );
// check status
AtEase::suppressWarnings();
$returnCodeFile = file_get_contents( $retvalLog );
AtEase::restoreWarnings();
// File based exit code seems more reliable than pcntl_wexitstatus
$retval = (int)$returnCodeFile;
// return the encoding log contents ( will be inserted into error table if an error )
// ( will be ignored and removed if success )
if ( $errorMsg !== '' ) {
$errorMsg .= "\n\n";
}
return $errorMsg . file_get_contents( $encodingLog );
}
/**
* check if proccess is running and not a zombie
* @param int $pid
* @return bool
*/
public static function isProcessRunningKillZombie( $pid ) {
exec( "ps $pid", $processState );
if ( !isset( $processState[1] ) ) {
return false;
}
if ( strpos( $processState[1], '' ) !== false ) {
// posix_kill( $pid, 9);
self::killProcess( $pid );
return false;
}
return true;
}
/**
* Kill Application PID
*
* @param int $pid
* @return bool
*/
public static function killProcess( $pid ) {
exec( "kill -9 $pid" );
exec( "ps $pid", $processState );
if ( isset( $processState[1] ) ) {
return false;
}
return true;
}
}
class_alias( WebVideoTranscodeJob::class, 'WebVideoTranscodeJob' );
diff --git a/maintenance/.eslintrc.json b/maintenance/.eslintrc.json
new file mode 100644
index 00000000..12ea3e28
--- /dev/null
+++ b/maintenance/.eslintrc.json
@@ -0,0 +1,9 @@
+{
+ "root": true,
+ "extends": [
+ "wikimedia/node"
+ ],
+ "rules": {
+ "no-restricted-properties": "warn"
+ }
+}
diff --git a/maintenance/requeueTranscodes.php b/maintenance/requeueTranscodes.php
index 06ddb754..989f06a1 100644
--- a/maintenance/requeueTranscodes.php
+++ b/maintenance/requeueTranscodes.php
@@ -1,168 +1,170 @@
addOption( "file", "re-queue selected formats only for the given file", false, true );
$this->addOption( "start", "(re)start batch at the given file", false, true );
$this->addOption( "key", "re-queue for given format key", false, true );
$this->addOption( "error", "re-queue formats that previously failed" );
$this->addOption( "stalled", "re-queue formats that were started but not finished" );
$this->addOption( "missing", "queue formats that were never started" );
$this->addOption( "all", "re-queue all output formats" );
$this->addOption( "audio", "process audio files (defaults to all media types)" );
$this->addOption( "video", "process video files (defaults to all media types)" );
$this->addOption( "mime", "mime type to filter on (e.g. audio/midi)", false, true );
$this->addOption( "throttle", "throttle on the queue" );
+ $this->addOption( "manual-override", "override soft limits on output file size" );
$this->addDescription( "re-queue existing and missing media transcodes." );
$this->requireExtension( 'TimedMediaHandler' );
}
public function execute() {
$this->output( "Cleanup transcodes:\n" );
$dbr = wfGetDB( DB_REPLICA );
$types = [];
if ( $this->hasOption( 'audio' ) ) {
$types[] = 'AUDIO';
}
if ( $this->hasOption( 'video' ) ) {
$types[] = 'VIDEO';
}
if ( !$types ) {
// Default to all if none specified
$types = [ 'AUDIO', 'VIDEO' ];
}
$where = [ 'img_media_type' => $types ];
if ( $this->hasOption( 'mime' ) ) {
list( $major, $minor ) = File::splitMime( $this->getOption( 'mime' ) );
$where['img_major_mime'] = $major;
$where['img_minor_mime'] = $minor;
}
if ( $this->hasOption( 'file' ) ) {
$title = Title::newFromText( $this->getOption( 'file' ), NS_FILE );
if ( !$title ) {
$this->output( "Invalid --file option provided" );
return;
}
$where['img_name'] = $title->getDBkey();
}
if ( $this->hasOption( 'start' ) ) {
$title = Title::newFromText( $this->getOption( 'start' ), NS_FILE );
if ( !$title ) {
$this->output( "Invalid --start option provided" );
return;
}
$where[] = 'img_name >= ' . $dbr->addQuotes( $title->getDBkey() );
}
$opts = [ 'ORDER BY' => 'img_media_type,img_name' ];
$res = $dbr->select( 'image', [ 'img_name' ], $where, __METHOD__, $opts );
$localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
foreach ( $res as $row ) {
$title = Title::newFromText( $row->img_name, NS_FILE );
$file = $localRepo->newFile( $title );
$handler = $file ? $file->getHandler() : null;
if ( $file && $handler && $handler instanceof TimedMediaHandler ) {
$this->output( $file->getName() . "\n" );
$this->processFile( $file );
}
}
$this->output( "Finished!\n" );
}
/**
* @param File $file
*/
public function processFile( File $file ) {
$transcodeSet = WebVideoTranscode::enabledTranscodes();
$dbw = wfGetDB( DB_PRIMARY );
WebVideoTranscode::cleanupTranscodes( $file );
if ( $this->hasOption( "all" ) ) {
$toAdd = $toRemove = $transcodeSet;
} elseif ( $this->hasOption( "key" ) ) {
$toAdd = $toRemove = [ $this->getOption( 'key' ) ];
} else {
$toAdd = $transcodeSet;
$toRemove = [];
$state = WebVideoTranscode::getTranscodeState( $file, $dbw );
foreach ( $state as $key => $item ) {
if ( $this->hasOption( 'error' ) && $item['time_error'] ) {
$toRemove[] = $key;
continue;
}
if ( $this->hasOption( 'stalled' ) &&
( $item['time_addjob'] && !$item['time_success'] && !$item['time_error'] ) ) {
$toRemove[] = $key;
continue;
}
if ( $this->hasOption( 'missing' ) &&
( !$item['time_addjob'] ) ) {
$toRemove[] = $key;
continue;
}
}
}
if ( $toRemove ) {
$state = WebVideoTranscode::getTranscodeState( $file, $dbw );
$keys = array_intersect( $toRemove, array_keys( $state ) );
natsort( $keys );
foreach ( $keys as $key ) {
$this->output( ".. removing $key\n" );
WebVideoTranscode::removeTranscodes( $file, $key );
}
}
if ( $toAdd ) {
$keys = $toAdd;
$state = WebVideoTranscode::getTranscodeState( $file, $dbw );
natsort( $keys );
foreach ( $keys as $key ) {
if ( !WebVideoTranscode::isTranscodeEnabled( $file, $key ) ) {
// don't enqueue too-big files
continue;
}
if ( !array_key_exists( $key, $state ) || !$state[$key]['time_addjob'] ) {
$this->output( ".. queueing $key\n" );
+ $manualOverride = $this->hasOption( 'manual-override' );
if ( !$this->hasOption( 'throttle' ) ) {
- WebVideoTranscode::updateJobQueue( $file, $key );
+ WebVideoTranscode::updateJobQueue( $file, $key, $manualOverride );
} else {
$startSize = WebVideoTranscode::getQueueSize( $file, $key );
- WebVideoTranscode::updateJobQueue( $file, $key );
+ WebVideoTranscode::updateJobQueue( $file, $key, $manualOverride );
while ( true ) {
$size = WebVideoTranscode::getQueueSize( $file, $key );
if ( $size > $startSize ) {
$this->output( ".. (queue $size) " );
sleep( 1 );
} else {
$this->output( "\n" );
break;
}
}
}
}
}
}
}
}
// Tells it to run the class
$maintClass = RequeueTranscodes::class;
require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/targetBitrates.js b/maintenance/targetBitrates.js
new file mode 100644
index 00000000..b534717a
--- /dev/null
+++ b/maintenance/targetBitrates.js
@@ -0,0 +1,43 @@
+'use strict';
+
+// Build a table of target output bitrates to be used
+// for reference editing WebVideoTranscode.php
+
+let heights = [
+ 120,
+ 180,
+ 240,
+ 360,
+ 480,
+ 720,
+ 1080,
+ 1440,
+ 2160
+];
+
+let base = 480;
+let exponent = 0.85;
+
+function area(h) {
+ let w = Math.round(h * 16 / 9);
+ return w * h;
+}
+
+function rate(h, bitrate) {
+ let ratio = area(h) / area(base);
+ return Math.round(bitrate * (ratio ** exponent));
+}
+
+let codecs = [
+ ['vp9', 1000],
+ ['vp8', 1280],
+ ['h264', 1280]
+];
+
+for (let [codec, bitrate] of codecs) {
+ console.log(codec);
+ for (let h of heights) {
+ console.log(`${h}: ${rate(h, bitrate)}`);
+ }
+ console.log('');
+}
\ No newline at end of file
diff --git a/resources/transcode-table/transcode-table.js b/resources/transcode-table/transcode-table.js
index 1aaf6cec..41e979eb 100644
--- a/resources/transcode-table/transcode-table.js
+++ b/resources/transcode-table/transcode-table.js
@@ -1,65 +1,99 @@
/*!
* Javascript to support transcode table on image page
*/
$( function () {
function errorPopup( event ) {
var tKey = $( event.target ).attr( 'data-transcodekey' ),
$message = $( [
document.createTextNode( mw.msg( 'timedmedia-reset-explanation' ) ),
document.createElement( 'br' ),
document.createElement( 'br' ),
document.createTextNode( mw.msg( 'timedmedia-reset-areyousure' ) )
] );
event.preventDefault();
OO.ui.confirm( $message, {
title: mw.msg( 'timedmedia-reset' ),
actions: [
{
action: 'accept',
label: mw.msg( 'timedmedia-reset-button-reset' ),
flags: [ 'primary', 'destructive' ]
},
{
action: 'cancel',
label: mw.msg( 'timedmedia-reset-button-cancel' ),
flags: 'safe'
}
]
} ).done( function ( confirmed ) {
var api;
if ( confirmed ) {
api = new mw.Api();
api.postWithEditToken( {
action: 'transcodereset',
transcodekey: tKey,
title: mw.config.get( 'wgPageName' ),
errorformat: 'html'
} ).done( function () {
// Refresh the page
location.reload();
} ).fail( function ( code, data ) {
var errorText;
if ( data.errors ) {
errorText = data.errors[ 0 ][ '*' ];
} else {
errorText = mw.msg( 'timedmedia-reset-error' );
}
OO.ui.alert( errorText, {
actions: [
{
action: 'ok',
label: mw.msg( 'timedmedia-reset-button-dismiss' ),
flags: 'safe'
}
]
} );
} );
}
} );
}
// eslint-disable-next-line no-jquery/no-global-selector
$( '.mw-filepage-transcodereset a' ).on( 'click', errorPopup );
+
+ function displayErrorDetails( error ) {
+ var messageDialog = new OO.ui.MessageDialog();
+ var windowManager = OO.ui.getWindowManager();
+ windowManager.addWindows( [ messageDialog ] );
+ windowManager.openWindow( messageDialog, {
+ title: mw.msg( 'timedmedia-error-details' ),
+ message: error,
+ actions: [
+ {
+ action: 'accept',
+ label: mw.msg( 'timedmedia-error-dismiss' ),
+ flags: 'primary'
+ }
+ ]
+ } );
+ }
+
+ mw.hook( 'wikipage.content' ).add( function ( $content ) {
+ $content.find( '.mw-tmh-pseudo-error-link' ).each( function ( _index, el ) {
+ var $el = $( el );
+ var error = String( $el.data( 'error' ) );
+ var text = $el.text();
+ var $link = $( document.createElement( 'a' ) )
+ .text( text )
+ .attr( 'href', '' )
+ .on( 'click', function ( event ) {
+ event.preventDefault();
+ displayErrorDetails( error );
+ } );
+ $el.replaceWith( $link );
+ } );
+ } );
+
} );