1010import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_VIDEO_FILE_SIZE ;
1111import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_VIDEO_FRAMES ;
1212import static com .github .stickerifier .stickerify .media .MediaConstraints .VP9_CODEC ;
13+ import static com .github .stickerifier .stickerify .process .ProcessHelper .IS_WINDOWS ;
1314import static java .nio .charset .StandardCharsets .UTF_8 ;
1415
1516import com .github .stickerifier .stickerify .exception .CorruptedVideoException ;
3839import java .io .FileInputStream ;
3940import java .io .IOException ;
4041import java .nio .file .Files ;
42+ import java .util .Arrays ;
4143import java .util .List ;
44+ import java .util .stream .Stream ;
4245import java .util .zip .GZIPInputStream ;
4346
4447public final class MediaHelper {
@@ -52,7 +55,6 @@ public final class MediaHelper {
5255
5356 private static final Gson GSON = new Gson ();
5457 static final ProcessLocator FFMPEG_LOCATOR = new PathLocator ();
55- private static final int PRESERVE_ASPECT_RATIO = -2 ;
5658 private static final List <String > SUPPORTED_VIDEOS = List .of ("image/gif" , "video/quicktime" , "video/webm" ,
5759 "video/mp4" , "video/x-m4v" , "application/x-matroska" , "video/x-msvideo" );
5860
@@ -333,42 +335,25 @@ private static void deleteFile(File file) throws FileOperationException {
333335 * @throws MediaException if file conversion is not successful
334336 */
335337 private static File convertToWebm (File file ) throws MediaException {
336- var mediaInfo = retrieveMultimediaInfo (file );
337-
338- if (isVideoCompliant (file , mediaInfo )) {
338+ if (isVideoCompliant (file )) {
339339 LOGGER .atInfo ().log ("The video doesn't need conversion" );
340340
341341 return null ;
342342 }
343343
344- return convertWithFfmpeg (file , mediaInfo );
345- }
346-
347- /**
348- * Convenience method to retrieve multimedia information of a file.
349- *
350- * @param file the video to check
351- * @return passed-in video's multimedia information
352- * @throws CorruptedVideoException if an error occurred retrieving video information
353- */
354- private static MultimediaInfo retrieveMultimediaInfo (File file ) throws CorruptedVideoException {
355- try {
356- return new MultimediaObject (file , FFMPEG_LOCATOR ).getInfo ();
357- } catch (EncoderException e ) {
358- throw new CorruptedVideoException ("The video could not be processed successfully" , e );
359- }
344+ return convertWithFfmpeg (file );
360345 }
361346
362347 /**
363348 * Checks if passed-in file is already compliant with Telegram's requisites.
364349 * If so, conversion won't take place and no file will be returned to the user.
365350 *
366351 * @param file the file to check
367- * @param mediaInfo video's multimedia information
368352 * @return {@code true} if the file is compliant
369353 * @throws FileOperationException if an error occurred retrieving the size of the file
370354 */
371- private static boolean isVideoCompliant (File file , MultimediaInfo mediaInfo ) throws FileOperationException {
355+ private static boolean isVideoCompliant (File file ) throws FileOperationException , CorruptedVideoException {
356+ var mediaInfo = retrieveMultimediaInfo (file );
372357 var videoInfo = mediaInfo .getVideo ();
373358 var videoSize = videoInfo .getSize ();
374359
@@ -382,63 +367,80 @@ private static boolean isVideoCompliant(File file, MultimediaInfo mediaInfo) thr
382367 && isFileSizeLowerThan (file , MAX_VIDEO_FILE_SIZE );
383368 }
384369
370+ /**
371+ * Convenience method to retrieve multimedia information of a file.
372+ *
373+ * @param file the video to check
374+ * @return passed-in video's multimedia information
375+ * @throws CorruptedVideoException if an error occurred retrieving video information
376+ */
377+ private static MultimediaInfo retrieveMultimediaInfo (File file ) throws CorruptedVideoException {
378+ try {
379+ return new MultimediaObject (file , FFMPEG_LOCATOR ).getInfo ();
380+ } catch (EncoderException e ) {
381+ throw new CorruptedVideoException ("The video could not be processed successfully" , e );
382+ }
383+ }
384+
385385 /**
386386 * Converts the passed-in file using FFmpeg applying Telegram's video stickers' constraints.
387387 *
388388 * @param file the file to convert
389- * @param mediaInfo video's multimedia information
390389 * @return converted video
391390 * @throws MediaException if file conversion is not successful
392391 */
393- private static File convertWithFfmpeg (File file , MultimediaInfo mediaInfo ) throws MediaException {
394- var webmVideo = createTempFile ("webm" );
395- var videoDetails = getResultingVideoDetails (mediaInfo );
396-
397- var ffmpegCommand = new String [] {
392+ private static File convertWithFfmpeg (File file ) throws MediaException {
393+ var logFile = createTempFile ("log" );
394+ var baseFfmpegCommand = new String [] {
398395 "ffmpeg" ,
396+ "-y" ,
399397 "-v" , "error" ,
400398 "-i" , file .getAbsolutePath (),
401- "-vf" , "scale=" + videoDetails . width () + ":" + videoDetails . height () + ", fps=" + videoDetails . frameRate () ,
399+ "-vf" , "scale='if(gt(iw,ih),512,-2)':'if(gt(iw,ih),-2,512)', fps=min(30 \\ ,source_fps)" ,
402400 "-c:v" , "libvpx-" + VP9_CODEC ,
403- "-b:v" , "256k " ,
404- "-crf " , "32 " ,
405- "-g " , "60 " ,
401+ "-b:v" , "650K " ,
402+ "-pix_fmt " , "yuv420p " ,
403+ "-t " , "3 " ,
406404 "-an" ,
407- "-t" , videoDetails .duration (),
408- "-y" , webmVideo .getAbsolutePath ()
405+ "-passlogfile" , logFile .getAbsolutePath ()
406+ };
407+
408+ var pass1Options = new String [] {
409+ "-pass" , "1" ,
410+ "-f" , "webm" ,
411+ IS_WINDOWS ? "NUL" : "/dev/null"
409412 };
410413
411414 try {
412- ProcessHelper .executeCommand (ffmpegCommand );
415+ var command = Stream .concat (Arrays .stream (baseFfmpegCommand ), Arrays .stream (pass1Options ))
416+ .toArray (String []::new );
417+ ProcessHelper .executeCommand (command );
413418 } catch (ProcessException e ) {
414- deleteFile (webmVideo );
419+ deleteFile (logFile );
415420 throw new MediaException (e .getMessage ());
416421 }
417422
418- return webmVideo ;
419- }
420-
421- /**
422- * Convenience method to group resulting video's details,
423- * calculated checking passed-in media info against Telegram's constraints.
424- *
425- * @param mediaInfo video's multimedia information
426- * @return resulting video's details
427- */
428- private static ResultingVideoDetails getResultingVideoDetails (MultimediaInfo mediaInfo ) {
429- var videoInfo = mediaInfo .getVideo ();
430- float frameRate = Math .min (videoInfo .getFrameRate (), MAX_VIDEO_FRAMES );
431- long duration = Math .min (mediaInfo .getDuration (), MAX_VIDEO_DURATION_MILLIS ) / 1_000L ;
423+ var webmVideo = createTempFile ("webm" );
424+ var pass2Options = new String [] {
425+ "-pass" , "2" ,
426+ "-movflags" , "+faststart" ,
427+ webmVideo .getAbsolutePath ()
428+ };
432429
433- boolean isWidthBigger = videoInfo .getSize ().getWidth () >= videoInfo .getSize ().getHeight ();
434- int width = isWidthBigger ? MAX_SIDE_LENGTH : PRESERVE_ASPECT_RATIO ;
435- int height = isWidthBigger ? PRESERVE_ASPECT_RATIO : MAX_SIDE_LENGTH ;
430+ try {
431+ var command = Stream .concat (Arrays .stream (baseFfmpegCommand ), Arrays .stream (pass2Options ))
432+ .toArray (String []::new );
433+ ProcessHelper .executeCommand (command );
434+ } catch (ProcessException e ) {
435+ deleteFile (webmVideo );
436+ throw new MediaException (e .getMessage ());
437+ } finally {
438+ deleteFile (logFile );
439+ }
436440
437- return new ResultingVideoDetails ( width , height , frameRate , String . valueOf ( duration )) ;
441+ return webmVideo ;
438442 }
439443
440- private record ResultingVideoDetails (int width , int height , float frameRate , String duration ) {}
441-
442444 private MediaHelper () {
443445 throw new UnsupportedOperationException ();
444446 }
0 commit comments