66import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_ANIMATION_FRAME_RATE ;
77import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_IMAGE_FILE_SIZE ;
88import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_SIDE_LENGTH ;
9- import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_VIDEO_DURATION_MILLIS ;
9+ import static com .github .stickerifier .stickerify .media .MediaConstraints .MAX_VIDEO_DURATION_SECONDS ;
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 ;
1313import static java .nio .charset .StandardCharsets .UTF_8 ;
1414
15- import com .github .stickerifier .stickerify .exception .CorruptedVideoException ;
15+ import com .github .stickerifier .stickerify .exception .CorruptedFileException ;
1616import com .github .stickerifier .stickerify .exception .FileOperationException ;
1717import com .github .stickerifier .stickerify .exception .MediaException ;
1818import com .github .stickerifier .stickerify .exception .ProcessException ;
1919import com .github .stickerifier .stickerify .process .OsConstants ;
20- import com .github .stickerifier .stickerify .process .PathLocator ;
2120import com .github .stickerifier .stickerify .process .ProcessHelper ;
2221import com .google .gson .Gson ;
2322import com .google .gson .JsonSyntaxException ;
2625import org .jspecify .annotations .Nullable ;
2726import org .slf4j .Logger ;
2827import org .slf4j .LoggerFactory ;
29- import ws .schild .jave .EncoderException ;
30- import ws .schild .jave .MultimediaObject ;
31- import ws .schild .jave .info .MultimediaInfo ;
3228
3329import java .io .File ;
3430import java .io .FileInputStream ;
3531import java .io .IOException ;
3632import java .nio .file .Files ;
33+ import java .util .List ;
3734import java .util .Set ;
3835import java .util .zip .GZIPInputStream ;
3936
@@ -128,37 +125,98 @@ private static boolean isSupportedVideo(String mimeType) {
128125 * @param file the file to check
129126 * @return {@code true} if the file is compliant
130127 * @throws FileOperationException if an error occurred retrieving the size of the file
128+ * @throws InterruptedException if the current thread is interrupted while retrieving file info
131129 */
132- private static boolean isVideoCompliant (File file ) throws FileOperationException , CorruptedVideoException {
130+ private static boolean isVideoCompliant (File file ) throws FileOperationException , CorruptedFileException , InterruptedException {
133131 var mediaInfo = retrieveMultimediaInfo (file );
134- var videoInfo = mediaInfo .getVideo ();
135- var videoSize = videoInfo .getSize ();
136-
137- return isSizeCompliant (videoSize .getWidth (), videoSize .getHeight ())
138- && videoInfo .getFrameRate () <= MAX_VIDEO_FRAMES
139- && videoInfo .getDecoder ().startsWith (VP9_CODEC )
140- && mediaInfo .getDuration () > 0L
141- && mediaInfo .getDuration () <= MAX_VIDEO_DURATION_MILLIS
142- && mediaInfo .getAudio () == null
143- && MATROSKA_FORMAT .equals (mediaInfo .getFormat ())
144- && isFileSizeLowerThan (file , MAX_VIDEO_FILE_SIZE );
132+
133+ var formatInfo = mediaInfo .format ();
134+ if (formatInfo == null ) {
135+ return false ;
136+ }
137+
138+ if (formatInfo .duration () == null ) {
139+ return false ;
140+ }
141+
142+ var videoInfo = mediaInfo .video ();
143+ if (videoInfo == null ) {
144+ return false ;
145+ }
146+
147+ return isSizeCompliant (videoInfo .width (), videoInfo .height ())
148+ && videoInfo .frameRate () <= MAX_VIDEO_FRAMES
149+ && VP9_CODEC .equals (videoInfo .codec ())
150+ && formatInfo .duration () <= MAX_VIDEO_DURATION_SECONDS
151+ && mediaInfo .audio () == null
152+ && formatInfo .format ().startsWith (MATROSKA_FORMAT )
153+ && formatInfo .size () <= MAX_VIDEO_FILE_SIZE ;
145154 }
146155
147156 /**
148157 * Convenience method to retrieve multimedia information of a file.
149158 *
150159 * @param file the video to check
151160 * @return passed-in video's multimedia information
152- * @throws CorruptedVideoException if an error occurred retrieving video information
161+ * @throws CorruptedFileException if an error occurred retrieving file information
162+ * @throws InterruptedException if the current thread is interrupted while retrieving file info
153163 */
154- private static MultimediaInfo retrieveMultimediaInfo (File file ) throws CorruptedVideoException {
164+ static MultimediaInfo retrieveMultimediaInfo (File file ) throws CorruptedFileException , InterruptedException {
165+ var command = new String [] {
166+ "ffprobe" ,
167+ "-hide_banner" ,
168+ "-v" , "quiet" ,
169+ "-print_format" , "json" ,
170+ "-show_format" ,
171+ "-show_streams" ,
172+ file .getAbsolutePath ()
173+ };
174+
155175 try {
156- return new MultimediaObject (file , PathLocator .INSTANCE ).getInfo ();
157- } catch (EncoderException e ) {
158- throw new CorruptedVideoException ("The video could not be processed successfully" , e );
176+ var output = ProcessHelper .executeCommand (command );
177+
178+ return GSON .fromJson (output , MultimediaInfo .class );
179+ } catch (ProcessException | JsonSyntaxException e ) {
180+ throw new CorruptedFileException ("The file could not be processed successfully" , e );
159181 }
160182 }
161183
184+ record MultimediaInfo (List <StreamInfo > streams , @ Nullable FormatInfo format ) {
185+ @ Nullable
186+ StreamInfo audio () {
187+ return streams .stream ()
188+ .filter (s -> s .type == CodecType .AUDIO )
189+ .findFirst ()
190+ .orElse (null );
191+ }
192+
193+ @ Nullable
194+ StreamInfo video () {
195+ return streams .stream ()
196+ .filter (s -> s .type == CodecType .VIDEO )
197+ .findFirst ()
198+ .orElse (null );
199+ }
200+ }
201+
202+ record StreamInfo (@ SerializedName ("codec_name" ) String codec , @ SerializedName ("codec_type" ) CodecType type , int width , int height , @ SerializedName ("avg_frame_rate" ) String frameRateRatio ) {
203+ float frameRate () {
204+ if (frameRateRatio .contains ("/" )) {
205+ var ratio = frameRateRatio .split ("/" );
206+ return Float .parseFloat (ratio [0 ]) / Float .parseFloat (ratio [1 ]);
207+ } else {
208+ return Float .parseFloat (frameRateRatio );
209+ }
210+ }
211+ }
212+
213+ private enum CodecType {
214+ @ SerializedName ("video" ) VIDEO ,
215+ @ SerializedName ("audio" ) AUDIO
216+ }
217+
218+ record FormatInfo (@ SerializedName ("format_name" ) String format , @ Nullable Float duration , long size ) {}
219+
162220 /**
163221 * Checks if the file is a {@code gzip} archive, then it reads its content and verifies if it's a valid JSON.
164222 * Once JSON information is retrieved, they are validated against Telegram's requirements.
@@ -183,7 +241,11 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th
183241
184242 boolean isAnimationCompliant = isAnimationCompliant (sticker );
185243 if (isAnimationCompliant ) {
186- return isFileSizeLowerThan (file , MAX_ANIMATION_FILE_SIZE );
244+ try {
245+ return Files .size (file .toPath ()) <= MAX_ANIMATION_FILE_SIZE ;
246+ } catch (IOException e ) {
247+ throw new FileOperationException (e );
248+ }
187249 }
188250
189251 LOGGER .atWarn ().log ("The {} doesn't meet Telegram's requirements" , sticker );
@@ -226,38 +288,31 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation
226288 && animation .height () == MAX_SIDE_LENGTH ;
227289 }
228290
229- /**
230- * Checks that passed-in file's size does not exceed the specified threshold.
231- *
232- * @param file the file to check
233- * @param threshold max allowed file size
234- * @return {@code true} if file's size is compliant
235- * @throws FileOperationException if an error occurred retrieving the size of the file
236- */
237- private static boolean isFileSizeLowerThan (File file , long threshold ) throws FileOperationException {
238- try {
239- return Files .size (file .toPath ()) <= threshold ;
240- } catch (IOException e ) {
241- throw new FileOperationException (e );
242- }
243- }
244-
245291 /**
246292 * Checks if passed-in image is already compliant with Telegram's requisites.
247293 *
248294 * @param image the image to check
249295 * @param mimeType the MIME type of the file
250296 * @return {@code true} if the file is compliant
297+ * @throws CorruptedFileException if an error occurred retrieving image information
298+ * @throws InterruptedException if the current thread is interrupted while retrieving file info
251299 */
252- private static boolean isImageCompliant (File image , String mimeType ) throws CorruptedVideoException , FileOperationException {
253- var videoSize = retrieveMultimediaInfo (image ).getVideo ().getSize ();
254- if (videoSize == null ) {
255- throw new FileOperationException ("Couldn't read image dimensions" );
300+ private static boolean isImageCompliant (File image , String mimeType ) throws CorruptedFileException , InterruptedException {
301+ var mediaInfo = retrieveMultimediaInfo (image );
302+
303+ var formatInfo = mediaInfo .format ();
304+ if (formatInfo == null ) {
305+ return false ;
306+ }
307+
308+ var imageInfo = mediaInfo .video ();
309+ if (imageInfo == null ) {
310+ return false ;
256311 }
257312
258313 return ("image/png" .equals (mimeType ) || "image/webp" .equals (mimeType ))
259- && isSizeCompliant (videoSize . getWidth (), videoSize . getHeight ())
260- && isFileSizeLowerThan ( image , MAX_IMAGE_FILE_SIZE ) ;
314+ && isSizeCompliant (imageInfo . width (), imageInfo . height ())
315+ && formatInfo . size () <= MAX_IMAGE_FILE_SIZE ;
261316 }
262317
263318 /**
@@ -362,7 +417,7 @@ private static File convertToWebm(File file) throws MediaException, InterruptedE
362417 "-c:v" , "libvpx-" + VP9_CODEC ,
363418 "-b:v" , "650K" ,
364419 "-pix_fmt" , "yuv420p" ,
365- "-t" , String .valueOf (MAX_VIDEO_DURATION_MILLIS / 1000 ),
420+ "-t" , String .valueOf (MAX_VIDEO_DURATION_SECONDS ),
366421 "-an" ,
367422 "-passlogfile" , logPrefix
368423 };
0 commit comments