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}