Skip to content

Instantly share code, notes, and snippets.

@ciencia
Last active March 16, 2025 21:20
Show Gist options
  • Save ciencia/66a8d5eb6ad540f0bf94900358bc51c8 to your computer and use it in GitHub Desktop.
Save ciencia/66a8d5eb6ad540f0bf94900358bc51c8 to your computer and use it in GitHub Desktop.
patch MediaWiki extension TimedMediaHandler autoplay videos REL1_43
diff --git a/extension.json b/extension.json
index e5780d22..2d72f6b3 100644
--- a/extension.json
+++ b/extension.json
@@ -127,10 +127,12 @@
"FileDeleteComplete": "main",
"FileUndeleteComplete": "main",
"FileUpload": "main",
+ "GetPreferences": "main",
"ImageOpenShowImageInlineBefore": "main",
"ImagePageAfterImageLinks": "main",
"ImagePageFileHistoryLine": "main",
"LoadExtensionSchemaUpdates": "installer",
+ "MakeGlobalVariablesScript": "main",
"MediaWikiPerformAction": "iframe",
"RevisionFromEditComplete": "main",
"PageMoveComplete": [
@@ -363,8 +365,16 @@
"description": "Path of a soundfont to use for MIDI-converted audio",
"public": true,
"value": null
+ },
+ "TmhEnableAutoPlayVideos": {
+ "description": "Allow videos to play automatically (but muted) using the autoplay attribute.",
+ "public": true,
+ "value": false
}
},
+ "DefaultUserOptions": {
+ "timedmediahandler-autoplayvideos": "allow"
+ },
"ForeignResourcesDir": "resources/lib",
"ResourceFileModulePaths": {
"localBasePath": "resources",
diff --git a/i18n/TimedMediaHandler.i18n.magic.php b/i18n/TimedMediaHandler.i18n.magic.php
index 737b4e06..4ba55f7a 100644
--- a/i18n/TimedMediaHandler.i18n.magic.php
+++ b/i18n/TimedMediaHandler.i18n.magic.php
@@ -15,6 +15,7 @@ $magicWords['en'] = [
'timedmedia_disablecontrols' => [ 0, 'disablecontrols=$1' ],
'timedmedia_loop' => [ 0, 'loop' ],
'timedmedia_muted' => [ 0, 'muted' ],
+ 'timedmedia_autoplay' => [ 0, 'autoplay' ],
];
$magicWords['af'] = [
diff --git a/i18n/en.json b/i18n/en.json
index 6504827b..a082bc22 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -190,6 +190,9 @@
"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}}",
+ "timedmedia-preference-autoplayvideos": "Behavior of videos marked to play automatically",
+ "timedmedia-preference-autoplayvideos-allow": "Allow videos marked as such to play automatically",
+ "timedmedia-preference-autoplayvideos-never": "Never play videos automatically",
"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",
diff --git a/i18n/es.json b/i18n/es.json
index 93c6b91c..6b8fc26a 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -139,6 +139,9 @@
"timedmedia-duration-hms": "{{PLURAL:$1|$1 hora|$1 horas}}, {{PLURAL:$2|$2 minuto|$2 minutos}} y {{PLURAL:$3|$3 segundo|$3 segundos}}",
"timedmedia-duration-ms": "{{PLURAL:$1|$1 minuto|$1 minutos}} y {{PLURAL:$2|$2 segundo|$2 segundos}}",
"timedmedia-duration-s": "{{PLURAL:$1|$1 segundo|$1 segundos}}",
+ "timedmedia-preference-autoplayvideos": "Comportamiento de los vídeos marcados para su reproducción automática",
+ "timedmedia-preference-autoplayvideos-allow": "Permitir la reproducción automática de vídeos",
+ "timedmedia-preference-autoplayvideos-never": "Nunca permitir la reproducción automática de vídeos",
"right-transcode-reset": "Reiniciar los vídeos erróneos o transcodificados por lo que se vuelven a colocar en la cola de trabajo",
"right-transcode-status": "Ver [[Special:TimedMediaHandler|información sobre la actividad de transcodificación actual]]",
"action-transcode-reset": "reiniciar transcodificación",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 5d524022..e25b5732 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -212,6 +212,9 @@
"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",
+ "timedmedia-preference-autoplayvideos": "Label in user preferences for a drop-down controlling the behavior of videos marked to play automatically.",
+ "timedmedia-preference-autoplayvideos-allow": "Videos marked to play automatically will start playing upon page load. One of the options for {{msg-mw|timedmedia-preference-autoplayvideos}}.",
+ "timedmedia-preference-autoplayvideos-never": "Videos marked to play automatically will require user interaction to start playing. One of the options for {{msg-mw|timedmedia-preference-autoplayvideos}}.",
"right-transcode-reset": "{{doc-right|transcode-reset}}",
"right-transcode-status": "{{doc-right|transcode-status}}",
"action-transcode-reset": "{{doc-action|transcode-reset}}",
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 67314938..242cd541 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -17,12 +17,14 @@ use MediaWiki\Hook\CanonicalNamespacesHook;
use MediaWiki\Hook\FileDeleteCompleteHook;
use MediaWiki\Hook\FileUndeleteCompleteHook;
use MediaWiki\Hook\FileUploadHook;
+use MediaWiki\Hook\MakeGlobalVariablesScriptHook;
use MediaWiki\Hook\PageMoveCompleteHook;
use MediaWiki\Hook\ParserTestGlobalsHook;
use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
use MediaWiki\Hook\TitleMoveHook;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
use MediaWiki\Output\Hook\BeforePageDisplayHook;
use MediaWiki\Output\OutputPage;
use MediaWiki\Page\Hook\ArticleFromTitleHook;
@@ -31,6 +33,7 @@ use MediaWiki\Page\Hook\ImageOpenShowImageInlineBeforeHook;
use MediaWiki\Page\Hook\ImagePageAfterImageLinksHook;
use MediaWiki\Page\Hook\ImagePageFileHistoryLineHook;
use MediaWiki\Page\Hook\RevisionFromEditCompleteHook;
+use MediaWiki\Preferences\Hook\GetPreferencesHook;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\SpecialPage\Hook\WgQueryPagesHook;
use MediaWiki\SpecialPage\SpecialPageFactory;
@@ -61,9 +64,11 @@ class Hooks implements
FileDeleteCompleteHook,
FileUndeleteCompleteHook,
FileUploadHook,
+ GetPreferencesHook,
ImageOpenShowImageInlineBeforeHook,
ImagePageAfterImageLinksHook,
ImagePageFileHistoryLineHook,
+ MakeGlobalVariablesScriptHook,
PageMoveCompleteHook,
ParserTestGlobalsHook,
RevisionFromEditCompleteHook,
@@ -478,6 +483,44 @@ class Hooks implements
}
}
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
+ * Register extension preferences
+ * @param User $user
+ * @param array &$prefs
+ */
+ public function onGetPreferences( $user, &$prefs ) {
+ global $wgTmhEnableAutoPlayVideos;
+
+ if ( $wgTmhEnableAutoPlayVideos === true ) {
+ $prefs['timedmediahandler-autoplayvideos'] = [
+ 'type' => 'select',
+ 'label-message' => 'timedmedia-preference-autoplayvideos',
+ 'section' => 'rendering/files',
+ 'options-messages' => [
+ 'timedmedia-preference-autoplayvideos-allow' => 'allow',
+ 'timedmedia-preference-autoplayvideos-never' => 'never',
+ ],
+ ];
+ }
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/MakeGlobalVariablesScript
+ * Export variables which depend on the current user
+ * @param array &$vars
+ * @param OutputPage $out
+ * @return void
+ */
+ public function onMakeGlobalVariablesScript( &$vars, $out ): void {
+ global $wgTmhEnableAutoPlayVideos;
+
+ $user = $out->getUser();
+ $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
+ $vars['wgTMHAutoplayVideos'] = $wgTmhEnableAutoPlayVideos &&
+ $userOptionsLookup->getOption( $user, 'timedmediahandler-autoplayvideos' ) == 'allow';
+ }
+
/**
* @param array &$qp
*/
diff --git a/includes/TimedMediaHandler.php b/includes/TimedMediaHandler.php
index 8ed12437..d77eab90 100644
--- a/includes/TimedMediaHandler.php
+++ b/includes/TimedMediaHandler.php
@@ -38,6 +38,7 @@ class TimedMediaHandler extends MediaHandler {
'timedmedia_disablecontrols' => 'disablecontrols',
'timedmedia_loop' => 'loop',
'timedmedia_muted' => 'muted',
+ 'timedmedia_autoplay' => 'autoplay',
];
}
@@ -134,6 +135,8 @@ class TimedMediaHandler extends MediaHandler {
* @return bool
*/
public function normaliseParams( $image, &$params ) {
+ global $wgTmhEnableAutoPlayVideos;
+
$timeParam = [ 'thumbtime', 'start', 'end' ];
// Parse time values if endtime or thumbtime can't be more than length -1
foreach ( $timeParam as $pn ) {
@@ -193,6 +196,18 @@ class TimedMediaHandler extends MediaHandler {
foreach ( [ 'loop', 'muted' ] as $flag ) {
$params[ $flag ] = isset( $params[ $flag ] );
}
+
+ // Autoplay only for video, since audio will likely be blocked
+ // by browsers
+ $params[ 'autoplay' ] = isset( $params[ 'autoplay' ] ) &&
+ !$this->isAudio( $image ) &&
+ ( $wgTmhEnableAutoPlayVideos === true );
+
+ // Autoplay videos must be muted
+ if ( $params[ 'autoplay' ] ) {
+ $params[ 'muted' ] = true;
+ }
+
return true;
}
@@ -380,6 +395,7 @@ class TimedMediaHandler extends MediaHandler {
'loop' => $params['loop'] ?? false,
'muted' => $params['muted'] ?? false,
'inline' => $params['inline'] ?? false,
+ 'autoPlay' => $params['autoplay'] ?? false,
];
// Allow start and end query string params on image pages (T203994)
diff --git a/includes/TimedMediaTransformOutput.php b/includes/TimedMediaTransformOutput.php
index 8a0ba68f..12e6f406 100644
--- a/includes/TimedMediaTransformOutput.php
+++ b/includes/TimedMediaTransformOutput.php
@@ -64,6 +64,9 @@ class TimedMediaTransformOutput extends MediaTransformOutput {
/** @var bool */
protected $loop;
+ /** @var bool */
+ protected $autoPlay;
+
// The prefix for player ids
private const PLAYER_ID_PREFIX = 'mwe_player_';
@@ -89,6 +92,7 @@ class TimedMediaTransformOutput extends MediaTransformOutput {
$this->inline = $conf['inline'] ?? false;
$this->muted = $conf['muted'] ?? false;
$this->loop = $conf['loop'] ?? false;
+ $this->autoPlay = $conf['autoPlay'] ?? false;
}
/**
@@ -466,10 +470,19 @@ class TimedMediaTransformOutput extends MediaTransformOutput {
if ( $this->fillwindow ) {
$mediaAttr[ 'data-player' ] = 'fillwindow';
}
- if ( $this->inline ) {
+ if ( $this->inline || $this->autoPlay ) {
$mediaAttr['class'] .= ' mw-tmh-inline';
+ if ( $this->autoPlay ) {
+ // javascript will trigger the video playback with this class
+ $mediaAttr['class'] .= ' mw-tmh-autoplay';
+ }
$mediaAttr['playsinline'] = '';
- $mediaAttr['preload'] = 'auto';
+ if ( $this->inline ) {
+ // autoplay will start playing when it gets on the viewport
+ // Do not preload it to prevent browsers from downloading
+ // a video that may never be viewed
+ $mediaAttr['preload'] = 'auto';
+ }
}
// Used by Score extension and to disable specific controls from wikicode
diff --git a/resources/ext.tmh.player.element.js b/resources/ext.tmh.player.element.js
index e497bdf3..f4b932bd 100644
--- a/resources/ext.tmh.player.element.js
+++ b/resources/ext.tmh.player.element.js
@@ -1,5 +1,14 @@
const OgvJsSupport = require( 'ext.tmh.OgvJsSupport' );
+/**
+ * Watches for autoplay videos entering or leaving the viewport, to play or
+ * pause them automatically
+ *
+ * @static
+ * @type {IntersectionObserver}
+ */
+let lazyVideoObserver = null;
+
function secondsToComponents( totalSeconds ) {
totalSeconds = parseInt( totalSeconds, 10 );
const hours = Math.floor( totalSeconds / 3600 );
@@ -54,6 +63,52 @@ function secondsToDurationLongString( totalSeconds ) {
return mw.msg( 'timedmedia-duration-s', seconds );
}
+/**
+ * Checks video element visibility and play/pause them
+ * Only relevant for autoplay videos
+ *
+ * @param {IntersectionObserverEntry} entry
+ */
+function playPauseVideosFromVisibility( entry ) {
+ const $element = $( entry.target );
+ const mediaElement = $element.data( 'MediaElement' );
+ const videojsPlayer = $element.data( 'videojsPlayer' );
+
+ if ( entry.isIntersecting ) {
+ if ( !videojsPlayer ) {
+ // Player not initialized
+ // Unobserve current element since it will be
+ // replaced by a cloned node
+ lazyVideoObserver.unobserve( entry.target );
+ mediaElement.playInlineOrOpenDialog();
+ } else if (
+ videojsPlayer.paused() &&
+ mediaElement.pausedByIntersectionObserver
+ ) {
+ videojsPlayer.play();
+ mediaElement.pausedByIntersectionObserver = false;
+ }
+ } else {
+ if ( videojsPlayer && !videojsPlayer.paused() ) {
+ videojsPlayer.pause();
+ mediaElement.pausedByIntersectionObserver = true;
+ }
+ }
+}
+
+function initLazyVideoObserver() {
+ if ( lazyVideoObserver !== null ) {
+ return;
+ }
+ if ( 'IntersectionObserver' in window ) {
+ lazyVideoObserver = new IntersectionObserver(
+ function ( entries ) {
+ entries.forEach( playPauseVideosFromVisibility );
+ }
+ );
+ }
+}
+
/**
* Main entry class for elements enhanced with videojs
* Provides page player loading, either with click-to-load dialog or inline mode
@@ -66,7 +121,14 @@ class MediaElement {
this.element = element;
this.$element = $( element );
this.isAudio = element.tagName.toLowerCase() === 'audio';
+ this.isAutoplayVideo = mw.config.get( 'wgTMHAutoplayVideos' ) === true &&
+ !this.isAudio &&
+ element.classList.contains( 'mw-tmh-autoplay' );
this.$placeholder = null;
+ // This property gets set for autoplay videos when they were playing
+ // and went outside of the viewport. Need to differentiate from videos
+ // paused by the user
+ this.pausedByIntersectionObserver = false;
}
/**
@@ -92,7 +154,9 @@ class MediaElement {
id: $clonedVid.attr( 'id' ) + '_placeholder',
disabled: '',
tabindex: -1
- } ).removeAttr( 'src' );
+ } )
+ .removeAttr( 'src' )
+ .data( 'MediaElement', this );
if ( !this.isAudio ) {
const aspectRatio = this.$element.attr( 'width' ) + ' / ' + this.$element.attr( 'height' );
@@ -156,6 +220,12 @@ class MediaElement {
if ( playing ) {
this.playInlineOrOpenDialog();
+ } else if ( this.isAutoplayVideo ) {
+ initLazyVideoObserver();
+ // Autoplay videos will be handled by the intersection observer
+ if ( lazyVideoObserver !== null ) {
+ lazyVideoObserver.observe( $clonedVid[ 0 ] );
+ }
}
}
@@ -244,10 +314,15 @@ class MediaElement {
* play the element in the dialog.
*/
playInlineOrOpenDialog() {
- MediaElement.$interstitial = $( '<div>' ).addClass( 'mw-tmh-player-interstitial' )
- .append( $( '<div>' ).addClass( 'mw-tmh-player-progress' )
- .append( $( '<div>' ).addClass( 'mw-tmh-player-progress-bar' ) ) )
- .appendTo( document.body );
+ // Do not add a progress bar for inline player
+ if ( !this.isInline() ) {
+ MediaElement.$interstitial = $( '<div>' ).addClass( 'mw-tmh-player-interstitial' )
+ .append( $( '<div>' ).addClass( 'mw-tmh-player-progress' )
+ .append( $( '<div>' ).addClass( 'mw-tmh-player-progress-bar' ) ) )
+ .appendTo( document.body );
+ } else {
+ MediaElement.$interstitial = $();
+ }
// If we're using ogv.js, we have to initialize the audio context
// during a click event to work on Safari, especially for iOS.
@@ -278,6 +353,7 @@ class MediaElement {
}
if ( this.isInline() ) {
+ const self = this;
mw.loader.using( 'ext.tmh.player.inline' ).then( () => {
this.$placeholder.find( 'a, .mw-tmh-label' ).detach();
this.$placeholder.find( 'video,audio' )
@@ -299,7 +375,21 @@ class MediaElement {
// Support: Edge 18
setTimeout( () => {
MediaElement.$interstitial.detach();
+ if ( self.isAutoplayVideo ) {
+ // Hides the control bar for autoplay videos when they
+ // start playing
+ videojsPlayer.userActive( false );
+ }
videojsPlayer.play();
+ if ( self.isAutoplayVideo && lazyVideoObserver !== null ) {
+ // References for IntersectionObserver
+ $( self.element )
+ .data( 'videojsPlayer', videojsPlayer )
+ .data( 'MediaElement', self );
+ // Add the element to the IntersectionObserver to pause
+ // it when it goes outside of the viewport
+ lazyVideoObserver.observe( self.element );
+ }
}, 0 );
} );
} );
diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt
index ac4cba8a..4a98bc08 100644
--- a/tests/parser/parserTests.txt
+++ b/tests/parser/parserTests.txt
@@ -240,6 +240,30 @@ wgParserEnableLegacyMediaDOM=false
<p><span class="mw-default-size" typeof="mw:File" data-mw='{"caption":"These are bogus."}'><span title="These are bogus."><video poster="http://example.com/images/thumb/0/00/Video.ogv/320px--Video.ogv.jpg" controls="" preload="none" muted="" loop="" height="240" width="320" resource="./File:Video.ogv" data-durationhint="5" class="mw-file-element"><source src="http://example.com/images/0/00/Video.ogv" type='video/ogg; codecs="theora"' data-file-width="320" data-file-height="240"/></video></span></span></p>
!! end
+!! test
+Video with flag autoplay but not enabled from config
+!! wikitext
+[[File:Video.ogv|autoplay|These are bogus.]]
+!! html/php
+<p><span class="mw-default-size" typeof="mw:File"><span><video id="mwe_player_1" poster="http://example.com/images/thumb/0/00/Video.ogv/320px--Video.ogv.jpg" controls="" preload="none" data-mw-tmh="" class="mw-file-element" width="320" height="240" data-durationhint="5" data-mwtitle="Video.ogv" data-mwprovider="local"><source src="http://example.com/images/0/00/Video.ogv" type="video/ogg; codecs=&quot;theora&quot;" data-width="320" data-height="240" /></video></span></span>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:File" data-parsoid='{"optList":[{"ck":"bogus","ak":"noplayer"},{"ck":"bogus","ak":"noicon"},{"ck":"bogus","ak":"disablecontrols=ok"},{"ck":"caption","ak":"These are bogus."}]}' data-mw='{"caption":"These are bogus."}'><span><video poster="http://example.com/images/thumb/0/00/Video.ogv/320px--Video.ogv.jpg" controls="" preload="none" height="240" width="320" resource="./File:Video.ogv"><source src="http://example.com/images/0/00/Video.ogv" type='video/ogg; codecs="theora"' data-file-width="320" data-file-height="240" data-title="Original Ogg file, 320 × 240 (590 kbps)" data-shorttitle="Ogg source"/></video></span></span></p>
+!! end
+
+!! test
+Video with flag autoplay
+!! config
+wgTmhEnableAutoPlayVideos=true
+!! wikitext
+[[File:Video.ogv|autoplay|These are bogus.]]
+!! html/php
+<p><span class="mw-default-size" typeof="mw:File"><span><video id="mwe_player_1" poster="http://example.com/images/thumb/0/00/Video.ogv/320px--Video.ogv.jpg" controls="" preload="none" muted="" data-mw-tmh="" class="mw-tmh-inline mw-tmh-autoplay mw-file-element" width="320" height="240" playsinline="" data-durationhint="5" data-mwtitle="Video.ogv" data-mwprovider="local"><source src="http://example.com/images/0/00/Video.ogv" type="video/ogg; codecs=&quot;theora&quot;" data-width="320" data-height="240" /></video></span></span>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:File" data-parsoid='{"optList":[{"ck":"bogus","ak":"noplayer"},{"ck":"bogus","ak":"noicon"},{"ck":"bogus","ak":"disablecontrols=ok"},{"ck":"caption","ak":"These are bogus."}]}' data-mw='{"caption":"These are bogus."}'><span><video poster="http://example.com/images/thumb/0/00/Video.ogv/320px--Video.ogv.jpg" controls="" preload="none" height="240" width="320" resource="./File:Video.ogv"><source src="http://example.com/images/0/00/Video.ogv" type='video/ogg; codecs="theora"' data-file-width="320" data-file-height="240" data-title="Original Ogg file, 320 × 240 (590 kbps)" data-shorttitle="Ogg source"/></video></span></span></p>
+!! end
+
## FIXME: Mock transcoding on the php side as well
!! test
Video with a transcoded source
diff --git a/tests/phpunit/mocks/MockOggHandler.php b/tests/phpunit/mocks/MockOggHandler.php
index 299d1f66..783a8650 100644
--- a/tests/phpunit/mocks/MockOggHandler.php
+++ b/tests/phpunit/mocks/MockOggHandler.php
@@ -64,6 +64,7 @@ class MockOggHandler extends OggHandler {
'disablecontrols' => $params['disablecontrols'] ?? false,
'loop' => $params['loop'] ?? false,
'muted' => $params['muted'] ?? false,
+ 'autoPlay' => $params['autoplay'] ?? false,
];
// No thumbs for audio
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment