forked from jsl/video_editing_poc
The storage port now has a real staging operation:
- materializeSourceObject(String sourceObjectKey, Path targetPath) in src/main/java/org/example/videoclips/storage/ObjectStoragePort.java Both storage adapters implement it: - src/main/java/org/example/videoclips/storage/InMemoryObjectStorageAdapter.java creates a placeholder local file for the default stub flow - src/main/java/org/example/videoclips/storage/S3ObjectStorageAdapter.java downloads the object from S3-compatible storage to the worker filesystem The processor now uses that seam before clip generation starts. In src/main/java/org/example/videoclips/processing/ClipProcessor.java it resolves the local input path under the configured FFmpeg input directory, materializes the source object there, and records a SOURCE_MATERIALIZED job event before invoking the clipper. That means the FFmpeg path is no longer relying on a file appearing magically in the input directory. The next concrete step is to finish the output side the same way: persist generated clip object keys and push FFmpeg outputs back through the storage port instead of only creating clip metadata in memory.
This commit is contained in:
parent
9df97362ba
commit
bd620fc773
|
|
@ -1,14 +1,17 @@
|
||||||
package org.example.videoclips.processing;
|
package org.example.videoclips.processing;
|
||||||
|
|
||||||
|
import org.example.videoclips.config.VideoClippingProperties;
|
||||||
import org.example.videoclips.application.VideoClippingRepository;
|
import org.example.videoclips.application.VideoClippingRepository;
|
||||||
import org.example.videoclips.domain.Clip;
|
import org.example.videoclips.domain.Clip;
|
||||||
import org.example.videoclips.domain.ClipJob;
|
import org.example.videoclips.domain.ClipJob;
|
||||||
import org.example.videoclips.domain.ClipJobStatus;
|
import org.example.videoclips.domain.ClipJobStatus;
|
||||||
import org.example.videoclips.domain.VideoAsset;
|
import org.example.videoclips.domain.VideoAsset;
|
||||||
import org.example.videoclips.domain.JobEvent;
|
import org.example.videoclips.domain.JobEvent;
|
||||||
|
import org.example.videoclips.storage.ObjectStoragePort;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -17,10 +20,19 @@ public class ClipProcessor {
|
||||||
|
|
||||||
private final VideoClippingRepository repository;
|
private final VideoClippingRepository repository;
|
||||||
private final VideoClipperPort videoClipperPort;
|
private final VideoClipperPort videoClipperPort;
|
||||||
|
private final ObjectStoragePort objectStoragePort;
|
||||||
|
private final VideoClippingProperties properties;
|
||||||
|
|
||||||
public ClipProcessor(VideoClippingRepository repository, VideoClipperPort videoClipperPort) {
|
public ClipProcessor(
|
||||||
|
VideoClippingRepository repository,
|
||||||
|
VideoClipperPort videoClipperPort,
|
||||||
|
ObjectStoragePort objectStoragePort,
|
||||||
|
VideoClippingProperties properties
|
||||||
|
) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.videoClipperPort = videoClipperPort;
|
this.videoClipperPort = videoClipperPort;
|
||||||
|
this.objectStoragePort = objectStoragePort;
|
||||||
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
|
|
@ -43,6 +55,9 @@ public class ClipProcessor {
|
||||||
recordEvent(jobId, "CANCELLED", "Job was cancelled before clip generation");
|
recordEvent(jobId, "CANCELLED", "Job was cancelled before clip generation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Path localInputPath = Path.of(properties.getFfmpeg().getInputDirectory()).resolve(asset.sourceObjectKey());
|
||||||
|
objectStoragePort.materializeSourceObject(asset.sourceObjectKey(), localInputPath);
|
||||||
|
recordEvent(jobId, "SOURCE_MATERIALIZED", "Source object staged to " + localInputPath);
|
||||||
List<Clip> clips = videoClipperPort.generateClips(
|
List<Clip> clips = videoClipperPort.generateClips(
|
||||||
jobId,
|
jobId,
|
||||||
asset.id(),
|
asset.id(),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package org.example.videoclips.storage;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -26,6 +29,18 @@ public class InMemoryObjectStorageAdapter implements ObjectStoragePort {
|
||||||
// No-op for the in-memory adapter.
|
// No-op for the in-memory adapter.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void materializeSourceObject(String sourceObjectKey, Path targetPath) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(targetPath.getParent());
|
||||||
|
if (!Files.exists(targetPath)) {
|
||||||
|
Files.writeString(targetPath, "stub-video-content");
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException("Unable to materialize in-memory source object", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createClipDownloadUrl(String clipId, Instant expiresAt) {
|
public String createClipDownloadUrl(String clipId, Instant expiresAt) {
|
||||||
return "https://storage.example/clips/" + clipId + "?expiresAt=" + expiresAt.toString() + "&signature=demo";
|
return "https://storage.example/clips/" + clipId + "?expiresAt=" + expiresAt.toString() + "&signature=demo";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package org.example.videoclips.storage;
|
package org.example.videoclips.storage;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -9,6 +10,8 @@ public interface ObjectStoragePort {
|
||||||
|
|
||||||
void completeMultipartUpload(String providerUploadId, String uploadId, List<CompletedUploadPart> parts);
|
void completeMultipartUpload(String providerUploadId, String uploadId, List<CompletedUploadPart> parts);
|
||||||
|
|
||||||
|
void materializeSourceObject(String sourceObjectKey, Path targetPath);
|
||||||
|
|
||||||
String createClipDownloadUrl(String clipId, Instant expiresAt);
|
String createClipDownloadUrl(String clipId, Instant expiresAt);
|
||||||
|
|
||||||
record UploadPartDescriptor(int partNumber, String url) {
|
record UploadPartDescriptor(int partNumber, String url) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import org.example.videoclips.config.VideoClippingProperties;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.core.sync.ResponseTransformer;
|
||||||
import software.amazon.awssdk.regions.Region;
|
import software.amazon.awssdk.regions.Region;
|
||||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||||
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
|
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
|
||||||
|
|
@ -18,6 +19,8 @@ import software.amazon.awssdk.services.s3.presigner.model.PresignedUploadPartReq
|
||||||
import software.amazon.awssdk.services.s3.presigner.model.UploadPartPresignRequest;
|
import software.amazon.awssdk.services.s3.presigner.model.UploadPartPresignRequest;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -87,6 +90,20 @@ public class S3ObjectStorageAdapter implements ObjectStoragePort {
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void materializeSourceObject(String sourceObjectKey, Path targetPath) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(targetPath.getParent());
|
||||||
|
} catch (java.io.IOException ex) {
|
||||||
|
throw new IllegalStateException("Unable to create local source-object directory", ex);
|
||||||
|
}
|
||||||
|
s3Client.getObject(GetObjectRequest.builder()
|
||||||
|
.bucket(properties.getS3().getBucket())
|
||||||
|
.key(sourceObjectKey)
|
||||||
|
.build(),
|
||||||
|
ResponseTransformer.toFile(targetPath));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createClipDownloadUrl(String clipId, Instant expiresAt) {
|
public String createClipDownloadUrl(String clipId, Instant expiresAt) {
|
||||||
String objectKey = "clips/" + clipId + ".mp4";
|
String objectKey = "clips/" + clipId + ".mp4";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
stub-video-content
|
||||||
Loading…
Reference in New Issue