diff options
Diffstat (limited to 'extensions/TimedMediaHandler/TimedMediaTransformOutput.php')
-rw-r--r-- | extensions/TimedMediaHandler/TimedMediaTransformOutput.php | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/TimedMediaTransformOutput.php b/extensions/TimedMediaHandler/TimedMediaTransformOutput.php new file mode 100644 index 00000000..36532093 --- /dev/null +++ b/extensions/TimedMediaHandler/TimedMediaTransformOutput.php @@ -0,0 +1,486 @@ +<?php + +class TimedMediaTransformOutput extends MediaTransformOutput { + static $serial = 0; + + // Video file sources object lazy init in getSources() + var $sources = null; + var $textTracks = null; + var $hashTime = null; + var $textHandler = null; // lazy init in getTextHandler + var $disablecontrols = null; + + var $start, $end, $fillwindow; + + // The prefix for player ids + const PLAYER_ID_PREFIX = 'mwe_player_'; + + function __construct( $conf ){ + $options = array( 'file', 'dstPath', 'sources', 'thumbUrl', 'start', 'end', + 'width', 'height', 'length', 'offset', 'isVideo', 'path', 'fillwindow', + 'sources', 'disablecontrols' ); + foreach ( $options as $key ) { + if( isset( $conf[ $key ]) ){ + $this->$key = $conf[$key]; + } else { + $this->$key = false; + } + } + } + + /** + * @return TextHandler + */ + function getTextHandler(){ + if( !$this->textHandler ){ + // Init an associated textHandler + $this->textHandler = new TextHandler( $this->file ); + } + return $this->textHandler; + } + + /** + * Get the media transform thumbnail + * @return string + */ + function getUrl( $sizeOverride = false ){ + global $wgVersion, $wgResourceBasePath, $wgStylePath; + // Needs to be 1.24c because version_compare() works in confusing ways + if ( version_compare( $wgVersion, '1.24c', '>=' ) ) { + $url = "$wgResourceBasePath/resources/assets/file-type-icons/fileicon-ogg.png"; + } else { + $url = "$wgStylePath/common/images/icons/fileicon-ogg.png"; + } + + if ( $this->isVideo ) { + if ( $this->thumbUrl ) { + $url = $this->thumbUrl; + } + + // Update the $posterUrl to $sizeOverride ( if not an old file ) + if( !$this->file->isOld() && $sizeOverride && + $sizeOverride[0] && intval( $sizeOverride[0] ) != intval( $this->width ) ){ + $apiUrl = $this->getPoster( $sizeOverride[0] ); + if( $apiUrl ){ + $url = $apiUrl; + } + } + } + return $url; + } + + /** + * TODO get the local path + * @return mixed + */ + function getPath(){ + return $this->dstPath; + } + + /** + * @return int + */ + function getPlayerHeight(){ + // Check if "video" tag output: + if ( $this->isVideo ) { + return intval( $this->height ); + } else { + // Give sound files a height of 23px + return 23; + } + } + + /** + * @return int + */ + function getPlayerWidth(){ + // Check if "video" tag output: + if ( $this->isVideo ) { + return intval( $this->width ); + } else { + // Give sound files a width of 300px ( if unsized ) + if( $this->width == 0 ){ + return 300; + } + // else give the target size down to 35 px wide + return ( $this->width < 35 ) ? 35 : intval( $this->width ) ; + } + } + + /** + * @return string + */ + function getTagName(){ + return ( $this->isVideo ) ? 'video' : 'audio'; + } + + /** + * @param $options array + * @return string + * @throws Exception + */ + function toHtml( $options = array() ) { + if ( count( func_get_args() ) == 2 ) { + throw new Exception( __METHOD__ .' called in the old style' ); + } + + $oldHeight = $this->height; + $oldWidth = $this->width; + if ( isset( $options['override-height'] ) ) { + $this->height = $options['override-height']; + } + if ( isset( $options['override-width'] ) ) { + $this->width = $options['override-width']; + } + + if ( $this->useImagePopUp() ) { + $res = $this->getImagePopUp(); + } else { + $res = $this->getHtmlMediaTagOutput(); + } + $this->width = $oldWidth; + $this->height = $oldHeight; + return $res; + } + + /** + * Helper to determine if to use pop up dialog for videos + * + * @return boolean + */ + private function useImagePopUp() { + global $wgMinimumVideoPlayerSize; + // Check if the video is too small to play inline ( instead do a pop-up dialog ) + // If we're filling the window (e.g. during an iframe embed) one probably doesn't want the pop up. + // Also the pop up is broken in that case. + return $this->isVideo + && !$this->fillwindow + && $this->getPlayerWidth() < $wgMinimumVideoPlayerSize + // Do not do pop-up if its going to be the same size as inline player anyways + && $this->getPlayerWidth() < $this->getPopupPlayerWidth(); + } + + /** + * XXX migrate this to the mediawiki Html class as 'tagSet' helper function + * @param $tagName + * @param $tagSet + * @return string + */ + static function htmlTagSet( $tagName, $tagSet ){ + if( empty( $tagSet ) ){ + return ''; + } + $s = ''; + foreach( $tagSet as $attr ){ + $s .= Html::element( $tagName, $attr); + } + return $s; + } + + /** + * @return string + */ + function getImagePopUp(){ + // pop up videos set the autoplay attribute to true: + $autoPlay = true; + return Xml::tags( 'div' , array( + 'id' => self::PLAYER_ID_PREFIX . TimedMediaTransformOutput::$serial++, + 'class' => 'PopUpMediaTransform', + 'style' => "width:" . $this->getPlayerWidth() . "px;", + 'videopayload' => $this->getHtmlMediaTagOutput( $this->getPopupPlayerSize(), $autoPlay ), + ), + Xml::tags( 'img', array( + 'alt' => $this->file->getTitle(), + 'style' => "width:" . $this->getPlayerWidth() . "px;height:" . + $this->getPlayerHeight() . "px", + 'src' => $this->getUrl(), + ),'') + . + // For javascript disabled browsers provide a link to the asset: + Xml::tags( 'a', array( + 'href'=> $this->file->getUrl(), + 'title' => wfMessage( 'timedmedia-play-media' )->escaped(), + 'target' => 'new' + ), + Xml::tags( 'span', array( + 'class' => 'play-btn-large' + ), + // Have some sort of text for lynx & screen readers. + Html::element( + 'span', + array( 'class' => 'mw-tmh-playtext' ), + wfMessage( 'timedmedia-play-media' )->text() + ) + ) + ) + ); + } + + /** + * Get target popup player size + */ + function getPopupPlayerSize(){ + // Get the max width from the enabled transcode settings: + $maxImageSize = WebVideoTranscode::getMaxSizeWebStream(); + return WebVideoTranscode::getMaxSizeTransform( $this->file, $maxImageSize); + } + + /** + * Helper function to get pop up width + * + * Silly function because array index operations aren't allowed + * on function calls before php 5.4 + */ + private function getPopupPlayerWidth() { + list( $popUpWidth ) = $this->getPopupPlayerSize(); + return $popUpWidth; + } + + /** + * Sort media by bandwidth, but with things not wide enough at end + * + * The list should be in preferred source order, so we want the file + * with the lowest bitrate (to save bandwidth) first, but we also want + * appropriate resolution files before the 160p transcodes. + */ + private function sortMediaByBandwidth( $a, $b ) { + $width = $this->getPlayerWidth(); + $maxWidth = $this->getPopupPlayerWidth(); + if ( $this->useImagePopUp() || $width > $maxWidth ) { + // If its a pop-up player than we should use the pop up player size + // if its a normal player, but has a bigger width than the pop-up + // player, then we use the pop-up players width as the target width + // as that is equivalent to the max transcode size. Otherwise this + // will suggest the original file as the best source, which seems like + // a potentially bad idea, as it could be anything size wise. + $width = $maxWidth; + } + + if ( $a['width'] < $width && $b['width'] >= $width ) { + // $a is not wide enough but $b is + // so we consider $a > $b as we want $b before $a + return 1; + } + if ( $a['width'] >= $width && $b['width'] < $width ) { + // $b not wide enough, so $a must be preferred. + return -1; + } + if ( $a['width'] < $width && $b['width'] < $width && $a['width'] != $b['width'] ) { + // both are too small. Go with the one closer to the target width + return ( $a['width'] < $b['width'] ) ? -1 : 1; + } + // Both are big enough, or both equally too small. Go with the one + // that has a lower bit-rate (as it will be faster to download). + if ( isset( $a['bandwidth'] ) && isset( $b['bandwidth'] ) ) { + return ( $a['bandwidth'] < $b['bandwidth'] ) ? -1 : 1; + } + + // We have no firm basis for a comparison, so consider them equal. + return 0; + } + + /** + * Call mediaWiki xml helper class to build media tag output from + * supplied arrays + * @param $sizeOverride array + * @param $autoPlay boolean sets the autoplay attribute + * @return string + */ + function getHtmlMediaTagOutput( $sizeOverride = array(), $autoPlay = false ){ + // Try to get the first source src attribute ( usually this should be the source file ) + $mediaSources = $this->getMediaSources(); + reset( $mediaSources ); // do not rely on auto-resetting of arrays under HHVM + $firstSource = current( $mediaSources ); + + if( !$firstSource['src'] ){ + // XXX media handlers don't seem to work with exceptions.. + return 'Error missing media source'; + }; + + // Sort sources by bandwidth least to greatest ( so default selection on resource constrained + // browsers ( without js? ) go with minimal source. + usort( $mediaSources, array( $this, 'sortMediaByBandwidth' ) ); + + // We prefix some source attributes with data- to pass along to the javascript player + $prefixedSourceAttr = Array( 'width', 'height', 'title', 'shorttitle', 'bandwidth', 'framerate', 'disablecontrols' ); + foreach( $mediaSources as &$source ){ + foreach( $source as $attr => $val ){ + if( in_array( $attr, $prefixedSourceAttr ) ){ + $source[ 'data-' . $attr ] = $val; + unset( $source[ $attr ] ); + } + } + } + + $width = $sizeOverride ? $sizeOverride[0] : $this->getPlayerWidth(); + if( $this->fillwindow ){ + $width = '100%'; + } else { + $width .= 'px'; + } + // Build the video tag output: + $s = Xml::tags( 'div' , array( + 'class' => 'mediaContainer', + 'style' => 'position:relative;display:block;width:'. $width + ), + Html::rawElement( $this->getTagName(), $this->getMediaAttr( $sizeOverride, $autoPlay ), + // The set of media sources: + self::htmlTagSet( 'source', $mediaSources ) . + + // Timed text: + self::htmlTagSet( 'track', + $this->file ? $this->getTextHandler()->getTracks() : null ) . + + // Fallback text displayed for browsers without js and without video tag support: + /// XXX note we may want to replace this with an image and download link play button + wfMessage( 'timedmedia-no-player-js', $firstSource['src'] )->text() + ) + ); + return $s; + } + + /** + * Get poster. + * @param $width Integer width of poster. Should not equal $this->width. + * @throws Exception If $width is same as $this->width. + * @return String|bool url for poster or false + */ + function getPoster ( $width ) { + if ( intval( $width ) === intval( $this->width ) ) { + // Prevent potential loop + throw new Exception( "Asked for poster in current size. Potential loop." ); + } + $params = array( "width" => intval( $width ) ); + $mto = $this->file->transform( $params ); + if ( $mto ) { + return $mto->getUrl(); + } else { + return false; + } + } + + /** + * Get the media attributes + * @param $sizeOverride Array|bool of width and height + * @return array + */ + function getMediaAttr( $sizeOverride = false, $autoPlay = false ){ + global $wgVideoPlayerSkin ; + // Normalize values + $length = floatval( $this->length ); + $offset = floatval( $this->offset ); + + $width = $sizeOverride ? $sizeOverride[0] : $this->getPlayerWidth(); + $height = $sizeOverride ? $sizeOverride[1]: $this->getPlayerHeight(); + + // The poster url: + $posterUrl = $this->getUrl( $sizeOverride ); + + if( $this->fillwindow ){ + $width = '100%'; + $height = '100%'; + } else{ + $width .= 'px'; + $height .= 'px'; + } + + $mediaAttr = array( + 'id' => self::PLAYER_ID_PREFIX . TimedMediaTransformOutput::$serial++, + 'style' => "width:{$width}", + // Get the correct size: + 'poster' => $posterUrl, + + // Note we set controls to true ( for no-js players ) when mwEmbed rewrites the interface + // it updates the controls attribute of the embed video + 'controls'=> 'true', + // Since we will reload the item with javascript, + // tell browser to not load the video before + 'preload'=>'none', + ); + + if ( $this->isVideo ) { + $mediaAttr['style'] .= ";height:{$height}"; + } + + if( $autoPlay === true ){ + $mediaAttr['autoplay'] = 'true'; + } + + // MediaWiki uses the kSkin class + $mediaAttr['class'] = 'kskin'; + + if ( $this->file ) { + // Custom data-attributes + $mediaAttr += array( + 'data-durationhint' => $length, + 'data-startoffset' => $offset, + 'data-mwtitle' => $this->file->getTitle()->getDBkey() + ); + + // Add api provider: + if( $this->file->isLocal() ){ + $apiProviderName = 'local'; + } else { + // Set the api provider name to "wikimediacommons" for shared ( instant commons convention ) + // (provider names should have identified the provider instead of the provider type "shared") + $apiProviderName = $this->file->getRepoName(); + if( $apiProviderName == 'shared' ) { + $apiProviderName = 'wikimediacommons'; + } + } + // XXX Note: will probably migrate mwprovider to an escaped api url. + $mediaAttr[ 'data-mwprovider' ] = $apiProviderName; + } else { + if ( $length ) { + $mediaAttr[ 'data-durationhint' ] = $length; + } + if ( $offset ) { + $mediaAttr[ 'data-startoffset' ] = $offset; + } + } + if ( $this->disablecontrols ) { + $mediaAttr[ 'data-disablecontrols' ] = $this->disablecontrols; + } + return $mediaAttr; + } + + /** + * @return null + */ + function getMediaSources(){ + if( !$this->sources ){ + // Generate transcode jobs ( and get sources that are already transcoded) + // At a minimum this should return the source video file. + $this->sources = WebVideoTranscode::getSources( $this->file ); + // Check if we have "start or end" times and append the temporal url fragment hash + foreach( $this->sources as &$source ){ + $source['src'].= $this->getTemporalUrlHash(); + } + } + return $this->sources; + } + + function getTemporalUrlHash(){ + if( $this->hashTime ){ + return $this->hashTime; + } + $hash =''; + if( $this->start ){ + $startSec = TimedMediaHandler::parseTimeString( $this->start ); + if( $startSec !== false ){ + $hash.= '#t=' . TimedMediaHandler::seconds2npt( $startSec ); + } + } + if( $this->end ){ + if( $hash == '' ){ + $hash .= '#t=0'; + } + $endSec = TimedMediaHandler::parseTimeString( $this->end ); + if( $endSec !== false ){ + $hash.= ',' . TimedMediaHandler::seconds2npt( $endSec ); + } + } + $this->hashTime = $hash; + return $this->hashTime; + } +} |