fix(file size): use Long instead of float
Float only stores up to 16 million, and file size is stored in bytes. A video can easily be over 16MB (16 million bytes), therefore Long is needed.
This commit is contained in:
@@ -27,5 +27,5 @@ public class ClipOptions {
|
|||||||
private Integer height;
|
private Integer height;
|
||||||
|
|
||||||
@Positive
|
@Positive
|
||||||
private Float fileSize;
|
private Long fileSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class Clip {
|
|||||||
private Float duration;
|
private Float duration;
|
||||||
|
|
||||||
@Column(name = "file_size", nullable = false)
|
@Column(name = "file_size", nullable = false)
|
||||||
private Float fileSize;
|
private Long fileSize;
|
||||||
|
|
||||||
@Column(name = "video_path", nullable = false, length = 255)
|
@Column(name = "video_path", nullable = false, length = 255)
|
||||||
private String videoPath;
|
private String videoPath;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class Vod {
|
|||||||
private Float duration;
|
private Float duration;
|
||||||
|
|
||||||
@Column(name = "file_size", nullable = false)
|
@Column(name = "file_size", nullable = false)
|
||||||
private Float fileSize;
|
private Long fileSize;
|
||||||
|
|
||||||
@Column(name = "video_path", nullable = false, length = 255)
|
@Column(name = "video_path", nullable = false, length = 255)
|
||||||
private String videoPath;
|
private String videoPath;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class CompressionService {
|
|||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> buildBitrate(Float length, Float fileSize) {
|
private List<String> buildBitrate(Float length, Long fileSize) {
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
|
|
||||||
float bitrate = ((fileSize * 8) / length) * BITRATE_MULTIPLIER;
|
float bitrate = ((fileSize * 8) / length) * BITRATE_MULTIPLIER;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.ddf.vodsystem.dto.ClipOptions;
|
|||||||
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.api.client.json.Json;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
@@ -63,13 +64,12 @@ public class MetadataService {
|
|||||||
metadata.setStartPoint(0f);
|
metadata.setStartPoint(0f);
|
||||||
|
|
||||||
JsonNode streamNode = extractStreamNode(node);
|
JsonNode streamNode = extractStreamNode(node);
|
||||||
|
JsonNode formatNode = extractFormatNode(node);
|
||||||
|
|
||||||
metadata.setDuration(extractDuration(streamNode));
|
metadata.setDuration(extractDuration(streamNode, formatNode));
|
||||||
metadata.setWidth(getWidth(streamNode));
|
metadata.setWidth(getWidth(streamNode));
|
||||||
metadata.setHeight(getHeight(streamNode));
|
metadata.setHeight(getHeight(streamNode));
|
||||||
metadata.setFps(extractFps(streamNode));
|
metadata.setFps(extractFps(streamNode));
|
||||||
|
|
||||||
JsonNode formatNode = extractFormatNode(node);
|
|
||||||
metadata.setFileSize(extractFileSize(formatNode));
|
metadata.setFileSize(extractFileSize(formatNode));
|
||||||
extractEndPointFromFormat(metadata, formatNode);
|
extractEndPointFromFormat(metadata, formatNode);
|
||||||
|
|
||||||
@@ -84,11 +84,15 @@ public class MetadataService {
|
|||||||
return streamNode;
|
return streamNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Float extractDuration(JsonNode streamNode) {
|
private Float extractDuration(JsonNode streamNode, JsonNode formatNode) {
|
||||||
if (streamNode.has(DURATION)) {
|
if (streamNode.has(DURATION)) {
|
||||||
return Float.valueOf(streamNode.get(DURATION).asText());
|
return Float.valueOf(streamNode.get(DURATION).asText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formatNode.has(DURATION)) {
|
||||||
|
return Float.valueOf(formatNode.get(DURATION).asText());
|
||||||
|
}
|
||||||
|
|
||||||
throw new FFMPEGException("ffprobe duration missing");
|
throw new FFMPEGException("ffprobe duration missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +117,9 @@ public class MetadataService {
|
|||||||
return (formatNode != null && !formatNode.isMissingNode()) ? formatNode : null;
|
return (formatNode != null && !formatNode.isMissingNode()) ? formatNode : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Float extractFileSize(JsonNode formatNode) {
|
private Long extractFileSize(JsonNode formatNode) {
|
||||||
if (formatNode != null && formatNode.has("size")) {
|
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");
|
throw new FFMPEGException("ffprobe file size missing");
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS clips (
|
|||||||
height INTEGER NOT NULL,
|
height INTEGER NOT NULL,
|
||||||
fps FLOAT NOT NULL,
|
fps FLOAT NOT NULL,
|
||||||
duration FLOAT NOT NULL,
|
duration FLOAT NOT NULL,
|
||||||
file_size FLOAT NOT NULL,
|
file_size BIGINT NOT NULL,
|
||||||
video_path VARCHAR(255) NOT NULL,
|
video_path VARCHAR(255) NOT NULL,
|
||||||
thumbnail_path VARCHAR(255) NOT NULL,
|
thumbnail_path VARCHAR(255) NOT NULL,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
@@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS vods (
|
|||||||
height INTEGER NOT NULL,
|
height INTEGER NOT NULL,
|
||||||
fps FLOAT NOT NULL,
|
fps FLOAT NOT NULL,
|
||||||
duration FLOAT NOT NULL,
|
duration FLOAT NOT NULL,
|
||||||
file_size FLOAT NOT NULL,
|
file_size BIGINT NOT NULL,
|
||||||
video_path VARCHAR(255) NOT NULL,
|
video_path VARCHAR(255) NOT NULL,
|
||||||
thumbnail_path VARCHAR(255) NOT NULL,
|
thumbnail_path VARCHAR(255) NOT NULL,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class CompressionServiceTest {
|
|||||||
options.setTitle("hello");
|
options.setTitle("hello");
|
||||||
options.setStartPoint(10.0f);
|
options.setStartPoint(10.0f);
|
||||||
options.setDuration(30.0f);
|
options.setDuration(30.0f);
|
||||||
options.setFileSize(10.0f);
|
options.setFileSize(10L);
|
||||||
|
|
||||||
float maximumBitrateKbps = (options.getFileSize() * 8 * 1000) / options.getDuration();
|
float maximumBitrateKbps = (options.getFileSize() * 8 * 1000) / options.getDuration();
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class MetadataServiceTest {
|
|||||||
assertThat(options.getWidth()).isEqualTo(1920);
|
assertThat(options.getWidth()).isEqualTo(1920);
|
||||||
assertThat(options.getHeight()).isEqualTo(1080);
|
assertThat(options.getHeight()).isEqualTo(1080);
|
||||||
assertThat(options.getFps()).isEqualTo(30.0f);
|
assertThat(options.getFps()).isEqualTo(30.0f);
|
||||||
assertThat(options.getFileSize()).isEqualTo(10485760f);
|
assertThat(options.getFileSize()).isEqualTo(10485760L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -252,23 +252,10 @@ class MetadataServiceTest {
|
|||||||
assertMetadataFailsWith("Error while getting video metadata");
|
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
|
@Test
|
||||||
void getVideoMetadata_streamMissingDuration_fallsBackToFormatDuration() throws Exception {
|
void getVideoMetadata_streamMissingDuration_fallsBackToFormatDuration() throws Exception {
|
||||||
// format=duration is requested from ffprobe specifically as a fallback source
|
// format=duration is requested from ffprobe specifically as a fallback source,
|
||||||
// (see extractEndPointFromFormat), but extractDuration() throws as soon as the
|
// used when the stream itself lacks "duration".
|
||||||
// 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.
|
|
||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"streams": [ { "width": 1280, "height": 720, "r_frame_rate": "25" } ],
|
"streams": [ { "width": 1280, "height": 720, "r_frame_rate": "25" } ],
|
||||||
@@ -283,13 +270,10 @@ class MetadataServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getVideoMetadata_largeFileSize_doesNotLosePrecision() throws Exception {
|
void getVideoMetadata_largeFileSize_preservesExactByteCount() throws Exception {
|
||||||
// extractFileSize() parses byte counts into a 32-bit float, which can only
|
// fileSize is a Long specifically so byte counts for large files (any real
|
||||||
// represent integers exactly up to 2^24 (16,777,216). Any real video file
|
// video is well past the 2^24 float-precision boundary) survive exactly.
|
||||||
// larger than ~16MB gets its reported size silently rounded. A metadata
|
long exactBytes = 16_777_217L; // smallest integer a 32-bit float cannot represent exactly
|
||||||
// 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
|
|
||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"streams": [ { "width": 1280, "height": 720, "r_frame_rate": "25", "duration": "10.0" } ],
|
"streams": [ { "width": 1280, "height": 720, "r_frame_rate": "25", "duration": "10.0" } ],
|
||||||
@@ -300,10 +284,19 @@ class MetadataServiceTest {
|
|||||||
|
|
||||||
ClipOptions options = runMetadata();
|
ClipOptions options = runMetadata();
|
||||||
|
|
||||||
double reportedBytes = options.getFileSize();
|
assertThat(options.getFileSize()).isEqualTo(exactBytes);
|
||||||
assertThat(reportedBytes).isEqualTo((double) 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
|
@Test
|
||||||
void getVideoMetadata_plainIOException_doesNotSetThreadInterruptFlag() throws Exception {
|
void getVideoMetadata_plainIOException_doesNotSetThreadInterruptFlag() throws Exception {
|
||||||
// The catch block treats IOException and InterruptedException identically and
|
// The catch block treats IOException and InterruptedException identically and
|
||||||
|
|||||||
Reference in New Issue
Block a user