Clean up VideoProcessingService

This commit is contained in:
Andrew Lalis 2023-04-05 08:20:41 +02:00
parent 63550c880d
commit ab3cf591c6
5 changed files with 61 additions and 42 deletions

View File

@ -1,5 +1,6 @@
package nl.andrewlalis.gymboardcdn; package nl.andrewlalis.gymboardcdn;
import com.fasterxml.jackson.databind.ObjectMapper;
import nl.andrewlalis.gymboardcdn.files.FileStorageService; import nl.andrewlalis.gymboardcdn.files.FileStorageService;
import nl.andrewlalis.gymboardcdn.files.util.ULID; import nl.andrewlalis.gymboardcdn.files.util.ULID;
import nl.andrewlalis.gymboardcdn.uploads.service.process.FfmpegThumbnailGenerator; import nl.andrewlalis.gymboardcdn.uploads.service.process.FfmpegThumbnailGenerator;
@ -44,9 +45,14 @@ public class Config {
return new ULID(); return new ULID();
} }
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean @Bean
public FileStorageService fileStorageService() { public FileStorageService fileStorageService() {
return new FileStorageService(ulid(), "cdn-files"); return new FileStorageService(ulid(), objectMapper(), "cdn-files");
} }
@Bean @Bean

View File

@ -36,11 +36,12 @@ public class FileStorageService {
private static final int HEADER_SIZE = 1024; private static final int HEADER_SIZE = 1024;
private final ULID ulid; private final ULID ulid;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper;
private final String baseStorageDir; private final String baseStorageDir;
public FileStorageService(ULID ulid, String baseStorageDir) { public FileStorageService(ULID ulid, ObjectMapper objectMapper, String baseStorageDir) {
this.ulid = ulid; this.ulid = ulid;
this.objectMapper = objectMapper;
this.baseStorageDir = baseStorageDir; this.baseStorageDir = baseStorageDir;
} }
@ -48,6 +49,12 @@ public class FileStorageService {
return ulid.nextULID(); return ulid.nextULID();
} }
public String save(Path inputFile, FileMetadata metadata) throws IOException {
try (var in = Files.newInputStream(inputFile)) {
return save(in, metadata, -1);
}
}
/** /**
* Saves a new file to the storage. * Saves a new file to the storage.
* @param in The input stream to the file contents. * @param in The input stream to the file contents.
@ -182,7 +189,7 @@ public class FileStorageService {
private FileMetadata readMetadata(InputStream in) throws IOException { private FileMetadata readMetadata(InputStream in) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE); ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE);
int readCount = in.read(buffer.array(), 0, HEADER_SIZE); int readCount = in.read(buffer.array(), 0, HEADER_SIZE);
if (readCount != HEADER_SIZE) throw new IOException("Invalid header."); if (readCount != HEADER_SIZE) throw new IOException("Invalid header. Read " + readCount + " bytes instead of " + HEADER_SIZE);
short metadataBytesLength = buffer.getShort(); short metadataBytesLength = buffer.getShort();
byte[] metadataBytes = new byte[metadataBytesLength]; byte[] metadataBytes = new byte[metadataBytesLength];
buffer.get(metadataBytes); buffer.get(metadataBytes);

View File

@ -45,20 +45,11 @@ public class VideoProcessingTask {
@Column(nullable = false, updatable = false, length = 26) @Column(nullable = false, updatable = false, length = 26)
private String uploadFileId; private String uploadFileId;
/**
* The file id for the final processed video file. This doesn't exist yet,
* but we generate the video id right away, just in case there's a need to
* preemptively link to it.
*/
@Column(nullable = false, updatable = false, length = 26)
private String videoFileId;
public VideoProcessingTask() {} public VideoProcessingTask() {}
public VideoProcessingTask(Status status, String uploadFileId, String videoFileId) { public VideoProcessingTask(Status status, String uploadFileId) {
this.status = status; this.status = status;
this.uploadFileId = uploadFileId; this.uploadFileId = uploadFileId;
this.videoFileId = videoFileId;
} }
public Long getId() { public Long getId() {
@ -80,8 +71,4 @@ public class VideoProcessingTask {
public String getUploadFileId() { public String getUploadFileId() {
return uploadFileId; return uploadFileId;
} }
public String getVideoFileId() {
return videoFileId;
}
} }

View File

@ -64,8 +64,7 @@ public class UploadService {
} }
var task = videoTaskRepository.save(new VideoProcessingTask( var task = videoTaskRepository.save(new VideoProcessingTask(
VideoProcessingTask.Status.NOT_STARTED, VideoProcessingTask.Status.NOT_STARTED,
uploadFileId, uploadFileId
fileStorageService.generateFileId()
)); ));
return new VideoUploadResponse(task.getId()); return new VideoUploadResponse(task.getId());
} }

View File

