com.day.cq.dam.video.FFMpegStoryBoardProcess Maven / Gradle / Ivy
package com.day.cq.dam.video;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.jcr.RepositoryException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.handler.ffmpeg.FFMpegWrapper;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.metadata.MetaDataMap;
/**
* Workflow process that calls FFMPEG on the command line to create a storyboard
* of a video. A storyboard consists of several key frames extracted from the
* video. The key frames will be stored as subassets of the video asset. Also, a
* merged film strip of the key frames will be stored as a storyboard rendition
* of the video asset.
*
* Process arguments / configuration:
*
*
*
* frames:{Number} The number of key frames to use for the strip. The frames will be evenly distributed
* over the length of the movie. Default number of frames is 10.
* E.g.: frames:10
*
* start:{Number} The start position in seconds for the first key frame. Default is 0. Formats supported:
* plain number (seconds)
* E.g.: start:5
*
* maxWidth:{Number} The maximum width in px of key frames. Default: 320 px. Aspect ratio is kept.
*
* maxHeight:{Number} The maximum height in px of key frames. Default: 240 px. Aspect ratio is kept.
*
* upScale:{Boolean} Whether to upscale key frames if the source video is smaller than maxWidth/maxHeight.
*
* [{time}],[{time}],.. Define number and time stamp of every key frame individually. Using this option
* causes the "frames" and "start" options to be ignored. Time formats supported:
* hh:mm:ss or plain number (seconds)
* E.g.: [00:05:00],[00:10:00]
* Examples:
*
* - Create 10 frames, starting with the first at 12 seconds into the movie.
* frames:10,start:12
*
* - Create 10 frames, each 100 x 80 px, upscaled:
* frames:10,maxWidth:100,maxHeight:80,upScale:true
*
* - Create 5 frames, each at a defined position, each 100 x 80 px, upscaled:
* maxWidth:100,maxHeight:80,[00:05:00],[00:10:00],[00:15:00],[00:20:00],[00:25:00]
*
*
* Will create thumbnails of size 140x100 and 48x48 with a black
* letterbox/pillarbox
*
* This will only happen for assets having a video-based mime-type, others are
* ignored.
*
*/
@Component(label = "Day CQ DAM FFmpeg Storyboard Process", description = "Workflow process that creates storyboards from video files")
@Service
@Properties({ @Property(name = "process.label", value = "Create Video Storyboard", propertyPrivate=true) })
public class FFMpegStoryBoardProcess extends AbstractFFMpegProcess {
private static final Logger log = LoggerFactory.getLogger(FFMpegStoryBoardProcess.class);
/**
* The available arguments to this process implementation.
*/
public enum Arguments {
PROCESS_ARGS("PROCESS_ARGS"), FRAME_COUNT("frames"), START("start"), MAX_WIDTH("maxWidth"), MAX_HEIGHT(
"maxHeight"), UPSCALE("upScale"), FRAMES("");
private String argumentName;
Arguments(String argumentName) {
this.argumentName = argumentName;
}
public String getArgumentName() {
return this.argumentName;
}
public String getArgumentPrefix() {
return this.argumentName + ":";
}
}
protected void processVideo(final MetaDataMap metaData, final Asset asset, final File tmpFile, final WorkflowSession wfSession) throws IOException,
RepositoryException {
String[] args = buildArguments(metaData);
FFMpegWrapper wrapper = null;
File tmpWorkingDir = null;
try {
//creating temp working directory for ffmpeg
tmpWorkingDir = createTempDir(getWorkingDir());
wrapper = new FFMpegWrapper(tmpFile, tmpWorkingDir);
wrapper.setExecutableLocator(locator);
final StoryBoard board = new StoryBoard(wrapper, asset);
for (final String arg : args) {
final String value = getValue(arg);
if (arg.startsWith("frames:")) {
board.setFrames(NumberUtils.toInt(value, 10));
} else if (arg.startsWith("start:")) {
board.setStart(NumberUtils.toInt(value, 0));
} else if (arg.startsWith("maxWidth:")) {
board.setMaxWidth(NumberUtils.toInt(value, 0));
} else if (arg.startsWith("maxHeight:")) {
board.setMaxHeight(NumberUtils.toInt(value, 0));
} else if (arg.startsWith("upScale:")) {
if (null != value) {
board.setUpscale(BooleanUtils.toBoolean(value));
}
} else if (arg.startsWith("[") && arg.endsWith("]")) {
String frameConfig = StringUtils.replaceEach(arg, new String[] { "[", "]" }, new String[] { "", "" });
if (StringUtils.isNotBlank(frameConfig)) {
board.addFrame(frameConfig);
}
}
}
board.create();
log.info("created storyboard for video [{}]", asset.getPath());
} finally {
try {
// cleaning up ffmpeg's temp working directory
if (tmpWorkingDir != null) {
FileUtils.deleteDirectory(tmpWorkingDir);
}
} catch (IOException e) {
log.warn(
"Could not delete ffmpeg's temporary working directory: {}",
tmpWorkingDir.getPath());
}
}
}
private String getValue(final String arg) {
final String[] strings = StringUtils.split(arg, ":");
return (strings.length == 2) ? strings[1] : null;
}
public String[] buildArguments(MetaDataMap metaData) {
// the 'old' way, ensures backward compatibility
String processArgs = metaData.get(Arguments.PROCESS_ARGS.name(), String.class);
if (processArgs != null && !processArgs.equals("")) {
return processArgs.split(",");
} else {
List arguments = new ArrayList();
Integer frameCount = metaData.get(Arguments.FRAME_COUNT.name(), Integer.class);
if (frameCount != null) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.FRAME_COUNT.getArgumentPrefix()).append(frameCount);
arguments.add(builder.toString());
}
Integer start = metaData.get(Arguments.START.name(), Integer.class);
if (start != null) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.START.getArgumentPrefix()).append(start);
arguments.add(builder.toString());
}
Integer maxWidth = metaData.get(Arguments.MAX_WIDTH.name(), Integer.class);
if (maxWidth != null) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.MAX_WIDTH.getArgumentPrefix()).append(maxWidth);
arguments.add(builder.toString());
}
Integer maxHeight = metaData.get(Arguments.MAX_HEIGHT.name(), Integer.class);
if (maxHeight != null) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.MAX_HEIGHT.getArgumentPrefix()).append(maxHeight);
arguments.add(builder.toString());
}
Boolean upScale = metaData.get(Arguments.UPSCALE.name(), Boolean.class);
if (upScale != null) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.UPSCALE.getArgumentPrefix()).append(upScale);
arguments.add(builder.toString());
}
String[] frames = metaData.get(Arguments.FRAMES.name(), String[].class);
if (frames != null) {
for(String frame :frames){
//frame setting must be in square brackets
if(!frame.startsWith("[")){
frame = "[" +frame;
}
if(!frame.endsWith("]")){
frame = frame + "]";
}
arguments.add(frame);
}
}
return arguments.toArray(new String[arguments.size()]);
}
}
}