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 *
020 */
021package org.nuxeo.ecm.platform.video.convert;
022
023import static org.nuxeo.ecm.platform.video.convert.Constants.INPUT_FILE_PATH_PARAMETER;
024import static org.nuxeo.ecm.platform.video.convert.Constants.OUTPUT_FILE_PATH_PARAMETER;
025import static org.nuxeo.ecm.platform.video.convert.Constants.POSITION_PARAMETER;
026
027import java.io.IOException;
028import java.io.Serializable;
029import java.math.BigDecimal;
030import java.math.RoundingMode;
031import java.util.ArrayList;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036
037import org.apache.commons.io.FilenameUtils;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.Blobs;
042import org.nuxeo.ecm.core.api.CloseableFile;
043import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
044import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolderWithProperties;
045import org.nuxeo.ecm.core.convert.api.ConversionException;
046import org.nuxeo.ecm.core.convert.extension.Converter;
047import org.nuxeo.ecm.core.convert.extension.ConverterDescriptor;
048import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
049import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
050import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService;
051import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
052import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * Converter to extract a list of equally spaced JPEG thumbnails to represent the story-line of a movie file using the
057 * ffmpeg commandline tool.
058 *
059 * @author ogrisel
060 */
061public class StoryboardConverter extends BaseVideoConverter implements Converter {
062
063    public static final Log log = LogFactory.getLog(StoryboardConverter.class);
064
065    public static final String FFMPEG_INFO_COMMAND = "ffmpeg-info";
066
067    public static final String FFMPEG_SCREENSHOT_RESIZE_COMMAND = "ffmpeg-screenshot-resize";
068
069    public static final String WIDTH_PARAM = "width";
070
071    public static final String HEIGHT_PARAM = "height";
072
073    public static final String THUMBNAIL_NUMBER_PARAM = "thumbnail_number";
074
075    protected Map<String, String> commonParams = new HashMap<>();
076
077    @Override
078    public void init(ConverterDescriptor descriptor) {
079        commonParams = descriptor.getParameters();
080        if (!commonParams.containsKey(WIDTH_PARAM)) {
081            commonParams.put(WIDTH_PARAM, "100");
082        }
083        if (!commonParams.containsKey(HEIGHT_PARAM)) {
084            commonParams.put(HEIGHT_PARAM, "62");
085        }
086    }
087
088    @Override
089    public BlobHolder convert(BlobHolder blobHolder, Map<String, Serializable> parameters) throws ConversionException {
090        // Build the empty output structure
091        Map<String, Serializable> properties = new HashMap<>();
092        List<Blob> blobs = new ArrayList<>();
093        List<Double> timecodes = new ArrayList<>();
094        List<String> comments = new ArrayList<>();
095        properties.put("timecodes", (Serializable) timecodes);
096        properties.put("comments", (Serializable) comments);
097        SimpleBlobHolderWithProperties bh = new SimpleBlobHolderWithProperties(blobs, properties);
098
099        Blob blob = blobHolder.getBlob();
100        try (CloseableFile source = blob.getCloseableFile("." + FilenameUtils.getExtension(blob.getFilename()))) {
101
102            CommandLineExecutorService cles = Framework.getLocalService(CommandLineExecutorService.class);
103            CmdParameters params = cles.getDefaultCmdParameters();
104            params.addNamedParameter(INPUT_FILE_PATH_PARAMETER, source.getFile().getAbsolutePath());
105
106            Double duration = (Double) parameters.get("duration");
107            if (duration == null) {
108                log.warn(String.format("Cannot extract storyboard for file '%s'" + " with missing duration info.",
109                        blob.getFilename()));
110                return bh;
111            }
112
113            // add the command line parameters for the storyboard extraction and run it
114            int numberOfThumbnails = getNumberOfThumbnails(parameters);
115            for (int i = 0; i < numberOfThumbnails; i++) {
116                double timecode = BigDecimal.valueOf(i * duration / numberOfThumbnails).setScale(2,
117                        RoundingMode.HALF_UP).doubleValue();
118                Blob thumbBlob = Blobs.createBlobWithExtension(".jpeg");
119                params.addNamedParameter(OUTPUT_FILE_PATH_PARAMETER, thumbBlob.getFile().getAbsolutePath());
120                params.addNamedParameter(POSITION_PARAMETER, String.valueOf(timecode));
121                params.addNamedParameter(WIDTH_PARAM, commonParams.get(WIDTH_PARAM));
122                params.addNamedParameter(HEIGHT_PARAM, commonParams.get(HEIGHT_PARAM));
123                ExecResult result = cles.execCommand(FFMPEG_SCREENSHOT_RESIZE_COMMAND, params);
124                if (!result.isSuccessful()) {
125                    throw result.getError();
126                }
127                thumbBlob.setMimeType("image/jpeg");
128                thumbBlob.setFilename(String.format(Locale.ENGLISH, "%.2f-seconds.jpeg", timecode));
129                blobs.add(thumbBlob);
130                timecodes.add(timecode);
131                comments.add(String.format("%s %d", blob.getFilename(), i));
132            }
133            return bh;
134        } catch (IOException | CommandNotAvailable | CommandException e) {
135            String msg;
136            if (blob != null) {
137                msg = "Error extracting story board from '" + blob.getFilename() + "'";
138            } else {
139                msg = "conversion failed";
140            }
141            throw new ConversionException(msg, e);
142        }
143    }
144
145    protected int getNumberOfThumbnails(Map<String, Serializable> parameters) {
146        int numberOfThumbnails = 9;
147        if (parameters.containsKey(THUMBNAIL_NUMBER_PARAM)) {
148            numberOfThumbnails = (int) parameters.get(THUMBNAIL_NUMBER_PARAM);
149        }
150        // param from converter descriptor still overrides the video service configuration to keep compat
151        if (commonParams.containsKey(THUMBNAIL_NUMBER_PARAM)) {
152            numberOfThumbnails = Integer.parseInt(commonParams.get(THUMBNAIL_NUMBER_PARAM));
153        }
154        if (numberOfThumbnails < 1) {
155            numberOfThumbnails = 1;
156        }
157        return numberOfThumbnails;
158    }
159}