summaryrefslogtreecommitdiff
path: root/extensions/TimedMediaHandler/TimedMediaThumbnail.php
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TimedMediaHandler/TimedMediaThumbnail.php')
-rw-r--r--extensions/TimedMediaHandler/TimedMediaThumbnail.php240
1 files changed, 240 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/TimedMediaThumbnail.php b/extensions/TimedMediaHandler/TimedMediaThumbnail.php
new file mode 100644
index 00000000..77757440
--- /dev/null
+++ b/extensions/TimedMediaHandler/TimedMediaThumbnail.php
@@ -0,0 +1,240 @@
+<?php
+class TimedMediaThumbnail {
+
+ /**
+ * @param $options array()
+ * @return bool|MediaTransformError
+ */
+ static function get( $options ){
+ if( !is_dir( dirname( $options['dstPath'] ) ) ){
+ wfMkdirParents( dirname( $options['dstPath'] ), null, __METHOD__ );
+ }
+
+ wfDebug( "Creating video thumbnail at " . $options['dstPath'] . "\n" );
+ if(
+ isset( $options['width'] ) && isset( $options['height'] ) &&
+ $options['width'] != $options['file']->getWidth() &&
+ $options['height'] != $options['file']->getHeight()
+ ){
+ return self::resizeThumb( $options );
+ }
+ // try OggThumb, and fallback to ffmpeg
+ $result = self::tryOggThumb( $options );
+ if ( $result === false ) {
+ return self::tryFfmpegThumb( $options );
+ }
+ return $result;
+ }
+
+ /**
+ * Run oggThumb to generate a still image from a video file, using a frame
+ * close to the given number of seconds from the start.
+ *
+ * @param $options array
+ * @return bool|MediaTransformError
+ *
+ */
+ static function tryOggThumb( $options ) {
+ global $wgOggThumbLocation;
+
+ // Check that the file is 'ogg' format
+ if( $options['file']->getHandler()->getMetadataType( $options['file'] ) != 'ogg' ){
+ return false;
+ }
+
+ // Check for $wgOggThumbLocation
+ if( !$wgOggThumbLocation || !is_file( $wgOggThumbLocation ) ){
+ return false;
+ }
+
+ $time = self::getThumbTime( $options );
+ $dstPath = $options['dstPath'];
+ $videoPath = $options['file']->getLocalRefPath();
+
+ $cmd = wfEscapeShellArg( $wgOggThumbLocation )
+ . ' -t ' . floatval( $time );
+ // Set the output size if set in options:
+ if( isset( $options['width'] ) && isset( $options['height'] ) ){
+ $cmd.= ' -s '. intval( $options['width'] ) . 'x' . intval( $options['height'] );
+ }
+ $cmd .= ' -n ' . wfEscapeShellArg( $dstPath ) .
+ ' ' . wfEscapeShellArg( $videoPath ) . ' 2>&1';
+ $retval = 0;
+ $returnText = wfShellExec( $cmd, $retval );
+
+ if ( $options['file']->getHandler()->removeBadFile( $dstPath, $retval ) || $retval ) {
+ // oggThumb spams both stderr and stdout with useless progress
+ // messages, and then often forgets to output anything when
+ // something actually does go wrong. So interpreting its output is
+ // a challenge.
+ $lines = explode( "\n", str_replace( "\r\n", "\n", $returnText ) );
+ if ( count( $lines ) > 0
+ && preg_match( '/invalid option -- \'n\'$/', $lines[0] ) )
+ {
+ $returnText = wfMessage( 'timedmedia-oggThumb-version', '0.9' )->inContentLanguage()->text();
+ } else {
+ $returnText = wfMessage( 'timedmedia-oggThumb-failed' )->inContentLanguage()->text();
+ }
+ return new MediaTransformError( 'thumbnail_error',
+ $options['width'], $options['height'], $returnText );
+ }
+ return true;
+ }
+
+ /**
+ * @param $options array
+ * @return bool|MediaTransformError
+ */
+ static function tryFfmpegThumb( $options ){
+ global $wgFFmpegLocation, $wgMaxShellMemory;
+
+ if( !$wgFFmpegLocation || !is_file( $wgFFmpegLocation ) ){
+ return false;
+ }
+
+ $cmd = wfEscapeShellArg( $wgFFmpegLocation ) . ' -threads 1 ';
+
+ $offset = intval( 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'] );
+ if ( $framerate > 0 ) {
+ $seekoffset = 1 + intval( 64 / $framerate );
+ } else {
+ $seekoffset = 3;
+ }
+
+ if($offset > $seekoffset) {
+ $cmd .= ' -ss ' . floatval($offset - $seekoffset);
+ $offset = $seekoffset;
+ }
+
+ //try to get temorary local url to file
+ $backend = $options['file']->getRepo()->getBackend();
+ // getFileHttpUrl was only added in mw 1.21, dont fail if it does not exist
+ if ( method_exists( $backend, 'getFileHttpUrl' ) ) {
+ $src = $backend->getFileHttpUrl( array(
+ 'src' => $options['file']->getPath()
+ ) );
+ } else {
+ $src = null;
+ }
+ if ( $src == null ) {
+ $src = $options['file']->getLocalRefPath();
+ }
+
+ $cmd .= ' -y -i ' . wfEscapeShellArg( $src );
+ $cmd .= ' -ss ' . $offset . ' ';
+
+ // Set the output size if set in options:
+ if( isset( $options['width'] ) && isset( $options['height'] ) ){
+ $cmd.= ' -s '. intval( $options['width'] ) . 'x' . intval( $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 $options array
+ * @return bool|MediaTransformError
+ */
+ static function resizeThumb( $options ) {
+ $file = $options['file'];
+ $params = array();
+ foreach( array( '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( array( 'start', 'thumbtime' ) );
+ $poolKey = wfAppendQuery( $poolKey, array_intersect_key( $options, $posOptions ) );
+
+ if ( class_exists( 'PoolCounterWorkViaCallback' ) ) {
+ $work = new PoolCounterWorkViaCallback( 'TMHTransformFrame',
+ '_tmh:frame:' . $poolKey,
+ array( 'doWork' => function() use ($file, $params) {
+ return $file->transform( $params, File::RENDER_NOW );
+ } ) );
+ $thumb = $work->execute();
+ } else {
+ $thumb = $file->transform( $params, File::RENDER_NOW );
+ }
+
+ if ( !$thumb || $thumb->isError() ) {
+ return $thumb;
+ }
+ $src = $thumb->getStoragePath();
+ if ( !$src ) {
+ return false;
+ }
+ $thumbFile = new UnregisteredLocalFile( $file->getTitle(),
+ RepoGroup::singleton()->getLocalRepo(), $src, false );
+ $thumbParams = array(
+ "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() ) {
+ return $scaledThumb;
+ }
+ return true;
+ }
+
+ /**
+ * @param $options array
+ * @return bool|float|int
+ */
+ 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;
+ }
+}