3333import java .io .FileInputStream ;
3434import java .io .IOException ;
3535import java .nio .file .Files ;
36+ import java .util .ArrayList ;
3637import java .util .List ;
3738import java .util .Set ;
3839import 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 () {
0 commit comments