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