diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/SubmissionPayload.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/SubmissionPayload.java
index 1fea3c6..9ac0a48 100644
--- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/SubmissionPayload.java
+++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/SubmissionPayload.java
@@ -8,5 +8,5 @@ public record SubmissionPayload(
float weight,
String weightUnit,
int reps,
- String videoFileId
+ long taskId
) {}
diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/model/submission/Submission.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/model/submission/Submission.java
index 3a98a48..209a02d 100644
--- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/model/submission/Submission.java
+++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/model/submission/Submission.java
@@ -32,13 +32,24 @@ public class Submission {
@Column(nullable = false)
private LocalDateTime performedAt;
+ /**
+ * The id of the video processing task that a user gives to us when they
+ * create the submission, so that when the task finishes processing, we can
+ * route its data to the right submission.
+ */
+ @Column(nullable = false, updatable = false)
+ private long videoProcessingTaskId;
+
/**
* The id of the video file that was submitted for this submission. It lives
* on the gymboard-cdn service as a stored file, which can be
* accessed via GET https://CDN-HOST/files/{videoFileId}
.
*/
- @Column(nullable = false, updatable = false, length = 26)
- private String videoFileId;
+ @Column(length = 26)
+ private String videoFileId = null;
+
+ @Column(length = 26)
+ private String thumbnailFileId = null;
@Column(nullable = false, precision = 7, scale = 2)
private BigDecimal rawWeight;
@@ -64,7 +75,7 @@ public class Submission {
Exercise exercise,
User user,
LocalDateTime performedAt,
- String videoFileId,
+ long videoProcessingTaskId,
BigDecimal rawWeight,
WeightUnit unit,
BigDecimal metricWeight,
@@ -73,9 +84,9 @@ public class Submission {
this.id = id;
this.gym = gym;
this.exercise = exercise;
- this.videoFileId = videoFileId;
this.user = user;
this.performedAt = performedAt;
+ this.videoProcessingTaskId = videoProcessingTaskId;
this.rawWeight = rawWeight;
this.weightUnit = unit;
this.metricWeight = metricWeight;
@@ -99,10 +110,26 @@ public class Submission {
return exercise;
}
+ public long getVideoProcessingTaskId() {
+ return videoProcessingTaskId;
+ }
+
public String getVideoFileId() {
return videoFileId;
}
+ public String getThumbnailFileId() {
+ return thumbnailFileId;
+ }
+
+ public void setVideoFileId(String videoFileId) {
+ this.videoFileId = videoFileId;
+ }
+
+ public void setThumbnailFileId(String thumbnailFileId) {
+ this.thumbnailFileId = thumbnailFileId;
+ }
+
public User getUser() {
return user;
}
diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java
index 9f355de..4208730 100644
--- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java
+++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java
@@ -50,4 +50,14 @@ public class CdnClient {
HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
return objectMapper.readValue(response.body(), responseType);
}
+
+ public void post(String urlPath) throws IOException, InterruptedException {
+ HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
+ if (response.statusCode() != 200) {
+ throw new IOException("Request failed with code " + response.statusCode());
+ }
+ }
}
diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java
index a08b25d..903d1fc 100644
--- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java
+++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java
@@ -3,8 +3,12 @@ package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client;
import java.nio.file.Path;
public record UploadsClient(CdnClient client) {
- public record FileUploadResponse(String id) {}
- public record VideoProcessingTaskStatusResponse(String status) {}
+ public record FileUploadResponse(long taskId) {}
+ public record VideoProcessingTaskStatusResponse(
+ String status,
+ String videoFileId,
+ String thumbnailFileId
+ ) {}
public record FileMetadataResponse(
String filename,
@@ -14,15 +18,19 @@ public record UploadsClient(CdnClient client) {
boolean availableForDownload
) {}
- public FileUploadResponse uploadVideo(Path filePath, String contentType) throws Exception {
- return client.postFile("/uploads/video", filePath, contentType, FileUploadResponse.class);
+ public long uploadVideo(Path filePath, String contentType) throws Exception {
+ return client.postFile("/uploads/video", filePath, contentType, FileUploadResponse.class).taskId();
}
- public VideoProcessingTaskStatusResponse getVideoProcessingStatus(String id) throws Exception {
+ public VideoProcessingTaskStatusResponse getVideoProcessingTaskStatus(long id) throws Exception {
return client.get("/uploads/video/" + id + "/status", VideoProcessingTaskStatusResponse.class);
}
public FileMetadataResponse getFileMetadata(String id) throws Exception {
return client.get("/files/" + id + "/metadata", FileMetadataResponse.class);
}
+
+ public void startTask(long taskId) throws Exception {
+ client.post("/uploads/video/" + taskId + "/start");
+ }
}
diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java
index b82a6a9..1df518e 100644
--- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java
+++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java
@@ -91,9 +91,14 @@ public class ExerciseSubmissionService {
Submission submission = submissionRepository.saveAndFlush(new Submission(
ulid.nextULID(), gym, exercise, user,
performedAt,
- payload.videoFileId(),
+ payload.taskId(),
rawWeight, weightUnit, metricWeight, payload.reps()
));
+ try {
+ cdnClient.uploads.startTask(submission.getVideoProcessingTaskId());
+ } catch (Exception e) {
+ log.error("Failed to start video processing task for submission " + submission.getId(), e);
+ }
return new SubmissionResponse(submission);
}
@@ -118,17 +123,13 @@ public class ExerciseSubmissionService {
}
try {
- UploadsClient.FileMetadataResponse metadata = cdnClient.uploads.getFileMetadata(data.videoFileId());
- if (metadata == null) {
- response.addMessage("Missing video file.");
- } else if (!metadata.availableForDownload()) {
- response.addMessage("File not yet available for download.");
- } else if (!"video/mp4".equals(metadata.mimeType())) {
- response.addMessage("Invalid video file format.");
+ var status = cdnClient.uploads.getVideoProcessingTaskStatus(data.taskId());
+ if (!status.status().equalsIgnoreCase("NOT_STARTED")) {
+ response.addMessage("Invalid video processing task.");
}
} catch (Exception e) {
- log.error("Error fetching file metadata.", e);
- throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error fetching uploaded video file metadata.");
+ log.error("Error fetching task status.", e);
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error fetching uploaded video task status.");
}
return response;
}
diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleDataLoader.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleDataLoader.java
index 285e174..70303cb 100644
--- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleDataLoader.java
+++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleDataLoader.java
@@ -56,7 +56,7 @@ public class SampleDataLoader implements ApplicationListener videoIds = new ArrayList<>();
- var video1 = cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_curl.mp4"), "video/mp4");
- var video2 = cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_ohp.mp4"), "video/mp4");
- videoIds.add(video1.id());
- videoIds.add(video2.id());
+ var uploads = generateUploads();
+ // Now that uploads are complete, we can proceed with generating the submissions.
List gyms = gymRepository.findAll();
List users = userRepository.findAll();
List exercises = exerciseRepository.findAll();
@@ -65,24 +64,27 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
Random random = new Random(1);
List submissions = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
- submissions.add(generateRandomSubmission(
+ Submission submission = generateRandomSubmission(
gyms,
users,
exercises,
- videoIds,
+ uploads,
earliestSubmission,
latestSubmission,
random
- ));
+ );
+ submissions.add(submission);
}
submissionRepository.saveAll(submissions);
+
+ // After adding all the submissions, we'll signal to CDN that it can start processing.
}
private Submission generateRandomSubmission(
List gyms,
List users,
List exercises,
- List videoIds,
+ Map> uploads,
LocalDateTime earliestSubmission,
LocalDateTime latestSubmission,
Random random
@@ -102,13 +104,16 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
randomChoice(exercises, random),
randomChoice(users, random),
time,
- randomChoice(videoIds, random),
+ randomChoice(new ArrayList<>(uploads.keySet()), random),
rawWeight,
weightUnit,
metricWeight,
random.nextInt(13) + 1
);
submission.setVerified(true);
+ var uploadData = uploads.get(submission.getVideoProcessingTaskId());
+ submission.setVideoFileId(uploadData.getFirst());
+ submission.setThumbnailFileId(uploadData.getSecond());
return submission;
}
@@ -125,4 +130,48 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
Duration dur = Duration.between(start, end);
return start.plusSeconds(rand.nextLong(dur.toSeconds() + 1));
}
+
+ /**
+ * Generates a set of sample video uploads to use for all the sample
+ * submissions.
+ * @return A map containing keys representing video processing task ids, and
+ * values being a pair of video and thumbnail file ids.
+ * @throws Exception If an error occurs.
+ */
+ private Map> generateUploads() throws Exception {
+ final CdnClient cdnClient = new CdnClient(cdnOrigin);
+
+ List taskIds = new ArrayList<>();
+ taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_curl.mp4"), "video/mp4"));
+ taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_ohp.mp4"), "video/mp4"));
+
+ Map taskStatus = new HashMap<>();
+ for (long taskId : taskIds) {
+ cdnClient.uploads.startTask(taskId);
+ taskStatus.put(taskId, cdnClient.uploads.getVideoProcessingTaskStatus(taskId));
+ }
+
+ // Wait for all video uploads to complete.
+ while (
+ taskStatus.values().stream()
+ .map(UploadsClient.VideoProcessingTaskStatusResponse::status)
+ .anyMatch(status -> !List.of("COMPLETED", "FAILED").contains(status.toUpperCase()))
+ ) {
+ log.info("Waiting for sample video upload tasks to finish...");
+ Thread.sleep(1000);
+ for (long taskId : taskIds) taskStatus.put(taskId, cdnClient.uploads.getVideoProcessingTaskStatus(taskId));
+ }
+
+ // If any upload failed, throw an exception and cancel this generator.
+ if (taskStatus.values().stream().anyMatch(r -> r.status().equalsIgnoreCase("FAILED"))) {
+ throw new IOException("Video upload task processing failed.");
+ }
+
+ // Prepare the final data structure.
+ Map> finalResults = new HashMap<>();
+ for (var entry : taskStatus.entrySet()) {
+ finalResults.put(entry.getKey(), Pair.of(entry.getValue().videoFileId(), entry.getValue().thumbnailFileId()));
+ }
+ return finalResults;
+ }
}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java
index a1a4d46..3f1d6ed 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java
@@ -180,6 +180,20 @@ public class FileStorageService {
Files.deleteIfExists(filePath);
}
+ public void copyTo(String fileId, Path filePath) throws IOException {
+ Path inputFilePath = getStoragePathForFile(fileId);
+ if (Files.notExists(inputFilePath)) {
+ throw new IOException("File " + fileId + " not found.");
+ }
+ try (
+ var in = Files.newInputStream(inputFilePath);
+ var out = Files.newOutputStream(filePath)
+ ) {
+ readMetadata(in);
+ in.transferTo(out);
+ }
+ }
+
private static LocalDateTime dateFromULID(ULID.Value value) {
return Instant.ofEpochMilli(value.timestamp())
.atOffset(ZoneOffset.UTC)
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java
index fdbfa74..74b1621 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java
@@ -1,5 +1,7 @@
package nl.andrewlalis.gymboardcdn.uploads.api;
public record VideoProcessingTaskStatusResponse(
- String status
+ String status,
+ String videoFileId,
+ String thumbnailFileId
) {}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java
index c0ca5d4..1f202f9 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java
@@ -45,6 +45,12 @@ public class VideoProcessingTask {
@Column(nullable = false, updatable = false, length = 26)
private String uploadFileId;
+ @Column(length = 26)
+ private String videoFileId;
+
+ @Column(length = 26)
+ private String thumbnailFileId;
+
public VideoProcessingTask() {}
public VideoProcessingTask(Status status, String uploadFileId) {
@@ -71,4 +77,20 @@ public class VideoProcessingTask {
public String getUploadFileId() {
return uploadFileId;
}
+
+ public String getVideoFileId() {
+ return videoFileId;
+ }
+
+ public void setVideoFileId(String videoFileId) {
+ this.videoFileId = videoFileId;
+ }
+
+ public String getThumbnailFileId() {
+ return thumbnailFileId;
+ }
+
+ public void setThumbnailFileId(String thumbnailFileId) {
+ this.thumbnailFileId = thumbnailFileId;
+ }
}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java
index 324fe43..e52f754 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java
@@ -78,7 +78,11 @@ public class UploadService {
public VideoProcessingTaskStatusResponse getVideoProcessingStatus(long id) {
VideoProcessingTask task = videoTaskRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- return new VideoProcessingTaskStatusResponse(task.getStatus().name());
+ return new VideoProcessingTaskStatusResponse(
+ task.getStatus().name(),
+ task.getVideoFileId(),
+ task.getThumbnailFileId()
+ );
}
/**
@@ -91,7 +95,9 @@ public class UploadService {
public void startVideoProcessing(long taskId) {
VideoProcessingTask task = videoTaskRepository.findById(taskId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- task.setStatus(VideoProcessingTask.Status.WAITING);
- videoTaskRepository.save(task);
+ if (task.getStatus() == VideoProcessingTask.Status.NOT_STARTED) {
+ task.setStatus(VideoProcessingTask.Status.WAITING);
+ videoTaskRepository.save(task);
+ }
}
}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java
index d2961e8..f9b8703 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java
@@ -89,20 +89,29 @@ public class VideoProcessingService {
log.info("Started processing task {}.", task.getId());
Path uploadFile = fileStorageService.getStoragePathForFile(task.getUploadFileId());
+ Path rawUploadFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-in");
if (Files.notExists(uploadFile) || !Files.isReadable(uploadFile)) {
log.error("Uploaded video file {} doesn't exist or isn't readable.", uploadFile);
updateTask(task, VideoProcessingTask.Status.FAILED);
return;
}
+ try {
+ fileStorageService.copyTo(task.getUploadFileId(), rawUploadFile);
+ } catch (IOException e) {
+ log.error("Failed to copy raw video file {} to {}.", uploadFile, rawUploadFile);
+ e.printStackTrace();
+ updateTask(task, VideoProcessingTask.Status.FAILED);
+ return;
+ }
// Run the actual processing here.
- Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out");
- Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out");
+ Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out.mp4");
+ Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out.jpeg");
try {
log.info("Processing video for uploaded video file {}.", uploadFile.getFileName());
- videoProcessor.processVideo(uploadFile, videoFile);
+ videoProcessor.processVideo(rawUploadFile, videoFile);
log.info("Generating thumbnail for uploaded video file {}.", uploadFile.getFileName());
- thumbnailGenerator.generateThumbnailImage(uploadFile, thumbnailFile);
+ thumbnailGenerator.generateThumbnailImage(videoFile, thumbnailFile);
} catch (Exception e) {
e.printStackTrace();
log.error("""
@@ -111,7 +120,7 @@ public class VideoProcessingService {
Output file: {}
Exception message: {}""",
task.getId(),
- uploadFile,
+ rawUploadFile,
videoFile,
e.getMessage()
);
@@ -129,6 +138,9 @@ public class VideoProcessingService {
// Save the thumbnail too.
FileMetadata thumbnailMetadata = new FileMetadata("thumbnail.jpeg", "image/jpeg", true);
String thumbnailFileId = fileStorageService.save(thumbnailFile, thumbnailMetadata);
+
+ task.setVideoFileId(videoFileId);
+ task.setThumbnailFileId(thumbnailFileId);
updateTask(task, VideoProcessingTask.Status.COMPLETED);
log.info("Finished processing task {}.", task.getId());
@@ -140,6 +152,7 @@ public class VideoProcessingService {
} finally {
try {
fileStorageService.delete(task.getUploadFileId());
+ Files.deleteIfExists(rawUploadFile);
Files.deleteIfExists(videoFile);
Files.deleteIfExists(thumbnailFile);
} catch (IOException e) {