@ -1,8 +1,9 @@
package nl.andrewlalis.gymboardcdn.uploads.service; package nl.andrewlalis.gymboardcdn.uploads.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import nl.andrewlalis.gymboardcdn.files.FileMetadata; import nl.andrewlalis.gymboardcdn.files.FileMetadata;
import nl.andrewlalis.gymboardcdn.files.FileStorageService; import nl.andrewlalis.gymboardcdn.files.FileStorageService;
import nl.andrewlalis.gymboardcdn.files.util.ULID;
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask; import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask;
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository; import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository;
import nl.andrewlalis.gymboardcdn.uploads.service.process.ThumbnailGenerator; import nl.andrewlalis.gymboardcdn.uploads.service.process.ThumbnailGenerator;
@ -13,6 +14,10 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -29,17 +34,24 @@ public class VideoProcessingService {
private final FileStorageService fileStorageService; private final FileStorageService fileStorageService;
private final VideoProcessor videoProcessor; private final VideoProcessor videoProcessor;
private final ThumbnailGenerator thumbnailGenerator; private final ThumbnailGenerator thumbnailGenerator;
private final ObjectMapper objectMapper;
public VideoProcessingService(Executor videoProcessingExecutor, public VideoProcessingService(Executor videoProcessingExecutor,
VideoProcessingTaskRepository taskRepo, VideoProcessingTaskRepository taskRepo,
FileStorageService fileStorageService, FileStorageService fileStorageService,
VideoProcessor videoProcessor, VideoProcessor videoProcessor,
ThumbnailGenerator thumbnailGenerator) { ThumbnailGenerator thumbnailGenerator, ObjectMapper objectMapper) {
this.videoProcessingExecutor = videoProcessingExecutor; this.videoProcessingExecutor = videoProcessingExecutor;
this.taskRepo = taskRepo; this.taskRepo = taskRepo;
this.fileStorageService = fileStorageService; this.fileStorageService = fileStorageService;
this.videoProcessor = videoProcessor; this.videoProcessor = videoProcessor;
this.thumbnailGenerator = thumbnailGenerator; this.thumbnailGenerator = thumbnailGenerator;
this.objectMapper = objectMapper;
}
private void updateTask(VideoProcessingTask task, VideoProcessingTask.Status status) {
task.setStatus(status);
taskRepo.saveAndFlush(task);
} }
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
@ -83,6 +95,7 @@ public class VideoProcessingService {
return; return;
} }
// Run the actual processing here.
Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out"); Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out");
Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out"); Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out");
try { try {
@ -107,29 +120,20 @@ public class VideoProcessingService {
} }
// And finally, copy the output to the final location. // And finally, copy the output to the final location.
try ( try {
var videoIn = Files.newInputStream(videoFile);
var thumbnailIn = Files.newInputStream(thumbnailFile)
) {
// Save the video to a final file location. // Save the video to a final file location.
var originalMetadata = fileStorageService.getMetadata(task.getUploadFileId()); var originalMetadata = fileStorageService.getMetadata(task.getUploadFileId());
FileMetadata metadata = new FileMetadata( FileMetadata metadata = new FileMetadata(originalMetadata.filename(), originalMetadata.mimeType(), true);
originalMetadata.filename(), String videoFileId = fileStorageService.save(videoFile, metadata);
originalMetadata.mimeType(),
true
);
fileStorageService.save(ULID.parseULID(task.getVideoFileId()), videoIn, metadata, Files.size(videoFile));
// Save the thumbnail too. // Save the thumbnail too.
FileMetadata thumbnailMetadata = new FileMetadata( FileMetadata thumbnailMetadata = new FileMetadata("thumbnail.jpeg", "image/jpeg", true);
"thumbnail.jpeg", String thumbnailFileId = fileStorageService.save(thumbnailFile, thumbnailMetadata);
"image/jpeg",
true
);
fileStorageService.save(thumbnailIn, thumbnailMetadata, Files.size(thumbnailFile));
updateTask(task, VideoProcessingTask.Status.COMPLETED); updateTask(task, VideoProcessingTask.Status.COMPLETED);
log.info("Finished processing task {}.", task.getId()); log.info("Finished processing task {}.", task.getId());
// TODO: Send HTTP POST to API, with video id and thumbnail id. // Send HTTP POST to API, with video id and thumbnail id.
sendProcessedDataToApi(videoFileId, thumbnailFileId);
} catch (IOException e) { } catch (IOException e) {
log.error("Failed to copy processed video to final storage location.", e); log.error("Failed to copy processed video to final storage location.", e);
updateTask(task, VideoProcessingTask.Status.FAILED); updateTask(task, VideoProcessingTask.Status.FAILED);
@ -145,8 +149,24 @@ public class VideoProcessingService {
} }
} }
private void updateTask(VideoProcessingTask task, VideoProcessingTask.Status status) { private void sendProcessedDataToApi(String videoId, String thumbnailId) throws IOException {
task.setStatus(status); ObjectNode obj = objectMapper.createObjectNode();
taskRepo.saveAndFlush(task); obj.put("videoFileId", videoId);
obj.put("thumbnailFileId", thumbnailId);
String json = objectMapper.writeValueAsString(obj);
HttpClient httpClient = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:8080/submissions/123/processed-data"))
.header("Authorization", "Bearer bullshit")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
try {
HttpResponse<Void> response = httpClient.send(request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() == 200) {
// We can now delete the task.
}
} catch (InterruptedException e) {
// TODO: Retry!
throw new RuntimeException(e);
}
} }
} }