|
12 | 12 | import static com.github.stickerifier.stickerify.media.MediaConstraints.VP9_CODEC; |
13 | 13 | import static java.nio.charset.StandardCharsets.UTF_8; |
14 | 14 |
|
15 | | -import com.github.stickerifier.stickerify.exception.CorruptedVideoException; |
| 15 | +import com.github.stickerifier.stickerify.exception.CorruptedFileException; |
16 | 16 | import com.github.stickerifier.stickerify.exception.FileOperationException; |
17 | 17 | import com.github.stickerifier.stickerify.exception.MediaException; |
18 | 18 | import com.github.stickerifier.stickerify.exception.ProcessException; |
19 | 19 | import com.github.stickerifier.stickerify.process.OsConstants; |
20 | | -import com.github.stickerifier.stickerify.process.PathLocator; |
21 | 20 | import com.github.stickerifier.stickerify.process.ProcessHelper; |
22 | 21 | import com.google.gson.Gson; |
23 | 22 | import com.google.gson.JsonSyntaxException; |
|
26 | 25 | import org.jspecify.annotations.Nullable; |
27 | 26 | import org.slf4j.Logger; |
28 | 27 | import org.slf4j.LoggerFactory; |
29 | | -import ws.schild.jave.EncoderException; |
30 | | -import ws.schild.jave.MultimediaObject; |
31 | | -import ws.schild.jave.info.MultimediaInfo; |
32 | 28 |
|
33 | 29 | import java.io.File; |
34 | 30 | import java.io.FileInputStream; |
35 | 31 | import java.io.IOException; |
36 | 32 | import java.nio.file.Files; |
| 33 | +import java.util.List; |
37 | 34 | import java.util.Set; |
38 | 35 | import java.util.zip.GZIPInputStream; |
39 | 36 |
|
@@ -128,34 +125,105 @@ private static boolean isSupportedVideo(String mimeType) { |
128 | 125 | * @param file the file to check |
129 | 126 | * @return {@code true} if the file is compliant |
130 | 127 | * @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 |
131 | 129 | */ |
132 | | - private static boolean isVideoCompliant(File file) throws FileOperationException, CorruptedVideoException { |
| 130 | + private static boolean isVideoCompliant(File file) throws FileOperationException, CorruptedFileException, InterruptedException { |
133 | 131 | 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; // not a video, maybe throw |
| 136 | + } |
| 137 | + |
| 138 | + var duration = formatInfo.durationAsMillis(); |
| 139 | + if (duration == null) { |
| 140 | + return false; // not a video, maybe throw |
| 141 | + } |
| 142 | + |
| 143 | + var videoInfo = mediaInfo.video(); |
| 144 | + if (videoInfo == null) { |
| 145 | + return false; // not a video, maybe throw |
| 146 | + } |
| 147 | + |
| 148 | + var size = formatInfo.sizeAsLong(); |
| 149 | + var frameRate = videoInfo.frameRateAsFloat(); |
| 150 | + |
| 151 | + return isSizeCompliant(videoInfo.width(), videoInfo.height()) |
| 152 | + && frameRate <= MAX_VIDEO_FRAMES |
| 153 | + && VP9_CODEC.equals(videoInfo.codec()) |
| 154 | + && duration <= MAX_VIDEO_DURATION_MILLIS |
| 155 | + && mediaInfo.audio() == null |
| 156 | + && formatInfo.format().startsWith(MATROSKA_FORMAT) |
| 157 | + && size <= MAX_VIDEO_FILE_SIZE; |
145 | 158 | } |
146 | 159 |
|
147 | 160 | /** |
148 | 161 | * Convenience method to retrieve multimedia information of a file. |
149 | 162 | * |
150 | 163 | * @param file the video to check |
151 | 164 | * @return passed-in video's multimedia information |
152 | | - * @throws CorruptedVideoException if an error occurred retrieving video information |
| 165 | + * @throws CorruptedFileException if an error occurred retrieving file information |
| 166 | + * @throws InterruptedException if the current thread is interrupted while retrieving file info |
153 | 167 | */ |
154 | | - private static MultimediaInfo retrieveMultimediaInfo(File file) throws CorruptedVideoException { |
| 168 | + static MultimediaInfo retrieveMultimediaInfo(File file) throws CorruptedFileException, InterruptedException { |
| 169 | + var command = new String[] { |
| 170 | + "ffprobe", |
| 171 | + "-hide_banner", |
| 172 | + "-v", "quiet", |
| 173 | + "-print_format", "json", |
| 174 | + "-show_format", |
| 175 | + "-show_streams", |
| 176 | + file.getAbsolutePath() |
| 177 | + }; |
| 178 | + |
155 | 179 | 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); |
| 180 | + var lines = ProcessHelper.executeCommand(command); |
| 181 | + var body = String.join("\n", lines); |
| 182 | + |
| 183 | + return GSON.fromJson(body, MultimediaInfo.class); |
| 184 | + } catch (ProcessException | JsonSyntaxException e) { |
| 185 | + throw new CorruptedFileException("The file could not be processed successfully", e); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + record MultimediaInfo(List<StreamInfo> streams, @Nullable FormatInfo format) { |
| 190 | + @Nullable StreamInfo audio() { |
| 191 | + return streams.stream() |
| 192 | + .filter(s -> StreamInfo.TYPE_AUDIO.equals(s.type)) |
| 193 | + .findFirst() |
| 194 | + .orElse(null); |
| 195 | + } |
| 196 | + |
| 197 | + @Nullable StreamInfo video() { |
| 198 | + return streams.stream() |
| 199 | + .filter(s -> StreamInfo.TYPE_VIDEO.equals(s.type)) |
| 200 | + .findFirst() |
| 201 | + .orElse(null); |
| 202 | + } |
| 203 | + } |
| 204 | + record StreamInfo(@SerializedName("codec_name") String codec, @SerializedName("codec_type") String type, int width, int height, @SerializedName("avg_frame_rate") String frameRateFraction) { |
| 205 | + private static final String TYPE_AUDIO = "audio"; |
| 206 | + private static final String TYPE_VIDEO = "video"; |
| 207 | + |
| 208 | + float frameRateAsFloat() { |
| 209 | + if (frameRateFraction.contains("/")) { |
| 210 | + var ratio = frameRateFraction.split("/"); |
| 211 | + return Float.parseFloat(ratio[0]) / Float.parseFloat(ratio[1]); |
| 212 | + } else { |
| 213 | + return Float.parseFloat(frameRateFraction); |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + record FormatInfo(@SerializedName("format_name") String format, @SerializedName("duration") @Nullable String duration, String size) { |
| 218 | + @Nullable Long durationAsMillis() { |
| 219 | + if (duration == null) { |
| 220 | + return null; |
| 221 | + } |
| 222 | + return (long) (Float.parseFloat(duration) * 1000); |
| 223 | + } |
| 224 | + |
| 225 | + long sizeAsLong() { |
| 226 | + return Long.parseLong(size); |
159 | 227 | } |
160 | 228 | } |
161 | 229 |
|
@@ -183,7 +251,11 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th |
183 | 251 |
|
184 | 252 | boolean isAnimationCompliant = isAnimationCompliant(sticker); |
185 | 253 | if (isAnimationCompliant) { |
186 | | - return isFileSizeLowerThan(file, MAX_ANIMATION_FILE_SIZE); |
| 254 | + try { |
| 255 | + return Files.size(file.toPath()) <= MAX_ANIMATION_FILE_SIZE; |
| 256 | + } catch (IOException e) { |
| 257 | + throw new FileOperationException(e); |
| 258 | + } |
187 | 259 | } |
188 | 260 |
|
189 | 261 | LOGGER.atWarn().log("The {} doesn't meet Telegram's requirements", sticker); |
@@ -226,38 +298,33 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation |
226 | 298 | && animation.height() == MAX_SIDE_LENGTH; |
227 | 299 | } |
228 | 300 |
|
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 | | - |
245 | 301 | /** |
246 | 302 | * Checks if passed-in image is already compliant with Telegram's requisites. |
247 | 303 | * |
248 | 304 | * @param image the image to check |
249 | 305 | * @param mimeType the MIME type of the file |
250 | 306 | * @return {@code true} if the file is compliant |
| 307 | + * @throws CorruptedFileException if an error occurred retrieving image information |
| 308 | + * @throws InterruptedException if the current thread is interrupted while retrieving file info |
251 | 309 | */ |
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"); |
| 310 | + private static boolean isImageCompliant(File image, String mimeType) throws CorruptedFileException, InterruptedException { |
| 311 | + var mediaInfo = retrieveMultimediaInfo(image); |
| 312 | + |
| 313 | + var formatInfo = mediaInfo.format(); |
| 314 | + if (formatInfo == null) { |
| 315 | + return false; // not an image, maybe throw |
256 | 316 | } |
257 | 317 |
|
| 318 | + var videoInfo = mediaInfo.video(); |
| 319 | + if (videoInfo == null) { |
| 320 | + return false; // not an image, maybe throw |
| 321 | + } |
| 322 | + |
| 323 | + var size = formatInfo.sizeAsLong(); |
| 324 | + |
258 | 325 | return ("image/png".equals(mimeType) || "image/webp".equals(mimeType)) |
259 | | - && isSizeCompliant(videoSize.getWidth(), videoSize.getHeight()) |
260 | | - && isFileSizeLowerThan(image, MAX_IMAGE_FILE_SIZE); |
| 326 | + && isSizeCompliant(videoInfo.width(), videoInfo.height()) |
| 327 | + && size <= MAX_IMAGE_FILE_SIZE; |
261 | 328 | } |
262 | 329 |
|
263 | 330 | /** |
|
0 commit comments