/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portlet.documentlibrary.util; import com.liferay.document.library.kernel.exception.NoSuchFileEntryException; import com.liferay.document.library.kernel.model.DLProcessorConstants; import com.liferay.document.library.kernel.util.DLPreviewableProcessor; import com.liferay.document.library.kernel.util.DLUtil; import com.liferay.document.library.kernel.util.VideoProcessor; import com.liferay.exportimport.kernel.lar.PortletDataContext; import com.liferay.portal.fabric.InputResource; import com.liferay.portal.fabric.OutputResource; import com.liferay.portal.kernel.exception.SystemException; import com.liferay.portal.kernel.image.ImageBag; import com.liferay.portal.kernel.image.ImageToolUtil; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.messaging.DestinationNames; import com.liferay.portal.kernel.process.ClassPathUtil; import com.liferay.portal.kernel.process.ProcessCallable; import com.liferay.portal.kernel.process.ProcessChannel; import com.liferay.portal.kernel.process.ProcessException; import com.liferay.portal.kernel.process.ProcessExecutorUtil; import com.liferay.portal.kernel.repository.model.FileEntry; import com.liferay.portal.kernel.repository.model.FileVersion; import com.liferay.portal.kernel.util.FileUtil; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.ServerDetector; import com.liferay.portal.kernel.util.SetUtil; import com.liferay.portal.kernel.util.StreamUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.SystemEnv; import com.liferay.portal.kernel.util.ThreadUtil; import com.liferay.portal.kernel.xml.Element; import com.liferay.portal.kernel.xuggler.XugglerUtil; import com.liferay.portal.log.Log4jLogFactoryImpl; import com.liferay.portal.repository.liferayrepository.model.LiferayFileVersion; import com.liferay.portal.util.PropsUtil; import com.liferay.portal.util.PropsValues; import com.liferay.util.log4j.Log4JUtil; import java.awt.image.RenderedImage; import java.io.File; import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.Vector; import java.util.concurrent.CancellationException; import java.util.concurrent.Future; import org.apache.commons.lang.time.StopWatch; /** * @author Juan González * @author Sergio González * @author Mika Koivisto * @author Ivica Cardic */ public class VideoProcessorImpl extends DLPreviewableProcessor implements VideoProcessor { @Override public void afterPropertiesSet() { boolean valid = true; if ((_PREVIEW_TYPES.length == 0) || (_PREVIEW_TYPES.length > 2)) { valid = false; } else { for (String previewType : _PREVIEW_TYPES) { if (!previewType.equals("mp4") && !previewType.equals("ogv")) { valid = false; break; } } } if (!valid && _log.isWarnEnabled()) { StringBundler sb = new StringBundler(5); sb.append("Liferay is incorrectly configured to generate video "); sb.append("previews using video containers other than MP4 or "); sb.append("OGV. Please change the property "); sb.append(PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_CONTAINERS); sb.append(" in portal-ext.properties."); _log.warn(sb.toString()); } FileUtil.mkdirs(PREVIEW_TMP_PATH); FileUtil.mkdirs(THUMBNAIL_TMP_PATH); } @Override public void generateVideo( FileVersion sourceFileVersion, FileVersion destinationFileVersion) throws Exception { _generateVideo(sourceFileVersion, destinationFileVersion); } @Override public InputStream getPreviewAsStream(FileVersion fileVersion, String type) throws Exception { return doGetPreviewAsStream(fileVersion, type); } @Override public long getPreviewFileSize(FileVersion fileVersion, String type) throws Exception { return doGetPreviewFileSize(fileVersion, type); } @Override public InputStream getThumbnailAsStream(FileVersion fileVersion, int index) throws Exception { return doGetThumbnailAsStream(fileVersion, index); } @Override public long getThumbnailFileSize(FileVersion fileVersion, int index) throws Exception { return doGetThumbnailFileSize(fileVersion, index); } @Override public String getType() { return DLProcessorConstants.VIDEO_PROCESSOR; } @Override public Set<String> getVideoMimeTypes() { return _videoMimeTypes; } @Override public boolean hasVideo(FileVersion fileVersion) { boolean hasVideo = false; try { hasVideo = _hasVideo(fileVersion); if (!hasVideo && isSupported(fileVersion)) { _queueGeneration(null, fileVersion); } } catch (Exception e) { _log.error(e, e); } return hasVideo; } @Override public boolean isSupported(String mimeType) { if (_videoMimeTypes.contains(mimeType) && XugglerUtil.isEnabled()) { return true; } return false; } @Override public boolean isVideoSupported(FileVersion fileVersion) { return isSupported(fileVersion); } @Override public boolean isVideoSupported(String mimeType) { return isSupported(mimeType); } @Override public void trigger( FileVersion sourceFileVersion, FileVersion destinationFileVersion) { super.trigger(sourceFileVersion, destinationFileVersion); _queueGeneration(sourceFileVersion, destinationFileVersion); } @Override protected void doExportGeneratedFiles( PortletDataContext portletDataContext, FileEntry fileEntry, Element fileEntryElement) throws Exception { exportThumbnails( portletDataContext, fileEntry, fileEntryElement, "video"); exportPreviews(portletDataContext, fileEntry, fileEntryElement); } @Override protected void doImportGeneratedFiles( PortletDataContext portletDataContext, FileEntry fileEntry, FileEntry importedFileEntry, Element fileEntryElement) throws Exception { importThumbnails( portletDataContext, fileEntry, importedFileEntry, fileEntryElement, "video"); importPreviews( portletDataContext, fileEntry, importedFileEntry, fileEntryElement); } protected void exportPreviews( PortletDataContext portletDataContext, FileEntry fileEntry, Element fileEntryElement) throws Exception { FileVersion fileVersion = fileEntry.getFileVersion(); if (!isSupported(fileVersion) || !hasPreviews(fileVersion)) { return; } if (!portletDataContext.isPerformDirectBinaryImport()) { if ((_PREVIEW_TYPES.length == 0) || (_PREVIEW_TYPES.length > 2)) { return; } for (String previewType : _PREVIEW_TYPES) { if (previewType.equals("mp4") || previewType.equals("ogv")) { exportPreview( portletDataContext, fileEntry, fileEntryElement, "video", previewType); } } } } @Override protected List<Long> getFileVersionIds() { return _fileVersionIds; } @Override protected String getPreviewType(FileVersion fileVersion) { return _PREVIEW_TYPES[0]; } @Override protected String[] getPreviewTypes() { return _PREVIEW_TYPES; } @Override protected String getThumbnailType(FileVersion fileVersion) { return THUMBNAIL_TYPE; } protected void importPreviews( PortletDataContext portletDataContext, FileEntry fileEntry, FileEntry importedFileEntry, Element fileEntryElement) throws Exception { if ((_PREVIEW_TYPES.length == 0) || (_PREVIEW_TYPES.length > 2)) { return; } for (String previewType : _PREVIEW_TYPES) { if (previewType.equals("mp4") || previewType.equals("ogv")) { importPreview( portletDataContext, fileEntry, importedFileEntry, fileEntryElement, "video", previewType); } } } @Override protected void storeThumbnailImages(FileVersion fileVersion, File file) throws Exception { if (!hasThumbnail(fileVersion, THUMBNAIL_INDEX_DEFAULT)) { addFileToStore( fileVersion.getCompanyId(), THUMBNAIL_PATH, getThumbnailFilePath(fileVersion, THUMBNAIL_INDEX_DEFAULT), file); } if (isThumbnailEnabled(THUMBNAIL_INDEX_CUSTOM_1) || isThumbnailEnabled(THUMBNAIL_INDEX_CUSTOM_2)) { ImageBag imageBag = ImageToolUtil.read(file); RenderedImage renderedImage = imageBag.getRenderedImage(); storeThumbnailImage( fileVersion, renderedImage, THUMBNAIL_INDEX_CUSTOM_1); storeThumbnailImage( fileVersion, renderedImage, THUMBNAIL_INDEX_CUSTOM_2); } } private static void _destroyHangingThread() { for (Thread thread : ThreadUtil.getThreads()) { if ((thread != null) && !thread.isDaemon() && !StringUtil.equalsIgnoreCase(thread.getName(), "main")) { System.exit(-1); } } } private void _generateThumbnailXuggler( FileVersion fileVersion, File file, int height, int width) throws Exception { StopWatch stopWatch = new StopWatch(); stopWatch.start(); String tempFileId = DLUtil.getTempFileId( fileVersion.getFileEntryId(), fileVersion.getVersion()); File thumbnailTempFile = getThumbnailTempFile(tempFileId); try { try { if (PropsValues.DL_FILE_ENTRY_PREVIEW_FORK_PROCESS_ENABLED) { ProcessCallable<String> processCallable = new LiferayVideoThumbnailProcessCallable( ServerDetector.getServerId(), PropsUtil.get(PropsKeys.LIFERAY_HOME), Log4JUtil.getCustomLogSettings(), file, thumbnailTempFile, THUMBNAIL_TYPE, height, width, PropsValues. DL_FILE_ENTRY_THUMBNAIL_VIDEO_FRAME_PERCENTAGE); ProcessChannel<String> processChannel = ProcessExecutorUtil.execute( ClassPathUtil.getPortalProcessConfig(), processCallable); Future<String> future = processChannel.getProcessNoticeableFuture(); String processIdentity = String.valueOf( fileVersion.getFileVersionId()); futures.put(processIdentity, future); future.get(); } else { LiferayConverter liferayConverter = new LiferayVideoThumbnailConverter( file.getCanonicalPath(), thumbnailTempFile, THUMBNAIL_TYPE, height, width, PropsValues. DL_FILE_ENTRY_THUMBNAIL_VIDEO_FRAME_PERCENTAGE); liferayConverter.convert(); } } catch (CancellationException ce) { if (_log.isInfoEnabled()) { _log.info( "Cancellation received for " + fileVersion.getFileVersionId() + " " + fileVersion.getTitle()); } } catch (Exception e) { _log.error(e, e); } storeThumbnailImages(fileVersion, thumbnailTempFile); if (_log.isInfoEnabled()) { _log.info( "Xuggler generated a thumbnail for " + fileVersion.getTitle() + " in " + stopWatch.getTime() + " ms"); } } catch (Exception e) { throw new SystemException(e); } finally { FileUtil.delete(thumbnailTempFile); } } private void _generateVideo( FileVersion sourceFileVersion, FileVersion destinationFileVersion) throws Exception { if (!XugglerUtil.isEnabled() || _hasVideo(destinationFileVersion)) { return; } InputStream inputStream = null; File[] previewTempFiles = new File[_PREVIEW_TYPES.length]; File videoTempFile = null; try { if (sourceFileVersion != null) { copy(sourceFileVersion, destinationFileVersion); return; } File file = null; if (!hasPreviews(destinationFileVersion) || !hasThumbnails(destinationFileVersion)) { if (destinationFileVersion instanceof LiferayFileVersion) { try { LiferayFileVersion liferayFileVersion = (LiferayFileVersion)destinationFileVersion; file = liferayFileVersion.getFile(false); } catch (UnsupportedOperationException uoe) { } } if (file == null) { inputStream = destinationFileVersion.getContentStream( false); videoTempFile = FileUtil.createTempFile( destinationFileVersion.getExtension()); FileUtil.write(videoTempFile, inputStream); file = videoTempFile; } } if (!hasPreviews(destinationFileVersion)) { String tempFileId = DLUtil.getTempFileId( destinationFileVersion.getFileEntryId(), destinationFileVersion.getVersion()); for (int i = 0; i < _PREVIEW_TYPES.length; i++) { previewTempFiles[i] = getPreviewTempFile( tempFileId, _PREVIEW_TYPES[i]); } try { _generateVideoXuggler( destinationFileVersion, file, previewTempFiles); } catch (Exception e) { _log.error(e, e); } } if (!hasThumbnails(destinationFileVersion)) { try { _generateThumbnailXuggler( destinationFileVersion, file, PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_HEIGHT, PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_WIDTH); } catch (Exception e) { _log.error(e, e); } } } catch (NoSuchFileEntryException nsfee) { if (_log.isDebugEnabled()) { _log.debug(nsfee, nsfee); } } finally { StreamUtil.cleanUp(inputStream); _fileVersionIds.remove(destinationFileVersion.getFileVersionId()); for (int i = 0; i < previewTempFiles.length; i++) { FileUtil.delete(previewTempFiles[i]); } FileUtil.delete(videoTempFile); } } private void _generateVideoXuggler( FileVersion fileVersion, File sourceFile, File destinationFile, String containerType) throws Exception { if (hasPreview(fileVersion, containerType)) { return; } StopWatch stopWatch = new StopWatch(); stopWatch.start(); try { if (PropsValues.DL_FILE_ENTRY_PREVIEW_FORK_PROCESS_ENABLED) { ProcessCallable<String> processCallable = new LiferayVideoProcessCallable( ServerDetector.getServerId(), PropsUtil.get(PropsKeys.LIFERAY_HOME), Log4JUtil.getCustomLogSettings(), sourceFile, destinationFile, containerType, PropsUtil.getProperties( PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO, false), PropsUtil.getProperties( PropsKeys.XUGGLER_FFPRESET, true)); ProcessChannel<String> processChannel = ProcessExecutorUtil.execute( ClassPathUtil.getPortalProcessConfig(), processCallable); Future<String> future = processChannel.getProcessNoticeableFuture(); String processIdentity = String.valueOf( fileVersion.getFileVersionId()); futures.put(processIdentity, future); future.get(); } else { LiferayConverter liferayConverter = new LiferayVideoConverter( sourceFile.getCanonicalPath(), destinationFile.getCanonicalPath(), containerType, PropsUtil.getProperties( PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO, false), PropsUtil.getProperties(PropsKeys.XUGGLER_FFPRESET, true)); liferayConverter.convert(); } } catch (Exception e) { _log.error(e, e); } addFileToStore( fileVersion.getCompanyId(), PREVIEW_PATH, getPreviewFilePath(fileVersion, containerType), destinationFile); if (_log.isInfoEnabled()) { _log.info( "Xuggler generated a " + containerType + " preview video for " + fileVersion.getTitle() + " in " + stopWatch.getTime() + " ms"); } } private void _generateVideoXuggler( FileVersion fileVersion, File sourceFile, File[] destinationFiles) { try { for (int i = 0; i < destinationFiles.length; i++) { _generateVideoXuggler( fileVersion, sourceFile, destinationFiles[i], _PREVIEW_TYPES[i]); } } catch (CancellationException ce) { if (_log.isInfoEnabled()) { _log.info( "Cancellation received for " + fileVersion.getFileVersionId() + " " + fileVersion.getTitle()); } } catch (Exception e) { _log.error(e, e); } } private boolean _hasVideo(FileVersion fileVersion) throws Exception { if (!isSupported(fileVersion)) { return false; } if (hasPreviews(fileVersion) && hasThumbnails(fileVersion)) { return true; } return false; } private void _queueGeneration( FileVersion sourceFileVersion, FileVersion destinationFileVersion) { if (_fileVersionIds.contains( destinationFileVersion.getFileVersionId()) || !isSupported(destinationFileVersion)) { return; } _fileVersionIds.add(destinationFileVersion.getFileVersionId()); sendGenerationMessage( DestinationNames.DOCUMENT_LIBRARY_VIDEO_PROCESSOR, sourceFileVersion, destinationFileVersion); } private static final String[] _PREVIEW_TYPES = PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_CONTAINERS; private static final Log _log = LogFactoryUtil.getLog( VideoProcessorImpl.class); private final List<Long> _fileVersionIds = new Vector<>(); private final Set<String> _videoMimeTypes = SetUtil.fromArray( PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_MIME_TYPES); private static class LiferayVideoProcessCallable implements ProcessCallable<String> { public LiferayVideoProcessCallable( String serverId, String liferayHome, Map<String, String> customLogSettings, File inputFile, File outputFile, String videoContainer, Properties videoProperties, Properties ffpresetProperties) { _serverId = serverId; _liferayHome = liferayHome; _customLogSettings = customLogSettings; _inputFile = inputFile; _outputFile = outputFile; _videoContainer = videoContainer; _videoProperties = videoProperties; _ffpresetProperties = ffpresetProperties; } @Override public String call() throws ProcessException { XugglerAutoInstallHelper.installNativeLibraries(); Properties systemProperties = System.getProperties(); SystemEnv.setProperties(systemProperties); Class<?> clazz = getClass(); ClassLoader classLoader = clazz.getClassLoader(); Log4JUtil.initLog4J( _serverId, _liferayHome, classLoader, new Log4jLogFactoryImpl(), _customLogSettings); try { LiferayConverter liferayConverter = new LiferayVideoConverter( _inputFile.getCanonicalPath(), _outputFile.getCanonicalPath(), _videoContainer, _videoProperties, _ffpresetProperties); liferayConverter.convert(); } catch (Exception e) { throw new ProcessException(e); } finally { _destroyHangingThread(); } return StringPool.BLANK; } private static final long serialVersionUID = 1L; private Map<String, String> _customLogSettings; private final Properties _ffpresetProperties; @InputResource private File _inputFile; private String _liferayHome; @OutputResource private File _outputFile; private String _serverId; private final String _videoContainer; private final Properties _videoProperties; } private static class LiferayVideoThumbnailProcessCallable implements ProcessCallable<String> { public LiferayVideoThumbnailProcessCallable( String serverId, String liferayHome, Map<String, String> customLogSettings, File inputFile, File outputFile, String extension, int height, int width, int percentage) { _serverId = serverId; _liferayHome = liferayHome; _customLogSettings = customLogSettings; _inputFile = inputFile; _outputFile = outputFile; _extension = extension; _height = height; _width = width; _percentage = percentage; } @Override public String call() throws ProcessException { XugglerAutoInstallHelper.installNativeLibraries(); Class<?> clazz = getClass(); ClassLoader classLoader = clazz.getClassLoader(); Properties systemProperties = System.getProperties(); SystemEnv.setProperties(systemProperties); Log4JUtil.initLog4J( _serverId, _liferayHome, classLoader, new Log4jLogFactoryImpl(), _customLogSettings); try { LiferayConverter liferayConverter = new LiferayVideoThumbnailConverter( _inputFile.getCanonicalPath(), _outputFile, _extension, _height, _width, _percentage); liferayConverter.convert(); } catch (Exception e) { throw new ProcessException(e); } finally { _destroyHangingThread(); } return StringPool.BLANK; } private static final long serialVersionUID = 1L; private Map<String, String> _customLogSettings; private final String _extension; private final int _height; @InputResource private File _inputFile; private String _liferayHome; @OutputResource private File _outputFile; private final int _percentage; private String _serverId; private final int _width; } }