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}