com.day.cq.dam.video.FFMpegTranscodeProcess Maven / Gradle / Ivy
package com.day.cq.dam.video;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.dam.api.Rendition;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
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.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
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;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.caconfig.resource.ConfigurationResourceResolver;
/**
* Workflow process that calls FFMPEG on the command line to create thumbnails
* of the image. You can specify the dimension of the thumbnails to be created
*
* For example, using the following workflow step arguments:
*
*
*
* [140x100],[48x48]
*
*
* 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 Transcode Process", description = "Workflow process that transcodes video files into different formats")
@Service
@Properties({ @Property(name = "process.label", value = "Transcode Video", propertyPrivate = true) })
public class FFMpegTranscodeProcess extends AbstractFFMpegProcess {
private static final String MIX_DAM_METADATA = "dam:Metadata";
@Reference
protected ConfigurationResourceResolver configResolver;
/**
* The available arguments to this process implementation.
*/
public enum Arguments {
PROCESS_ARGS(""), CONFIGS("tn"), VIDEO_PROFILES("profile");
private String argumentName;
Arguments(String argumentName) {
this.argumentName = argumentName;
}
public String getArgumentName() {
return this.argumentName;
}
public String getArgumentPrefix() {
return this.argumentName + ":";
}
}
private static final String[] propertiesLong = {
VideoConstants.PN_AUDIO_CHANNELS,
VideoConstants.PN_AUDIO_SAMPLING_RATE,
VideoConstants.PN_VIDEO_BITRATE,
VideoConstants.PN_AUDIO_BITRATE,
VideoConstants.PN_VIDEO_BITRATE_TOLERANCE,
VideoConstants.PN_VIDEO_HEIGHT,
VideoConstants.PN_VIDEO_WIDTH,
};
protected void processVideo(final MetaDataMap metaData, final Asset asset,
final File tmpFile, final WorkflowSession wfSession)
throws IOException, RepositoryException {
final long start = System.currentTimeMillis();
log.info("processing asset [{}]...", asset.getPath());
ResourceResolver resolver = getResourceResolver(wfSession.getSession());
// create videos from profiles
String[] videoProfiles = getVideoProfiles(metaData);
for (String videoProfile : videoProfiles) {
VideoProfile profile = VideoProfile.get(resolver, configResolver, videoProfile);
if (profile != null) {
log.info("processVideo: creating video using profile [{}]",
videoProfile);
// creating temp working directory for ffmpeg
File tmpWorkingDir = createTempDir(getWorkingDir());
FFMpegWrapper ffmpegWrapper = FFMpegWrapper.fromProfile(
tmpFile, profile, tmpWorkingDir);
ffmpegWrapper.setExecutableLocator(locator);
FileInputStream fis = null;
try {
final String renditionName = getRenditionName(ffmpegWrapper);
final File video = ffmpegWrapper.transcode();
fis = new FileInputStream(video);
Rendition rendition = asset.addRendition(renditionName,
fis, ffmpegWrapper.getOutputMimetype());
addEncodingMetadata(profile, rendition);
video.delete();
} catch (IOException e) {
log.error(e.getMessage(), e);
log.error(
"processVideo: failed creating video from profile [{}]: {}",
videoProfile, e.getMessage());
} finally {
IOUtils.closeQuietly(fis);
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());
}
}
}
}
log.info("finished processing asset [{}] in [{}ms].", asset.getPath(),
System.currentTimeMillis() - start);
}
private void addEncodingMetadata(VideoProfile profile, Rendition rendition)
throws RepositoryException, PersistenceException {
Resource contentRes = rendition.getChild(JcrConstants.JCR_CONTENT);
if (contentRes != null) {
Node contentNode = contentRes.adaptTo(Node.class);
// apply mixin, this mixin allows a metadata node of type
// nt:unstructured under nt:resource
contentNode.addMixin(MIX_DAM_METADATA);
Node metadataNode = null;
if(!contentNode.hasNode(DamConstants.METADATA_FOLDER))
metadataNode = contentNode.addNode(DamConstants.METADATA_FOLDER,JcrConstants.NT_UNSTRUCTURED);
else
metadataNode = contentNode.getNode(DamConstants.METADATA_FOLDER);
// add profile properties to the rendition
ValueMap profileVM = profile.getProperties();
Set> entries = profileVM.entrySet();
for (Map.Entry entry : entries) {
String key = entry.getKey();
// ignore jcr/sling/cq properties
if (!ignoreProperty(key)) {
if(ArrayUtils.indexOf(propertiesLong,key)>=0){
try{
Long value = new Long(Long.parseLong((String) entry.getValue()));
metadataNode.setProperty(key,value);
}
catch(Exception e){
metadataNode.setProperty(key,(String)entry.getValue());
}
}
else{
metadataNode.setProperty(key,(String)entry.getValue());
}
}
}
}
}
private boolean ignoreProperty(String key) {
return key.startsWith("jcr:") || key.startsWith("sling:")
|| key.startsWith("cq:");
}
private String getRenditionName(FFMpegWrapper ffmpegWrapper) {
String outputFormat = ffmpegWrapper.getOutputExtension();
String renditionSelector = ffmpegWrapper.getRenditionSelector();
if (StringUtils.isEmpty(renditionSelector)) {
// fallback to profile name, for legacy reasons
renditionSelector = ffmpegWrapper.getProfileName();
}
StringBuilder builder = new StringBuilder();
builder.append(VideoConstants.RENDITION_PREFIX).append(renditionSelector);
if (ffmpegWrapper.getOutputSize() != null) {
builder.append(".").append(ffmpegWrapper.getOutputSize().width)
.append(".").append(ffmpegWrapper.getOutputSize().height);
}
builder.append(".").append(outputFormat);
return builder.toString();
}
/**
* Reads the thumbnail configurations from the given meta data.
*
* @param metaData
* @return String[] of thumbnail configurations.
*/
public String[] getThumbnailConfigs(MetaDataMap metaData) {
if (isLegacy(metaData)) {
List configs = getValuesFromArgs(
Arguments.CONFIGS.getArgumentName(),
getLegacyArguments(metaData));
return configs.toArray(new String[configs.size()]);
} else {
String[] configs = metaData.get(Arguments.CONFIGS.name(),
String[].class);
return configs != null ? configs : new String[0];
}
}
public String[] getVideoProfiles(MetaDataMap metaData) {
if (isLegacy(metaData)) {
List profiles = getValuesFromArgs(
Arguments.VIDEO_PROFILES.getArgumentName(),
getLegacyArguments(metaData));
return profiles.toArray(new String[profiles.size()]);
} else {
String[] profiles = metaData.get(Arguments.VIDEO_PROFILES.name(),
String[].class);
return profiles != null ? profiles : new String[0];
}
}
private boolean isLegacy(MetaDataMap metaDataMap) {
return metaDataMap.get(Arguments.PROCESS_ARGS.name(), String.class) != null;
}
private String[] getLegacyArguments(MetaDataMap metaData) {
String processArgs = metaData.get(Arguments.PROCESS_ARGS.name(),
String.class);
if (processArgs != null && !processArgs.equals("")) {
return processArgs.split(",");
} else {
return new String[0];
}
}
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(",");
}
// the 'new' way
else {
String[] configs = metaData.get(Arguments.CONFIGS.name(),
String[].class);
if (configs != null) {
return configs;
} else {
return new String[0];
}
}
}
}