001/*
002 * (C) Copyright 2010-2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 */
018package org.nuxeo.ecm.platform.video.convert;
019
020import static org.nuxeo.ecm.platform.video.convert.Constants.INPUT_FILE_PATH_PARAMETER;
021import static org.nuxeo.ecm.platform.video.convert.Constants.OUTPUT_FILE_PATH_PARAMETER;
022import static org.nuxeo.ecm.platform.video.convert.Constants.POSITION_PARAMETER;
023
024import java.io.IOException;
025import java.io.Serializable;
026import java.math.BigDecimal;
027import java.math.RoundingMode;
028import java.util.ArrayList;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032
033import org.apache.commons.io.FilenameUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.core.api.Blob;
037import org.nuxeo.ecm.core.api.Blobs;
038import org.nuxeo.ecm.core.api.CloseableFile;
039import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
040import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolderWithProperties;
041import org.nuxeo.ecm.core.convert.api.ConversionException;
042import org.nuxeo.ecm.core.convert.extension.Converter;
043import org.nuxeo.ecm.core.convert.extension.ConverterDescriptor;
044import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
045import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
046import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService;
047import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
048import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
049import org.nuxeo.runtime.api.Framework;
050
051/**
052 * Converter to extract a list of equally spaced JPEG thumbnails to represent the story-line of a movie file using the
053 * ffmpeg commandline tool.
054 *
055 * @author ogrisel
056 */
057public class StoryboardConverter extends BaseVideoConverter implements Converter {
058
059    public static final Log log = LogFactory.getLog(StoryboardConverter.class);
060
061    public static final String FFMPEG_INFO_COMMAND = "ffmpeg-info";
062
063    public static final String FFMPEG_SCREENSHOT_RESIZE_COMMAND = "ffmpeg-screenshot-resize";
064
065    public static final String WIDTH_PARAM = "width";
066
067    public static final String HEIGHT_PARAM = "height";
068
069    public static final String THUMBNAIL_NUMBER_PARAM = "thumbnail_number";
070
071    protected Map<String, String> commonParams = new HashMap<>();
072
073    @Override
074    public void init(ConverterDescriptor descriptor) {
075        commonParams = descriptor.getParameters();
076        if (!commonParams.containsKey(WIDTH_PARAM)) {
077            commonParams.put(WIDTH_PARAM, "100");
078        }
079        if (!commonParams.containsKey(HEIGHT_PARAM)) {
080            commonParams.put(HEIGHT_PARAM, "62");
081        }
082    }
083
084    @Override
085    public BlobHolder convert(BlobHolder blobHolder, Map<String, Serializable> parameters) throws ConversionException {
086        // Build the empty output structure
087        Map<String, Serializable> properties = new HashMap<>();
088        List<Blob> blobs = new ArrayList<>();
089        List<Double> timecodes = new ArrayList<>();
090        List<String> comments = new ArrayList<>();
091        properties.put("timecodes", (Serializable) timecodes);
092        properties.put("comments", (Serializable) comments);
093        SimpleBlobHolderWithProperties bh = new SimpleBlobHolderWithProperties(blobs, properties);
094
095        Blob blob = blobHolder.getBlob();
096        try (CloseableFile source = blob.getCloseableFile("." + FilenameUtils.getExtension(blob.getFilename()))) {
097
098            CommandLineExecutorService cles = Framework.getLocalService(CommandLineExecutorService.class);
099            CmdParameters params = cles.getDefaultCmdParameters();
100            params.addNamedParameter(INPUT_FILE_PATH_PARAMETER, source.getFile().getAbsolutePath());
101
102            Double duration = (Double) parameters.get("duration");
103            if (duration == null) {
104                log.warn(String.format("Cannot extract storyboard for file '%s'" + " with missing duration info.",
105                        blob.getFilename()));
106                return bh;
107            }
108
109            // add the command line parameters for the storyboard extraction and run it
110            int numberOfThumbnails = getNumberOfThumbnails(parameters);
111            for (int i = 0; i < numberOfThumbnails; i++) {
112                double timecode = BigDecimal.valueOf(i * duration / numberOfThumbnails)
113                                            .setScale(2, RoundingMode.HALF_UP)
114                                            .doubleValue();
115                Blob thumbBlob = Blobs.createBlobWithExtension(".jpeg");
116                params.addNamedParameter(OUTPUT_FILE_PATH_PARAMETER, thumbBlob.getFile().getAbsolutePath());
117                params.addNamedParameter(POSITION_PARAMETER, String.valueOf(timecode));
118                params.addNamedParameter(WIDTH_PARAM, commonParams.get(WIDTH_PARAM));
119                params.addNamedParameter(HEIGHT_PARAM, commonParams.get(HEIGHT_PARAM));
120                ExecResult result = cles.execCommand(FFMPEG_SCREENSHOT_RESIZE_COMMAND, params);
121                if (!result.isSuccessful()) {
122                    throw result.getError();
123                }
124                thumbBlob.setMimeType("image/jpeg");
125                thumbBlob.setFilename(String.format("%.2f-seconds.jpeg", timecode));
126                blobs.add(thumbBlob);
127                timecodes.add(timecode);
128                comments.add(String.format("%s %d", blob.getFilename(), i));
129            }
130            return bh;
131        } catch (IOException | CommandNotAvailable | CommandException e) {
132            String msg;
133            if (blob != null) {
134                msg = "Error extracting story board from '" + blob.getFilename() + "'";
135            } else {
136                msg = "conversion failed";
137            }
138            throw new ConversionException(msg, e);
139        }
140    }
141
142    protected int getNumberOfThumbnails(Map<String, Serializable> parameters) {
143        int numberOfThumbnails = 9;
144        if (parameters.containsKey(THUMBNAIL_NUMBER_PARAM)) {
145            numberOfThumbnails = (int) parameters.get(THUMBNAIL_NUMBER_PARAM);
146        }
147        // param from converter descriptor still overrides the video service configuration to keep compat
148        if (commonParams.containsKey(THUMBNAIL_NUMBER_PARAM)) {
149            numberOfThumbnails = Integer.parseInt(commonParams.get(THUMBNAIL_NUMBER_PARAM));
150        }
151        if (numberOfThumbnails < 1) {
152            numberOfThumbnails = 1;
153        }
154        return numberOfThumbnails;
155    }
156}