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}