Skip to content

Commit 617fc0a

Browse files
Go back to previous bitrate, and add a second try if converted file is too big
1 parent c0d57e0 commit 617fc0a

4 files changed

Lines changed: 78 additions & 37 deletions

File tree

Railway.dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ FROM rob93c/stickerify:$DOCKER_TAG
33
ARG STICKERIFY_TOKEN
44
ARG LOG_LEVEL
55
ARG CONCURRENT_PROCESSES
6-
ARG VIDEO_BITRATE_LIMIT
76
ENV STICKERIFY_TOKEN=$STICKERIFY_TOKEN \
87
LOG_LEVEL=$LOG_LEVEL \
9-
CONCURRENT_PROCESSES=$CONCURRENT_PROCESSES \
10-
VIDEO_BITRATE_LIMIT=$VIDEO_BITRATE_LIMIT
8+
CONCURRENT_PROCESSES=$CONCURRENT_PROCESSES

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

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.io.FileInputStream;
3434
import java.io.IOException;
3535
import java.nio.file.Files;
36+
import java.util.ArrayList;
3637
import java.util.List;
3738
import java.util.Set;
3839
import java.util.zip.GZIPInputStream;
@@ -56,8 +57,6 @@ public final class MediaHelper {
5657
private static final int WEBP_ANIMATION_BIT_MASK = 0x02;
5758
private static final String WEBP_EXTENDED_FILE_FORMAT = "VP8X";
5859

59-
private static final String VIDEO_BITRATE_LIMIT = getVideoBitrateLimit();
60-
6160
/**
6261
* Based on the type of passed-in file, it converts it into the proper media.
6362
* If no conversion was needed, {@code null} is returned.
@@ -185,15 +184,15 @@ private static boolean isVideoCompliant(File file) throws MediaException, Interr
185184
* @throws InterruptedException if the current thread is interrupted while retrieving file info
186185
*/
187186
static MultimediaInfo retrieveMultimediaInfo(File file) throws MediaException, InterruptedException {
188-
var command = new String[] {
187+
var command = List.of(
189188
"ffprobe",
190189
"-hide_banner",
191190
"-v", "error",
192191
"-print_format", "json",
193192
"-show_format",
194193
"-show_streams",
195194
file.getAbsolutePath()
196-
};
195+
);
197196

198197
try {
199198
var output = ProcessHelper.executeCommand(command);
@@ -388,7 +387,7 @@ && isSizeCompliant(imageInfo.width(), imageInfo.height())
388387
*/
389388
private static File convertToWebp(File file) throws MediaException, InterruptedException {
390389
var webpImage = createTempFile("webp");
391-
var command = new String[] {
390+
var command = List.of(
392391
"ffmpeg",
393392
"-y",
394393
"-hide_banner",
@@ -399,7 +398,7 @@ private static File convertToWebp(File file) throws MediaException, InterruptedE
399398
"-lossless", "1",
400399
"-compression_level", "6",
401400
webpImage.getAbsolutePath()
402-
};
401+
);
403402

404403
try {
405404
ProcessHelper.executeCommand(command);
@@ -468,9 +467,47 @@ private static void deleteFile(File file) throws FileOperationException {
468467
* @throws InterruptedException if the current thread is interrupted while converting the video file
469468
*/
470469
private static File convertToWebm(File file) throws MediaException, InterruptedException {
470+
var optimisticBitrate = List.of(
471+
"-b:v", "650K",
472+
"-maxrate", "650K",
473+
"-bufsize", "1300K"
474+
);
475+
var fallbackBitrate = List.of(
476+
"-b:v", "250K",
477+
"-maxrate", "250K",
478+
"-bufsize", "125K",
479+
"-qmin", "25"
480+
);
481+
482+
var webmVideo = convertVideoWithBitrate(file, optimisticBitrate);
483+
if (webmVideo.exists() && webmVideo.length() <= MAX_VIDEO_FILE_SIZE) {
484+
return webmVideo;
485+
}
486+
487+
LOGGER.at(Level.WARN).log("Resulting file was too large (actual size was {} bytes), retrying with lower bitrate", webmVideo.length());
488+
try {
489+
deleteFile(webmVideo);
490+
} catch (FileOperationException e) {
491+
LOGGER.at(Level.WARN).setCause(e).log("Could not delete file");
492+
}
493+
494+
return convertVideoWithBitrate(file, fallbackBitrate);
495+
}
496+
497+
/**
498+
* Given a video file and a set of bitrate rules, it converts it to a WebM file of the proper dimension (max 512 x 512),
499+
* based on the requirements specified by <a href="https://core.telegram.org/stickers/webm-vp9-encoding">Telegram documentation</a>.
500+
*
501+
* @param file the file to convert
502+
* @param bitrateCommands the list of bitrate commands to add to the conversion
503+
* @return converted video
504+
* @throws MediaException if file conversion is not successful
505+
* @throws InterruptedException if the current thread is interrupted while converting the video file
506+
*/
507+
private static File convertVideoWithBitrate(File file, List<String> bitrateCommands) throws MediaException, InterruptedException {
471508
var webmVideo = createTempFile("webm");
472509
var logPrefix = webmVideo.getAbsolutePath() + "-passlog";
473-
var baseCommand = new String[] {
510+
var baseCommand = List.of(
474511
"ffmpeg",
475512
"-y",
476513
"-hide_banner",
@@ -480,10 +517,6 @@ private static File convertToWebm(File file) throws MediaException, InterruptedE
480517
"-c:v", "libvpx-" + VP9_CODEC,
481518
"-row-mt", "1",
482519
"-threads", "2",
483-
"-b:v", VIDEO_BITRATE_LIMIT,
484-
"-maxrate", VIDEO_BITRATE_LIMIT,
485-
"-bufsize", VIDEO_BITRATE_LIMIT,
486-
"-qmin", "25",
487520
"-qmax", "63",
488521
"-g", "120",
489522
"-auto-alt-ref", "0",
@@ -492,11 +525,22 @@ private static File convertToWebm(File file) throws MediaException, InterruptedE
492525
"-an",
493526
"-enc_time_base", "1/1000",
494527
"-passlogfile", logPrefix
495-
};
528+
);
529+
var firstPass = List.of(
530+
"-cpu-used", "8",
531+
"-pass", "1",
532+
"-f", "webm",
533+
OsConstants.NULL_FILE
534+
);
535+
var secondPass = List.of(
536+
"-cpu-used", "4",
537+
"-pass", "2",
538+
webmVideo.getAbsolutePath()
539+
);
496540

497541
try {
498-
ProcessHelper.executeCommand(buildFfmpegCommand(baseCommand, "-cpu-used", "8", "-pass", "1", "-f", "webm", OsConstants.NULL_FILE));
499-
ProcessHelper.executeCommand(buildFfmpegCommand(baseCommand, "-cpu-used", "4", "-pass", "2", webmVideo.getAbsolutePath()));
542+
ProcessHelper.executeCommand(buildFfmpegCommand(baseCommand, bitrateCommands, firstPass));
543+
ProcessHelper.executeCommand(buildFfmpegCommand(baseCommand, bitrateCommands, secondPass));
500544
} catch (ProcessException e) {
501545
try {
502546
deleteFile(webmVideo);
@@ -516,24 +560,19 @@ private static File convertToWebm(File file) throws MediaException, InterruptedE
516560
}
517561

518562
/**
519-
* Builds the ffmpeg command combining a base part with a specific part (useful for 2 pass processing)
563+
* Builds the ffmpeg command combining multiple parts (useful for 2 pass processing)
520564
*
521-
* @param baseCommand the common ffmpeg command
522-
* @param additionalOptions command specific options
565+
* @param commands a series of list containing commands
523566
* @return the complete ffmpeg invocation command
524567
*/
525-
private static String[] buildFfmpegCommand(String[] baseCommand, String... additionalOptions) {
526-
var commands = new String[baseCommand.length + additionalOptions.length];
527-
System.arraycopy(baseCommand, 0, commands, 0, baseCommand.length);
528-
System.arraycopy(additionalOptions, 0, commands, baseCommand.length, additionalOptions.length);
529-
530-
return commands;
531-
}
532-
533-
private static String getVideoBitrateLimit() {
534-
var value = System.getenv("VIDEO_BITRATE_LIMIT");
568+
@SafeVarargs
569+
private static List<String> buildFfmpegCommand(final List<String>... commands) {
570+
var command = new ArrayList<String>();
571+
for (List<String> cmd : commands) {
572+
command.addAll(cmd);
573+
}
535574

536-
return value == null || value.isBlank() ? "600K" : value;
575+
return command;
537576
}
538577

539578
private MediaHelper() {

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import java.io.IOException;
1010
import java.io.UncheckedIOException;
11+
import java.util.List;
1112
import java.util.StringJoiner;
1213
import java.util.concurrent.Semaphore;
1314
import java.util.concurrent.TimeUnit;
@@ -33,7 +34,7 @@ public final class ProcessHelper {
3334
* </ul>
3435
* @throws InterruptedException if the current thread is interrupted while waiting for the command to finish
3536
*/
36-
public static String executeCommand(final String... command) throws ProcessException, InterruptedException {
37+
public static String executeCommand(final List<String> command) throws ProcessException, InterruptedException {
3738
SEMAPHORE.acquire();
3839

3940
try (var process = new ProcessBuilder(command).start()) {
@@ -55,22 +56,24 @@ public static String executeCommand(final String... command) throws ProcessExcep
5556
}
5657
});
5758

59+
var commandName = command.getFirst();
60+
5861
var finished = process.waitFor(1, TimeUnit.MINUTES);
5962
if (!finished) {
6063
process.destroyForcibly();
6164
outputThread.join();
6265
errorThread.join();
63-
LOGGER.at(Level.WARN).log("The command {} timed out after 1m: {}", command[0], standardError.toString());
64-
throw new ProcessException("The command {} timed out after 1m", command[0]);
66+
LOGGER.at(Level.WARN).log("The command {} timed out after 1m: {}", commandName, standardError.toString());
67+
throw new ProcessException("The command {} timed out after 1m", commandName);
6568
}
6669

6770
outputThread.join();
6871
errorThread.join();
6972

7073
var exitCode = process.exitValue();
7174
if (exitCode != 0) {
72-
LOGGER.at(Level.WARN).log("The command {} exited with code {}: {}", command[0], exitCode, standardError.toString());
73-
throw new ProcessException("The command {} exited with code {}", command[0], exitCode);
75+
LOGGER.at(Level.WARN).log("The command {} exited with code {}: {}", commandName, exitCode, standardError.toString());
76+
throw new ProcessException("The command {} exited with code {}", commandName, exitCode);
7477
}
7578

7679
return standardOutput.toString();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
3030

3131
import java.io.File;
32+
import java.util.List;
3233
import java.util.concurrent.ConcurrentHashMap;
3334
import java.util.concurrent.atomic.AtomicInteger;
3435
import java.util.stream.IntStream;
@@ -143,7 +144,7 @@ void resizeSvgImage() throws Exception {
143144
}
144145

145146
private static void assumeSvgSupport() throws Exception {
146-
var decoders = ProcessHelper.executeCommand("ffmpeg", "-v", "quiet", "-hide_banner", "-decoders");
147+
var decoders = ProcessHelper.executeCommand(List.of("ffmpeg", "-v", "quiet", "-hide_banner", "-decoders"));
147148
var supportsSvg = decoders.contains("(codec svg)");
148149
assumeTrue(supportsSvg, "FFmpeg was not compiled with SVG support");
149150
}

0 commit comments

Comments
 (0)