Skip to content

Commit 34a197e

Browse files
Call ffmpeg using a 2pass encoding, to get the best quality and a guaranteed max file size
1 parent 2854bb4 commit 34a197e

File tree

3 files changed

+60
-58
lines changed

3 files changed

+60
-58
lines changed

src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FILE_SIZE;
1111
import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FRAMES;
1212
import static com.github.stickerifier.stickerify.media.MediaConstraints.VP9_CODEC;
13+
import static com.github.stickerifier.stickerify.process.ProcessHelper.IS_WINDOWS;
1314
import static java.nio.charset.StandardCharsets.UTF_8;
1415

1516
import com.github.stickerifier.stickerify.exception.CorruptedVideoException;
@@ -38,7 +39,9 @@
3839
import java.io.FileInputStream;
3940
import java.io.IOException;
4041
import java.nio.file.Files;
42+
import java.util.Arrays;
4143
import java.util.List;
44+
import java.util.stream.Stream;
4245
import java.util.zip.GZIPInputStream;
4346

4447
public 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
}

src/main/java/com/github/stickerifier/stickerify/process/ProcessHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
public final class ProcessHelper {
1313

14-
static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");
14+
public static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");
1515
private static final int MAX_CONCURRENT_PROCESSES = IS_WINDOWS ? 4 : 5;
1616
private static final Semaphore SEMAPHORE = new Semaphore(MAX_CONCURRENT_PROCESSES);
1717

src/test/java/com/github/stickerifier/stickerify/media/MediaHelperTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ void resizeSmallWebmVideo() throws Exception {
188188
var webmVideo = loadResource("small_video_sticker.webm");
189189
var result = MediaHelper.convert(webmVideo);
190190

191-
assertVideoConsistency(result, 512, 212, 30F, 2_000L);
191+
assertVideoConsistency(result, 512, 212, 30F, 2_600L);
192192
}
193193

194194
@Test

0 commit comments

Comments
 (0)