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}