Clean up VideoProcessingService
This commit is contained in:
parent
63550c880d
commit
ab3cf591c6
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue