package io.lumify.gpw.video;
import com.google.common.io.Files;
import com.google.inject.Inject;
import io.lumify.core.ingest.graphProperty.GraphPropertyWorkData;
import io.lumify.core.ingest.graphProperty.GraphPropertyWorker;
import io.lumify.core.ingest.graphProperty.GraphPropertyWorkerPrepareData;
import io.lumify.core.ingest.video.VideoFrameInfo;
import io.lumify.core.model.artifactThumbnails.ArtifactThumbnailRepository;
import io.lumify.core.model.properties.LumifyProperties;
import io.lumify.core.model.properties.MediaLumifyProperties;
import io.lumify.core.model.properties.types.IntegerLumifyProperty;
import io.lumify.core.security.LumifyVisibility;
import io.lumify.core.util.LumifyLogger;
import io.lumify.core.util.LumifyLoggerFactory;
import io.lumify.core.util.ProcessRunner;
import io.lumify.gpw.util.FFprobeRotationUtil;
import org.apache.commons.io.FileUtils;
import org.securegraph.*;
import org.securegraph.mutation.ExistingElementMutation;
import org.securegraph.property.StreamingPropertyValue;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.securegraph.util.IterableUtils.toList;
public class VideoFrameExtractGraphPropertyWorker extends GraphPropertyWorker {
private static final LumifyLogger LOGGER = LumifyLoggerFactory.getLogger(VideoFrameExtractGraphPropertyWorker.class);
private ProcessRunner processRunner;
private IntegerLumifyProperty videoRotationProperty;
@Override
public void prepare(GraphPropertyWorkerPrepareData workerPrepareData) throws Exception {
super.prepare(workerPrepareData);
getAuthorizationRepository().addAuthorizationToGraph(VideoFrameInfo.VISIBILITY_STRING);
videoRotationProperty = new IntegerLumifyProperty(getOntologyRepository().getRequiredPropertyIRIByIntent("media.clockwiseRotation"));
}
@Override
public void execute(InputStream in, GraphPropertyWorkData data) throws Exception {
Integer videoRotation = videoRotationProperty.getPropertyValue(data.getElement(), 0);
Visibility newVisibility = new LumifyVisibility(LumifyVisibility.and(getVisibilityTranslator().toVisibilityNoSuperUser(data.getVisibilityJson()), VideoFrameInfo.VISIBILITY_STRING)).getVisibility();
Pattern fileNamePattern = Pattern.compile("image-([0-9]+)\\.png");
File tempDir = Files.createTempDir();
try {
Double defaultFPSToExtract = 1.0;
extractFrames(data.getLocalFile(), tempDir, data, defaultFPSToExtract, videoRotation);
List<String> propertyKeys = new ArrayList<String>();
long videoDuration = 0;
for (File frameFile : tempDir.listFiles()) {
Matcher m = fileNamePattern.matcher(frameFile.getName());
if (!m.matches()) {
continue;
}
long frameStartTime = (long) ((Double.parseDouble(m.group(1)) / defaultFPSToExtract) * 1000.0);
if (frameStartTime > videoDuration) {
videoDuration = frameStartTime;
}
try (InputStream frameFileIn = new FileInputStream(frameFile)) {
ExistingElementMutation<Vertex> mutation = data.getElement().prepareMutation();
StreamingPropertyValue frameValue = new StreamingPropertyValue(frameFileIn, byte[].class);
frameValue.searchIndex(false);
String key = String.format("%08d", Math.max(0L, frameStartTime));
Metadata metadata = data.createPropertyMetadata();
metadata.add(LumifyProperties.MIME_TYPE.getPropertyName(), "image/png", getVisibilityTranslator().getDefaultVisibility());
metadata.add(MediaLumifyProperties.METADATA_VIDEO_FRAME_START_TIME, frameStartTime, getVisibilityTranslator().getDefaultVisibility());
MediaLumifyProperties.VIDEO_FRAME.addPropertyValue(mutation, key, frameValue, metadata, newVisibility);
propertyKeys.add(key);
mutation.save(getAuthorizations());
}
}
getGraph().flush();
generateAndSaveVideoPreviewImage((Vertex) data.getElement(), videoRotation);
for (String propertyKey : propertyKeys) {
getWorkQueueRepository().pushGraphPropertyQueue(data.getElement(), propertyKey, MediaLumifyProperties.VIDEO_FRAME.getPropertyName());
}
} finally {
FileUtils.deleteDirectory(tempDir);
}
}
private void extractFrames(File videoFileName, File outDir, GraphPropertyWorkData data, double framesPerSecondToExtract, int videoRotation) throws IOException, InterruptedException {
String[] ffmpegOptionsArray = prepareFFMPEGOptions(videoFileName, outDir, data, framesPerSecondToExtract, videoRotation);
processRunner.execute(
"ffmpeg",
ffmpegOptionsArray,
null,
videoFileName.getAbsolutePath() + ": "
);
}
private String[] prepareFFMPEGOptions(File videoFileName, File outDir, GraphPropertyWorkData data, double framesPerSecondToExtract, int videoRotation) {
ArrayList<String> ffmpegOptionsList = new ArrayList<>();
ffmpegOptionsList.add("-i");
ffmpegOptionsList.add(videoFileName.getAbsolutePath());
ffmpegOptionsList.add("-r");
ffmpegOptionsList.add("" + framesPerSecondToExtract);
//Scale.
//Will not force conversion to 720:480 aspect ratio, but will resize video with original aspect ratio.
if (videoRotation == 0 || videoRotation == 180) {
ffmpegOptionsList.add("-s");
ffmpegOptionsList.add("720x480");
} else if (videoRotation == 90 || videoRotation == 270) {
ffmpegOptionsList.add("-s");
ffmpegOptionsList.add("480x720");
}
//Rotate.
String[] ffmpegRotationOptions = FFprobeRotationUtil.createFFMPEGRotationOptions(videoRotation);
if (ffmpegRotationOptions != null) {
ffmpegOptionsList.add(ffmpegRotationOptions[0]);
ffmpegOptionsList.add(ffmpegRotationOptions[1]);
}
ffmpegOptionsList.add(new File(outDir, "image-%8d.png").getAbsolutePath());
return ffmpegOptionsList.toArray(new String[ffmpegOptionsList.size()]);
}
@Override
public boolean isHandled(Element element, Property property) {
if (property == null) {
return false;
}
if (!property.getName().equals(LumifyProperties.RAW.getPropertyName())) {
return false;
}
String mimeType = LumifyProperties.MIME_TYPE.getMetadataValue(property.getMetadata(), null);
if (mimeType == null || !mimeType.startsWith("video")) {
return false;
}
return true;
}
private void generateAndSaveVideoPreviewImage(Vertex artifactVertex, int videoRotation) {
LOGGER.info("Generating video preview for %s", artifactVertex.getId());
try {
Iterable<Property> videoFrames = getVideoFrameProperties(artifactVertex);
List<Property> videoFramesForPreview = getFramesForPreview(videoFrames);
BufferedImage previewImage = createPreviewImage(videoFramesForPreview, videoRotation);
saveImage(artifactVertex, previewImage);
} catch (IOException e) {
throw new RuntimeException("Could not create preview image for artifact: " + artifactVertex.getId(), e);
}
LOGGER.debug("Finished creating preview for: %s", artifactVertex.getId());
}
private void saveImage(Vertex artifactVertex, BufferedImage previewImage) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(previewImage, "png", out);
StreamingPropertyValue spv = new StreamingPropertyValue(new ByteArrayInputStream(out.toByteArray()), byte[].class);
spv.searchIndex(false);
MediaLumifyProperties.VIDEO_PREVIEW_IMAGE.setProperty(artifactVertex, spv, artifactVertex.getVisibility(), getAuthorizations());
getGraph().flush();
}
private BufferedImage createPreviewImage(List<Property> videoFrames, int videoRotation) throws IOException {
int previewFrameWidth;
int previewFrameHeight;
if (videoRotation == 0 || videoRotation == 180) {
previewFrameWidth = ArtifactThumbnailRepository.PREVIEW_FRAME_WIDTH;
previewFrameHeight = ArtifactThumbnailRepository.PREVIEW_FRAME_HEIGHT;
} else {
previewFrameWidth = ArtifactThumbnailRepository.PREVIEW_FRAME_HEIGHT;
previewFrameHeight = ArtifactThumbnailRepository.PREVIEW_FRAME_WIDTH;
}
BufferedImage previewImage = new BufferedImage(previewFrameWidth * videoFrames.size(), previewFrameHeight, BufferedImage.TYPE_INT_RGB);
Graphics g = previewImage.getGraphics();
for (int i = 0; i < videoFrames.size(); i++) {
Property videoFrame = videoFrames.get(i);
Image img = loadImage(videoFrame);
int dx1 = i * previewFrameWidth;
int dy1 = 0;
int dx2 = dx1 + previewFrameWidth;
int dy2 = previewFrameHeight;
int sx1 = 0;
int sy1 = 0;
int sx2 = img.getWidth(null);
int sy2 = img.getHeight(null);
g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null);
}
return previewImage;
}
private Image loadImage(Property videoFrame) throws IOException {
StreamingPropertyValue spv = (StreamingPropertyValue) videoFrame.getValue();
try (InputStream spvIn = spv.getInputStream()) {
BufferedImage img = ImageIO.read(spvIn);
checkNotNull(img, "Could not load image from frame: " + videoFrame);
return img;
}
}
private Iterable<Property> getVideoFrameProperties(Vertex artifactVertex) {
List<Property> videoFrameProperties = toList(artifactVertex.getProperties(MediaLumifyProperties.VIDEO_FRAME.getPropertyName()));
Collections.sort(videoFrameProperties, new Comparator<Property>() {
@Override
public int compare(Property p1, Property p2) {
Long p1StartTime = (Long) p1.getMetadata().getValue(MediaLumifyProperties.METADATA_VIDEO_FRAME_START_TIME);
Long p2StartTime = (Long) p2.getMetadata().getValue(MediaLumifyProperties.METADATA_VIDEO_FRAME_START_TIME);
return p1StartTime.compareTo(p2StartTime);
}
});
return videoFrameProperties;
}
private List<Property> getFramesForPreview(Iterable<Property> videoFramesIterable) {
List<Property> videoFrames = toList(videoFramesIterable);
ArrayList<Property> results = new ArrayList<Property>();
double skip = (double) videoFrames.size() / (double) ArtifactThumbnailRepository.FRAMES_PER_PREVIEW;
for (double i = 0; i < videoFrames.size(); i += skip) {
results.add(videoFrames.get((int) Math.floor(i)));
}
if (results.size() < 20) {
results.add(videoFrames.get(videoFrames.size() - 1));
}
if (results.size() > 20) {
results.remove(results.size() - 1);
}
return results;
}
@Inject
public void setProcessRunner(ProcessRunner processRunner) {
this.processRunner = processRunner;
}
}