001/* 002 * (C) Copyright 2010-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 * YannisJULIENNE 019 */ 020package org.nuxeo.ecm.platform.video.convert; 021 022import static org.nuxeo.ecm.platform.video.convert.Constants.INPUT_FILE_PATH_PARAMETER; 023import static org.nuxeo.ecm.platform.video.convert.Constants.OUTPUT_FILE_PATH_PARAMETER; 024import static org.nuxeo.ecm.platform.video.convert.Constants.POSITION_PARAMETER; 025 026import java.io.IOException; 027import java.io.Serializable; 028import java.math.BigDecimal; 029import java.math.RoundingMode; 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Locale; 034import java.util.Map; 035 036import org.apache.commons.io.FilenameUtils; 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.nuxeo.ecm.core.api.Blob; 040import org.nuxeo.ecm.core.api.Blobs; 041import org.nuxeo.ecm.core.api.CloseableFile; 042import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 043import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolderWithProperties; 044import org.nuxeo.ecm.core.convert.api.ConversionException; 045import org.nuxeo.ecm.core.convert.extension.Converter; 046import org.nuxeo.ecm.core.convert.extension.ConverterDescriptor; 047import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters; 048import org.nuxeo.ecm.platform.commandline.executor.api.CommandException; 049import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService; 050import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable; 051import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult; 052import org.nuxeo.runtime.api.Framework; 053 054/** 055 * Converter to extract a list of equally spaced JPEG thumbnails to represent the story-line of a movie file using the 056 * ffmpeg commandline tool. 057 * 058 * @author ogrisel 059 */ 060public class StoryboardConverter implements Converter { 061 062 public static final Log log = LogFactory.getLog(StoryboardConverter.class); 063 064 /** @deprecated since 11.1, not used. */ 065 @Deprecated 066 public static final String FFMPEG_INFO_COMMAND = "ffmpeg-info"; 067 068 public static final String FFMPEG_SCREENSHOT_RESIZE_COMMAND = "ffmpeg-screenshot-resize"; 069 070 /** 071 * @since 11.1 072 */ 073 public static final String ORIGINAL_WIDTH_PARAM = "original_width"; 074 075 /** 076 * @since 11.1 077 */ 078 public static final String ORIGINAL_HEIGHT_PARAM = "original_height"; 079 080 public static final String WIDTH_PARAM = "width"; 081 082 public static final String HEIGHT_PARAM = "height"; 083 084 public static final String THUMBNAIL_NUMBER_PARAM = "thumbnail_number"; 085 086 protected Map<String, String> commonParams = new HashMap<>(); 087 088 @Override 089 public void init(ConverterDescriptor descriptor) { 090 commonParams = descriptor.getParameters(); 091 } 092 093 @Override 094 public BlobHolder convert(BlobHolder blobHolder, Map<String, Serializable> parameters) { 095 // Build the empty output structure 096 Map<String, Serializable> properties = new HashMap<>(); 097 List<Blob> blobs = new ArrayList<>(); 098 List<Double> timecodes = new ArrayList<>(); 099 List<String> comments = new ArrayList<>(); 100 properties.put("timecodes", (Serializable) timecodes); 101 properties.put("comments", (Serializable) comments); 102 SimpleBlobHolderWithProperties bh = new SimpleBlobHolderWithProperties(blobs, properties); 103 104 Blob blob = blobHolder.getBlob(); 105 try (CloseableFile source = blob.getCloseableFile("." + FilenameUtils.getExtension(blob.getFilename()))) { 106 107 CommandLineExecutorService cles = Framework.getService(CommandLineExecutorService.class); 108 CmdParameters params = cles.getDefaultCmdParameters(); 109 params.addNamedParameter(INPUT_FILE_PATH_PARAMETER, source.getFile().getAbsolutePath()); 110 111 Double duration = (Double) parameters.get("duration"); 112 if (duration == null) { 113 log.warn(String.format("Cannot extract storyboard for file '%s'" + " with missing duration info.", 114 blob.getFilename())); 115 return bh; 116 } 117 118 // add the command line parameters for the storyboard extraction and run it 119 int numberOfThumbnails = getNumberOfThumbnails(parameters); 120 for (int i = 0; i < numberOfThumbnails; i++) { 121 double timecode = BigDecimal.valueOf(i * duration / numberOfThumbnails) 122 .setScale(2, RoundingMode.HALF_UP) 123 .doubleValue(); 124 Blob thumbBlob = Blobs.createBlobWithExtension(".jpeg"); 125 params.addNamedParameter(OUTPUT_FILE_PATH_PARAMETER, thumbBlob.getFile().getAbsolutePath()); 126 params.addNamedParameter(POSITION_PARAMETER, String.valueOf(timecode)); 127 fillWidthAndHeightParameters(params, parameters); 128 ExecResult result = cles.execCommand(FFMPEG_SCREENSHOT_RESIZE_COMMAND, params); 129 if (!result.isSuccessful()) { 130 throw result.getError(); 131 } 132 thumbBlob.setMimeType("image/jpeg"); 133 thumbBlob.setFilename(String.format(Locale.ENGLISH, "%.2f-seconds.jpeg", timecode)); 134 blobs.add(thumbBlob); 135 timecodes.add(timecode); 136 comments.add(String.format("%s %d", blob.getFilename(), i)); 137 } 138 return bh; 139 } catch (IOException | CommandNotAvailable | CommandException e) { 140 throw new ConversionException("Error extracting story board from '" + blob.getFilename() + "'", e); 141 } 142 } 143 144 protected int getNumberOfThumbnails(Map<String, Serializable> parameters) { 145 int numberOfThumbnails = 9; 146 if (parameters.containsKey(THUMBNAIL_NUMBER_PARAM)) { 147 numberOfThumbnails = (int) parameters.get(THUMBNAIL_NUMBER_PARAM); 148 } 149 // param from converter descriptor still overrides the video service configuration to keep compat 150 if (commonParams.containsKey(THUMBNAIL_NUMBER_PARAM)) { 151 numberOfThumbnails = Integer.parseInt(commonParams.get(THUMBNAIL_NUMBER_PARAM)); 152 } 153 if (numberOfThumbnails < 1) { 154 numberOfThumbnails = 1; 155 } 156 return numberOfThumbnails; 157 } 158 159 protected void fillWidthAndHeightParameters(CmdParameters cmdParameters, Map<String, Serializable> parameters) { 160 String screenshotWidth = commonParams.getOrDefault(WIDTH_PARAM, "130"); 161 String screenshotHeight = commonParams.getOrDefault(HEIGHT_PARAM, "80"); 162 if (parameters.containsKey(ORIGINAL_WIDTH_PARAM) && parameters.containsKey(ORIGINAL_HEIGHT_PARAM)) { 163 long originalWidth = (long) parameters.get(ORIGINAL_WIDTH_PARAM); 164 long originalHeight = (long) parameters.get(ORIGINAL_HEIGHT_PARAM); 165 if (originalHeight > originalWidth) { 166 // invert screenshot size 167 String temp = screenshotWidth; 168 screenshotWidth = screenshotHeight; 169 screenshotHeight = temp; 170 } 171 } 172 cmdParameters.addNamedParameter(WIDTH_PARAM, screenshotWidth); 173 cmdParameters.addNamedParameter(HEIGHT_PARAM, screenshotHeight); 174 } 175}