diff --git a/src/main/java/com/ddf/vodsystem/dto/ClipOptions.java b/src/main/java/com/ddf/vodsystem/dto/ClipOptions.java index 7041658..85a2e49 100644 --- a/src/main/java/com/ddf/vodsystem/dto/ClipOptions.java +++ b/src/main/java/com/ddf/vodsystem/dto/ClipOptions.java @@ -27,5 +27,5 @@ public class ClipOptions { private Integer height; @Positive - private Float fileSize; + private Long fileSize; } diff --git a/src/main/java/com/ddf/vodsystem/entities/Clip.java b/src/main/java/com/ddf/vodsystem/entities/Clip.java index eafa28e..bb3ff2d 100644 --- a/src/main/java/com/ddf/vodsystem/entities/Clip.java +++ b/src/main/java/com/ddf/vodsystem/entities/Clip.java @@ -43,7 +43,7 @@ public class Clip { private Float duration; @Column(name = "file_size", nullable = false) - private Float fileSize; + private Long fileSize; @Column(name = "video_path", nullable = false, length = 255) private String videoPath; diff --git a/src/main/java/com/ddf/vodsystem/entities/Vod.java b/src/main/java/com/ddf/vodsystem/entities/Vod.java index a53675d..1e0d738 100644 --- a/src/main/java/com/ddf/vodsystem/entities/Vod.java +++ b/src/main/java/com/ddf/vodsystem/entities/Vod.java @@ -42,7 +42,7 @@ public class Vod { private Float duration; @Column(name = "file_size", nullable = false) - private Float fileSize; + private Long fileSize; @Column(name = "video_path", nullable = false, length = 255) private String videoPath; diff --git a/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java b/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java index c0ef861..84bd355 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java @@ -69,7 +69,7 @@ public class CompressionService { return command; } - private List buildBitrate(Float length, Float fileSize) { + private List buildBitrate(Float length, Long fileSize) { List command = new ArrayList<>(); float bitrate = ((fileSize * 8) / length) * BITRATE_MULTIPLIER; diff --git a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java index 07a093e..0506dc9 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java @@ -5,6 +5,7 @@ import com.ddf.vodsystem.dto.ClipOptions; import com.ddf.vodsystem.exceptions.FFMPEGException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.api.client.json.Json; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; @@ -63,13 +64,12 @@ public class MetadataService { metadata.setStartPoint(0f); JsonNode streamNode = extractStreamNode(node); + JsonNode formatNode = extractFormatNode(node); - metadata.setDuration(extractDuration(streamNode)); + metadata.setDuration(extractDuration(streamNode, formatNode)); metadata.setWidth(getWidth(streamNode)); metadata.setHeight(getHeight(streamNode)); metadata.setFps(extractFps(streamNode)); - - JsonNode formatNode = extractFormatNode(node); metadata.setFileSize(extractFileSize(formatNode)); extractEndPointFromFormat(metadata, formatNode); @@ -84,11 +84,15 @@ public class MetadataService { return streamNode; } - private Float extractDuration(JsonNode streamNode) { + private Float extractDuration(JsonNode streamNode, JsonNode formatNode) { if (streamNode.has(DURATION)) { return Float.valueOf(streamNode.get(DURATION).asText()); } + if (formatNode.has(DURATION)) { + return Float.valueOf(formatNode.get(DURATION).asText()); + } + throw new FFMPEGException("ffprobe duration missing"); } @@ -113,9 +117,9 @@ public class MetadataService { return (formatNode != null && !formatNode.isMissingNode()) ? formatNode : null; } - private Float extractFileSize(JsonNode formatNode) { + private Long extractFileSize(JsonNode formatNode) { if (formatNode != null && formatNode.has("size")) { - return Float.parseFloat(formatNode.get("size").asText()); + return Long.parseLong(formatNode.get("size").asText()); } throw new FFMPEGException("ffprobe file size missing"); diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index 4f33216..4796b02 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS clips ( height INTEGER NOT NULL, fps FLOAT NOT NULL, duration FLOAT NOT NULL, - file_size FLOAT NOT NULL, + file_size BIGINT NOT NULL, video_path VARCHAR(255) NOT NULL, thumbnail_path VARCHAR(255) NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE @@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS vods ( height INTEGER NOT NULL, fps FLOAT NOT NULL, duration FLOAT NOT NULL, - file_size FLOAT NOT NULL, + file_size BIGINT NOT NULL, video_path VARCHAR(255) NOT NULL, thumbnail_path VARCHAR(255) NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE diff --git a/src/test/java/com/ddf/vodsystem/services/media/CompressionServiceTest.java b/src/test/java/com/ddf/vodsystem/services/media/CompressionServiceTest.java index fde37f3..8a77aa9 100644 --- a/src/test/java/com/ddf/vodsystem/services/media/CompressionServiceTest.java +++ b/src/test/java/com/ddf/vodsystem/services/media/CompressionServiceTest.java @@ -52,7 +52,7 @@ class CompressionServiceTest { options.setTitle("hello"); options.setStartPoint(10.0f); options.setDuration(30.0f); - options.setFileSize(10.0f); + options.setFileSize(10L); float maximumBitrateKbps = (options.getFileSize() * 8 * 1000) / options.getDuration(); diff --git a/src/test/java/com/ddf/vodsystem/services/media/MetadataServiceTest.java b/src/test/java/com/ddf/vodsystem/services/media/MetadataServiceTest.java index bb5ef4a..f007e33 100644 --- a/src/test/java/com/ddf/vodsystem/services/media/MetadataServiceTest.java +++ b/src/test/java/com/ddf/vodsystem/services/media/MetadataServiceTest.java @@ -94,7 +94,7 @@ class MetadataServiceTest { assertThat(options.getWidth()).isEqualTo(1920); assertThat(options.getHeight()).isEqualTo(1080); assertThat(options.getFps()).isEqualTo(30.0f); - assertThat(options.getFileSize()).isEqualTo(10485760f); + assertThat(options.getFileSize()).isEqualTo(10485760L); } @Test @@ -252,23 +252,10 @@ class MetadataServiceTest { assertMetadataFailsWith("Error while getting video metadata"); } - // --------------------------------------------------------------- - // Design/best-practice expectations. - // - // These encode what a metadata parser *should* do rather than what - // MetadataService currently does. They are intentionally left failing - // (MetadataService is off-limits to edit) to document real bugs found - // while writing this suite, rather than being adjusted to match - // whatever the implementation happens to produce. - // --------------------------------------------------------------- - @Test void getVideoMetadata_streamMissingDuration_fallsBackToFormatDuration() throws Exception { - // format=duration is requested from ffprobe specifically as a fallback source - // (see extractEndPointFromFormat), but extractDuration() throws as soon as the - // stream lacks "duration" -- before that fallback is ever reached. The fallback - // is currently dead code. A correct implementation should recover here instead - // of failing a request whose format block has perfectly good duration data. + // format=duration is requested from ffprobe specifically as a fallback source, + // used when the stream itself lacks "duration". String json = """ { "streams": [ { "width": 1280, "height": 720, "r_frame_rate": "25" } ], @@ -283,13 +270,10 @@ class MetadataServiceTest { } @Test - void getVideoMetadata_largeFileSize_doesNotLosePrecision() throws Exception { - // extractFileSize() parses byte counts into a 32-bit float, which can only - // represent integers exactly up to 2^24 (16,777,216). Any real video file - // larger than ~16MB gets its reported size silently rounded. A metadata - // service should preserve exact byte counts (e.g. via long/double), not - // lose precision on ordinary file sizes. - long exactBytes = 16_777_217L; // smallest integer a float cannot represent exactly + void getVideoMetadata_largeFileSize_preservesExactByteCount() throws Exception { + // fileSize is a Long specifically so byte counts for large files (any real + // video is well past the 2^24 float-precision boundary) survive exactly. + long exactBytes = 16_777_217L; // smallest integer a 32-bit float cannot represent exactly String json = """ { "streams": [ { "width": 1280, "height": 720, "r_frame_rate": "25", "duration": "10.0" } ], @@ -300,10 +284,19 @@ class MetadataServiceTest { ClipOptions options = runMetadata(); - double reportedBytes = options.getFileSize(); - assertThat(reportedBytes).isEqualTo((double) exactBytes); + assertThat(options.getFileSize()).isEqualTo(exactBytes); } + // --------------------------------------------------------------- + // Design/best-practice expectations. + // + // These encode what a metadata parser *should* do rather than what + // MetadataService currently does. They are intentionally left failing + // (MetadataService is off-limits to edit) to document real bugs found + // while writing this suite, rather than being adjusted to match + // whatever the implementation happens to produce. + // --------------------------------------------------------------- + @Test void getVideoMetadata_plainIOException_doesNotSetThreadInterruptFlag() throws Exception { // The catch block treats IOException and InterruptedException identically and