Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 96 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
TimedMediaThumbnail | |
0.00% |
0 / 96 |
|
0.00% |
0 / 4 |
930 | |
0.00% |
0 / 1 |
get | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
tryFfmpegThumb | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
110 | |||
resizeThumb | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
90 | |||
getThumbTime | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace MediaWiki\TimedMediaHandler; |
4 | |
5 | use File; |
6 | use MediaTransformError; |
7 | use MediaWiki\MainConfigNames; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
10 | use UnregisteredLocalFile; |
11 | |
12 | class TimedMediaThumbnail { |
13 | |
14 | /** |
15 | * @param array $options |
16 | * @return bool|MediaTransformError |
17 | */ |
18 | public static function get( $options ) { |
19 | if ( !is_dir( dirname( $options['dstPath'] ) ) ) { |
20 | wfMkdirParents( dirname( $options['dstPath'] ), null, __METHOD__ ); |
21 | } |
22 | |
23 | wfDebug( "Creating video thumbnail at " . $options['dstPath'] . "\n" ); |
24 | if ( |
25 | isset( $options['width'] ) && isset( $options['height'] ) && |
26 | $options['width'] != $options['file']->getWidth() && |
27 | $options['height'] != $options['file']->getHeight() |
28 | ) { |
29 | return self::resizeThumb( $options ); |
30 | } |
31 | return self::tryFfmpegThumb( $options ); |
32 | } |
33 | |
34 | /** |
35 | * @param array $options |
36 | * @return bool|MediaTransformError |
37 | */ |
38 | private static function tryFfmpegThumb( $options ) { |
39 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
40 | $fFmpegLocation = $config->get( 'FFmpegLocation' ); |
41 | $maxShellMemory = $config->get( MainConfigNames::MaxShellMemory ); |
42 | |
43 | if ( !$fFmpegLocation || !is_file( $fFmpegLocation ) ) { |
44 | return false; |
45 | } |
46 | |
47 | $cmd = wfEscapeShellArg( $fFmpegLocation ) . ' -nostdin -threads 1 '; |
48 | |
49 | $file = $options['file']; |
50 | $handler = $file->getHandler(); |
51 | |
52 | $offset = (int)self::getThumbTime( $options ); |
53 | /* |
54 | This is a workaround until ffmpegs ogg demuxer properly seeks to keyframes. |
55 | Seek N seconds before offset and seek in decoded stream after that. |
56 | -ss before input seeks without decode |
57 | -ss after input seeks in decoded stream |
58 | |
59 | N depends on framerate of input, keyframe interval defaults |
60 | to 64 for most encoders, seeking a bit before that |
61 | */ |
62 | |
63 | $framerate = $handler->getFramerate( $file ); |
64 | if ( $framerate > 0 ) { |
65 | $seekoffset = 1 + (int)( 64 / $framerate ); |
66 | } else { |
67 | $seekoffset = 3; |
68 | } |
69 | |
70 | if ( $offset > $seekoffset ) { |
71 | $cmd .= ' -ss ' . (float)( $offset - $seekoffset ); |
72 | $offset = $seekoffset; |
73 | } |
74 | |
75 | // try to get temporary local url to file |
76 | $backend = $file->getRepo()->getBackend(); |
77 | |
78 | $src = $backend->getFileHttpUrl( [ |
79 | 'src' => $file->getPath() |
80 | ] ); |
81 | if ( $src === null ) { |
82 | $src = $file->getLocalRefPath(); |
83 | } |
84 | |
85 | $cmd .= ' -y -i ' . wfEscapeShellArg( $src ); |
86 | $cmd .= ' -ss ' . $offset . ' '; |
87 | |
88 | // Deinterlace MPEG-2 if necessary |
89 | if ( $handler->isInterlaced( $file ) ) { |
90 | // Send one frame only |
91 | $cmd .= ' -vf yadif=mode=0'; |
92 | } |
93 | |
94 | // Set the output size if set in options: |
95 | if ( isset( $options['width'] ) && isset( $options['height'] ) ) { |
96 | $cmd .= ' -s ' . (int)$options['width'] . 'x' . (int)$options['height']; |
97 | } |
98 | |
99 | // MJPEG, that's the same as JPEG except it's supported by the windows build of ffmpeg |
100 | // No audio, one frame |
101 | $cmd .= ' -f mjpeg -an -vframes 1 ' . |
102 | wfEscapeShellArg( $options['dstPath'] ) . ' 2>&1'; |
103 | |
104 | $retval = 0; |
105 | $returnText = wfShellExec( $cmd, $retval ); |
106 | // Check if it was successful |
107 | if ( !$options['file']->getHandler()->removeBadFile( $options['dstPath'], $retval ) ) { |
108 | return true; |
109 | } |
110 | $returnText = $cmd . "\nwgMaxShellMemory: $maxShellMemory\n" . $returnText; |
111 | // Return error box |
112 | return new MediaTransformError( |
113 | 'thumbnail_error', $options['width'], $options['height'], $returnText |
114 | ); |
115 | } |
116 | |
117 | /** |
118 | * @param array $options |
119 | * @return bool|MediaTransformError |
120 | */ |
121 | private static function resizeThumb( $options ) { |
122 | $file = $options['file']; |
123 | $params = []; |
124 | foreach ( [ 'start', 'thumbtime' ] as $key ) { |
125 | if ( isset( $options[ $key ] ) ) { |
126 | $params[ $key ] = $options[ $key ]; |
127 | } |
128 | } |
129 | $params["width"] = $file->getWidth(); |
130 | $params["height"] = $file->getHeight(); |
131 | |
132 | $poolKey = $file->getRepo()->getSharedCacheKey( 'file', md5( $file->getName() ) ); |
133 | $posOptions = array_flip( [ 'start', 'thumbtime' ] ); |
134 | $poolKey = wfAppendQuery( $poolKey, array_intersect_key( $options, $posOptions ) ); |
135 | |
136 | $work = new PoolCounterWorkViaCallback( 'TMHTransformFrame', |
137 | '_tmh:frame:' . $poolKey, |
138 | [ 'doWork' => static function () use ( $file, $params ) { |
139 | return $file->transform( $params, File::RENDER_NOW ); |
140 | } ] ); |
141 | $thumb = $work->execute(); |
142 | |
143 | if ( !$thumb || $thumb->isError() ) { |
144 | return $thumb; |
145 | } |
146 | $src = $thumb->getStoragePath(); |
147 | if ( !$src ) { |
148 | return false; |
149 | } |
150 | $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); |
151 | $thumbFile = new UnregisteredLocalFile( $file->getTitle(), |
152 | $localRepo, $src, false ); |
153 | $thumbParams = [ |
154 | "width" => $options['width'], |
155 | "height" => $options['height'] |
156 | ]; |
157 | $handler = $thumbFile->getHandler(); |
158 | if ( !$handler ) { |
159 | return false; |
160 | } |
161 | $scaledThumb = $handler->doTransform( |
162 | $thumbFile, |
163 | $options['dstPath'], |
164 | $options['dstPath'], |
165 | $thumbParams |
166 | ); |
167 | |
168 | if ( !$scaledThumb || $scaledThumb->isError() ) { |
169 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable |
170 | return $scaledThumb; |
171 | } |
172 | return true; |
173 | } |
174 | |
175 | /** |
176 | * @param array $options |
177 | * @return bool|float|int |
178 | */ |
179 | private static function getThumbTime( $options ) { |
180 | $length = $options['file']->getLength(); |
181 | |
182 | // If start time param isset use that for the thumb: |
183 | if ( isset( $options['start'] ) ) { |
184 | $thumbtime = TimedMediaHandler::parseTimeString( $options['start'], $length ); |
185 | if ( $thumbtime !== false ) { |
186 | return $thumbtime; |
187 | } |
188 | } |
189 | // else use thumbtime |
190 | if ( isset( $options['thumbtime'] ) ) { |
191 | $thumbtime = TimedMediaHandler::parseTimeString( $options['thumbtime'], $length ); |
192 | if ( $thumbtime !== false ) { |
193 | return $thumbtime; |
194 | } |
195 | } |
196 | // Seek to midpoint by default, it tends to be more interesting than the start |
197 | return $length / 2; |
198 | } |
199 | } |