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 */ 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.Locale; 034import java.util.Map; 035 036import org.apache.commons.io.FilenameUtils; 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.nuxeo.ecm.core.api.Blob; 040import org.nuxeo.ecm.core.api.Blobs; 041import org.nuxeo.ecm.core.api.CloseableFile; 042import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 043import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolderWithProperties; 044import org.nuxeo.ecm.core.convert.api.ConversionException; 045import org.nuxeo.ecm.core.convert.extension.Converter; 046import org.nuxeo.ecm.core.convert.extension.ConverterDescriptor; 047import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters; 048import org.nuxeo.ecm.platform.commandline.executor.api.CommandException; 049import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService; 050import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable; 051import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult; 052import org.nuxeo.runtime.api.Framework; 053 054/** 055 * Converter to extract a list of equally spaced JPEG thumbnails to represent the story-line of a movie file using the 056 * ffmpeg commandline tool. 057 * 058 * @author ogrisel 059 */ 060public class StoryboardConverter implements Converter { 061 062 public static final Log log = LogFactory.getLog(StoryboardConverter.class); 063 064 public static final String FFMPEG_INFO_COMMAND = "ffmpeg-info"; 065 066 public static final String FFMPEG_SCREENSHOT_RESIZE_COMMAND = "ffmpeg-screenshot-resize"; 067 068 public static final String WIDTH_PARAM = "width"; 069 070 public static final String HEIGHT_PARAM = "height"; 071 072 public static final String THUMBNAIL_NUMBER_PARAM = "thumbnail_number"; 073 074 protected Map<String, String> commonParams = new HashMap<>(); 075 076 @Override 077 public void init(ConverterDescriptor descriptor) { 078 commonParams = descriptor.getParameters(); 079 if (!commonParams.containsKey(WIDTH_PARAM)) { 080 commonParams.put(WIDTH_PARAM, "130"); 081 } 082 if (!commonParams.containsKey(HEIGHT_PARAM)) { 083 commonParams.put(HEIGHT_PARAM, "80"); 084 } 085 } 086 087 @Override 088 public BlobHolder convert(BlobHolder blobHolder, Map<String, Serializable> parameters) throws ConversionException { 089 // Build the empty output structure 090 Map<String, Serializable> properties = new HashMap<>(); 091 List<Blob> blobs = new ArrayList<>(); 092 List<Double> timecodes = new ArrayList<>(); 093 List<String> comments = new ArrayList<>(); 094 properties.put("timecodes", (Serializable) timecodes); 095 properties.put("comments", (Serializable) comments); 096 SimpleBlobHolderWithProperties bh = new SimpleBlobHolderWithProperties(blobs, properties); 097 098 Blob blob = blobHolder.getBlob(); 099 try (CloseableFile source = blob.getCloseableFile("." + FilenameUtils.getExtension(blob.getFilename()))) { 100 101 CommandLineExecutorService cles = Framework.getService(CommandLineExecutorService.class); 102 CmdParameters params = cles.getDefaultCmdParameters(); 103 params.addNamedParameter(INPUT_FILE_PATH_PARAMETER, source.getFile().getAbsolutePath()); 104 105 Double duration = (Double) parameters.get("duration"); 106 if (duration == null) { 107 log.warn(String.format("Cannot extract storyboard for file '%s'" + " with missing duration info.", 108 blob.getFilename())); 109 return bh; 110 } 111 112 // add the command line parameters for the storyboard extraction and run it 113 int numberOfThumbnails = getNumberOfThumbnails(parameters); 114 for (int i = 0; i < numberOfThumbnails; i++) { 115 double timecode = BigDecimal.valueOf(i * duration / numberOfThumbnails) 116 .setScale(2, RoundingMode.HALF_UP) 117 .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}