001    /**
002     * Copyright (c) 2000-2011 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portlet.documentlibrary.util;
016    
017    import com.liferay.portal.kernel.exception.SystemException;
018    import com.liferay.portal.kernel.log.Log;
019    import com.liferay.portal.kernel.log.LogFactoryUtil;
020    import com.liferay.portal.kernel.messaging.DestinationNames;
021    import com.liferay.portal.kernel.messaging.MessageBusException;
022    import com.liferay.portal.kernel.messaging.MessageBusUtil;
023    import com.liferay.portal.kernel.process.ClassPathUtil;
024    import com.liferay.portal.kernel.process.ProcessCallable;
025    import com.liferay.portal.kernel.process.ProcessException;
026    import com.liferay.portal.kernel.process.ProcessExecutor;
027    import com.liferay.portal.kernel.repository.model.FileVersion;
028    import com.liferay.portal.kernel.util.FileUtil;
029    import com.liferay.portal.kernel.util.PropsKeys;
030    import com.liferay.portal.kernel.util.ServerDetector;
031    import com.liferay.portal.kernel.util.SetUtil;
032    import com.liferay.portal.kernel.util.StringBundler;
033    import com.liferay.portal.kernel.util.StringPool;
034    import com.liferay.portal.kernel.util.Validator;
035    import com.liferay.portal.log.Log4jLogFactoryImpl;
036    import com.liferay.portal.repository.liferayrepository.model.LiferayFileVersion;
037    import com.liferay.portal.util.PrefsPropsUtil;
038    import com.liferay.portal.util.PropsUtil;
039    import com.liferay.portal.util.PropsValues;
040    import com.liferay.portlet.documentlibrary.NoSuchFileEntryException;
041    import com.liferay.portlet.documentlibrary.store.DLStoreUtil;
042    import com.liferay.util.log4j.Log4JUtil;
043    
044    import java.io.File;
045    import java.io.InputStream;
046    
047    import java.util.List;
048    import java.util.Map;
049    import java.util.Properties;
050    import java.util.Set;
051    import java.util.Vector;
052    
053    import org.apache.commons.lang.time.StopWatch;
054    
055    /**
056     * @author Juan González
057     * @author Sergio González
058     * @author Mika Koivisto
059     */
060    public class VideoProcessor extends DefaultPreviewableProcessor {
061    
062            public static final String THUMBNAIL_TYPE = "jpg";
063    
064            public static void generateVideo(FileVersion fileVersion)
065                    throws Exception {
066    
067                    _instance._generateVideo(fileVersion);
068            }
069    
070            public static InputStream getPreviewAsStream(FileVersion fileVersion)
071                    throws Exception {
072    
073                    return _instance.doGetPreviewAsStream(fileVersion);
074            }
075    
076            public static InputStream getPreviewAsStream(
077                            FileVersion fileVersion, String type)
078                    throws Exception {
079    
080                    return _instance.doGetPreviewAsStream(fileVersion, type);
081            }
082    
083            public static long getPreviewFileSize(FileVersion fileVersion)
084                    throws Exception {
085    
086                    return _instance.doGetPreviewFileSize(fileVersion);
087            }
088    
089            public static long getPreviewFileSize(FileVersion fileVersion, String type)
090                    throws Exception {
091    
092                    return _instance.doGetPreviewFileSize(fileVersion, type);
093            }
094    
095            public static InputStream getThumbnailAsStream(FileVersion fileVersion)
096                    throws Exception {
097    
098                    return _instance.doGetThumbnailAsStream(fileVersion);
099            }
100    
101            public static long getThumbnailFileSize(FileVersion fileVersion)
102                    throws Exception {
103    
104                    return _instance.doGetThumbnailFileSize(fileVersion);
105            }
106    
107            public static Set<String> getVideoMimeTypes() {
108                    return _instance._videoMimeTypes;
109            }
110    
111            public static boolean hasVideo(FileVersion fileVersion) {
112                    boolean hasVideo = false;
113    
114                    try {
115                            hasVideo = _instance._hasVideo(fileVersion);
116    
117                            if (!hasVideo && _instance.isSupported(fileVersion)) {
118                                    _instance._queueGeneration(fileVersion);
119                            }
120                    }
121                    catch (Exception e) {
122                            _log.error(e, e);
123                    }
124    
125                    return hasVideo;
126            }
127    
128            public static boolean isVideoSupported(FileVersion fileVersion) {
129                    return _instance.isSupported(fileVersion);
130            }
131    
132            public static boolean isVideoSupported(String mimeType) {
133                    return _instance.isSupported(mimeType);
134            }
135    
136            public VideoProcessor() {
137                    boolean valid = true;
138    
139                    if ((_PREVIEW_TYPES.length == 0) || (_PREVIEW_TYPES.length > 2)) {
140                            valid = false;
141                    }
142                    else {
143                            for (String previewType : _PREVIEW_TYPES) {
144                                    if (!previewType.equals("mp4") && !previewType.equals("ogv")) {
145                                            valid = false;
146    
147                                            break;
148                                    }
149                            }
150                    }
151    
152                    if (!valid && _log.isWarnEnabled()) {
153                            StringBundler sb = new StringBundler(5);
154    
155                            sb.append("Liferay is incorrectly configured to generate video ");
156                            sb.append("previews using video containers other than MP4 or ");
157                            sb.append("OGV. Please change the property ");
158                            sb.append(PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_CONTAINERS);
159                            sb.append(" in portal-ext.properties.");
160    
161                            _log.warn(sb.toString());
162                    }
163    
164                    FileUtil.mkdirs(PREVIEW_TMP_PATH);
165                    FileUtil.mkdirs(THUMBNAIL_TMP_PATH);
166            }
167    
168            public boolean isSupported(String mimeType) {
169                    if (Validator.isNull(mimeType)) {
170                            return false;
171                    }
172    
173                    try {
174                            if (PrefsPropsUtil.getBoolean(
175                                            PropsKeys.XUGGLER_ENABLED, PropsValues.XUGGLER_ENABLED)) {
176    
177                                    return _videoMimeTypes.contains(mimeType);
178                            }
179                    }
180                    catch (Exception e) {
181                    }
182    
183                    return false;
184            }
185    
186            public void trigger(FileVersion fileVersion) {
187                    _instance._queueGeneration(fileVersion);
188            }
189    
190            @Override
191            protected String getPreviewType() {
192                    return _PREVIEW_TYPES[0];
193            }
194    
195            @Override
196            protected String[] getPreviewTypes() {
197                    return _PREVIEW_TYPES;
198            }
199    
200            @Override
201            protected String getThumbnailType() {
202                    return THUMBNAIL_TYPE;
203            }
204    
205            private void _generateThumbnailXuggler(
206                            FileVersion fileVersion, File file, int height, int width)
207                    throws Exception {
208    
209                    StopWatch stopWatch = null;
210    
211                    if (_log.isInfoEnabled()) {
212                            stopWatch = new StopWatch();
213    
214                            stopWatch.start();
215                    }
216    
217                    String tempFileId = DLUtil.getTempFileId(
218                            fileVersion.getFileEntryId(), fileVersion.getVersion());
219    
220                    File thumbnailTempFile = getThumbnailTempFile(tempFileId);
221    
222                    try {
223                            try {
224                                    ProcessCallable<String> processCallable =
225                                            new LiferayVideoThumbnailProcessCallable(
226                                                    ServerDetector.getServerId(),
227                                                    PropsUtil.get(PropsKeys.LIFERAY_HOME),
228                                                    Log4JUtil.getCustomLogSettings(),
229                                                    file.getCanonicalPath(),
230                                                    thumbnailTempFile, THUMBNAIL_TYPE, height, width,
231                                                    PropsValues.
232                                                            DL_FILE_ENTRY_THUMBNAIL_VIDEO_FRAME_PERCENTAGE);
233    
234                                    ProcessExecutor.execute(
235                                            processCallable, ClassPathUtil.getPortalClassPath());
236                            }
237                            catch (Exception e) {
238                                    _log.error(e, e);
239                            }
240    
241                            addFileToStore(
242                                    fileVersion.getCompanyId(), THUMBNAIL_PATH,
243                                    getThumbnailFilePath(fileVersion), thumbnailTempFile);
244    
245                            if (_log.isInfoEnabled()) {
246                                    _log.info(
247                                            "Xuggler generated a thumbnail for " +
248                                                    fileVersion.getTitle() + " in " + stopWatch);
249                            }
250                    }
251                    catch (Exception e) {
252                            throw new SystemException(e);
253                    }
254                    finally {
255                            FileUtil.delete(thumbnailTempFile);
256                    }
257            }
258    
259            private void _generateVideo(FileVersion fileVersion) throws Exception {
260                    String tempFileId = DLUtil.getTempFileId(
261                            fileVersion.getFileEntryId(), fileVersion.getVersion());
262    
263                    File videoTempFile = _getVideoTempFile(
264                            tempFileId, fileVersion.getExtension());
265    
266                    File[] previewTempFiles = new File[_PREVIEW_TYPES.length];
267    
268                    for (int i = 0; i < _PREVIEW_TYPES.length; i++) {
269                            previewTempFiles[i] = getPreviewTempFile(
270                                    tempFileId, _PREVIEW_TYPES[i]);
271                    }
272    
273                    try {
274                            if (!PrefsPropsUtil.getBoolean(
275                                            PropsKeys.XUGGLER_ENABLED, PropsValues.XUGGLER_ENABLED) ||
276                                    _hasVideo(fileVersion)) {
277    
278                                    return;
279                            }
280    
281                            File file = null;
282    
283                            if (_isGeneratePreview(fileVersion) ||
284                                    _isGenerateThumbnail(fileVersion)) {
285    
286                                    if (fileVersion instanceof LiferayFileVersion) {
287                                            try {
288                                                    LiferayFileVersion liferayFileVersion =
289                                                            (LiferayFileVersion)fileVersion;
290    
291                                                    file = liferayFileVersion.getFile(false);
292                                            }
293                                            catch (UnsupportedOperationException uoe) {
294                                            }
295                                    }
296    
297                                    if (file == null) {
298                                            InputStream inputStream = fileVersion.getContentStream(
299                                                    false);
300    
301                                            FileUtil.write(videoTempFile, inputStream);
302    
303                                            file = videoTempFile;
304                                    }
305                            }
306    
307                            if (_isGeneratePreview(fileVersion)) {
308                                    try {
309                                            _generateVideoXuggler(
310                                                    fileVersion, file, previewTempFiles,
311                                                    PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_HEIGHT,
312                                                    PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_WIDTH);
313                                    }
314                                    catch (Exception e) {
315                                            _log.error(e, e);
316                                    }
317                            }
318    
319                            if (_isGenerateThumbnail(fileVersion)) {
320                                    try {
321                                            _generateThumbnailXuggler(
322                                                    fileVersion, file,
323                                                    PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_HEIGHT,
324                                                    PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_WIDTH);
325                                    }
326                                    catch (Exception e) {
327                                            _log.error(e, e);
328                                    }
329                            }
330                    }
331                    catch (NoSuchFileEntryException nsfee) {
332                    }
333                    finally {
334                            _fileVersionIds.remove(fileVersion.getFileVersionId());
335    
336                            for (int i = 0; i < previewTempFiles.length; i++) {
337                                    FileUtil.delete(previewTempFiles[i]);
338                            }
339    
340                            FileUtil.delete(videoTempFile);
341                    }
342            }
343    
344            private void _generateVideoXuggler(
345                            FileVersion fileVersion, File srcFile, File destFile,
346                            String containerType)
347                    throws Exception {
348    
349                    if (!_isGeneratePreview(fileVersion, containerType)) {
350                            return;
351                    }
352    
353                    StopWatch stopWatch = null;
354    
355                    if (_log.isInfoEnabled()) {
356                            stopWatch = new StopWatch();
357    
358                            stopWatch.start();
359                    }
360    
361                    ProcessCallable<String> processCallable =
362                            new LiferayVideoProcessCallable(
363                                    ServerDetector.getServerId(),
364                                    PropsUtil.get(PropsKeys.LIFERAY_HOME),
365                                    Log4JUtil.getCustomLogSettings(), srcFile.getCanonicalPath(),
366                                    destFile.getCanonicalPath(), FileUtil.createTempFileName(),
367                                    PropsUtil.getProperties(PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO,
368                                            false),
369                                    PropsUtil.getProperties(PropsKeys.XUGGLER_FFPRESET, true));
370    
371                    ProcessExecutor.execute(
372                            processCallable, ClassPathUtil.getPortalClassPath());
373    
374                    addFileToStore(
375                            fileVersion.getCompanyId(), PREVIEW_PATH,
376                            getPreviewFilePath(fileVersion, containerType), destFile);
377    
378                    if (_log.isInfoEnabled()) {
379                            _log.info(
380                                    "Xuggler generated a " + containerType + " preview video for " +
381                                            fileVersion.getTitle() + " in " + stopWatch);
382                    }
383            }
384    
385            private void _generateVideoXuggler(
386                    FileVersion fileVersion, File srcFile, File[] destFiles, int height,
387                    int width) {
388    
389                    try {
390                            for (int i = 0; i < destFiles.length; i++) {
391                                    _generateVideoXuggler(
392                                            fileVersion, srcFile, destFiles[i], _PREVIEW_TYPES[i]);
393                            }
394                    }
395                    catch (Exception e) {
396                            _log.error(e, e);
397                    }
398            }
399    
400            private File _getVideoTempFile(String tempFileId, String targetExtension) {
401                    String videoTempFilePath = _getVideoTempFilePath(
402                            tempFileId, targetExtension);
403    
404                    return new File(videoTempFilePath);
405            }
406    
407            private String _getVideoTempFilePath(
408                    String tempFileId, String targetExtension) {
409    
410                    StringBundler sb = new StringBundler(5);
411    
412                    sb.append(PREVIEW_TMP_PATH);
413                    sb.append(tempFileId);
414    
415                    for (int i = 0; i < _PREVIEW_TYPES.length; i++) {
416                            if (_PREVIEW_TYPES[i].equals(targetExtension)) {
417                                    sb.append("_tmp");
418    
419                                    break;
420                            }
421                    }
422    
423                    sb.append(StringPool.PERIOD);
424                    sb.append(targetExtension);
425    
426                    return sb.toString();
427            }
428    
429            private boolean _hasVideo(FileVersion fileVersion) throws Exception {
430                    if (!isSupported(fileVersion)) {
431                            return false;
432                    }
433    
434                    int previewsCount = 0;
435    
436                    for (int i = 0; i < _PREVIEW_TYPES.length; i++) {
437                            String previewFilePath = getPreviewFilePath(
438                                    fileVersion, _PREVIEW_TYPES[i]);
439    
440                            if (DLStoreUtil.hasFile(
441                                            fileVersion.getCompanyId(), REPOSITORY_ID,
442                                            previewFilePath)) {
443    
444                                    previewsCount++;
445                            }
446                    }
447    
448                    boolean previewExists = false;
449    
450                    if (previewsCount == _PREVIEW_TYPES.length) {
451                            previewExists = true;
452                    }
453    
454                    boolean thumbnailExists = DLStoreUtil.hasFile(
455                            fileVersion.getCompanyId(), REPOSITORY_ID,
456                            getThumbnailFilePath(fileVersion));
457    
458                    if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED &&
459                            PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED) {
460    
461                            if (previewExists && thumbnailExists) {
462                                    return true;
463                            }
464                    }
465                    else if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED && previewExists) {
466                            return true;
467                    }
468                    else if (PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED &&
469                                     thumbnailExists) {
470    
471                            return true;
472                    }
473    
474                    return false;
475            }
476    
477            private boolean _isGeneratePreview(FileVersion fileVersion)
478                    throws Exception {
479    
480                    int contPreviewsCreated = 0;
481    
482                    for (int i = 0; i < _PREVIEW_TYPES.length; i++) {
483                            if (!_isGeneratePreview(fileVersion, _PREVIEW_TYPES[i])) {
484                                    contPreviewsCreated++;
485                            }
486                    }
487    
488                    if (contPreviewsCreated < _PREVIEW_TYPES.length) {
489                            return true;
490                    }
491                    else {
492                            return false;
493                    }
494            }
495    
496            private boolean _isGeneratePreview(
497                            FileVersion fileVersion, String previewType)
498                    throws Exception {
499    
500                    String previewFilePath = getPreviewFilePath(fileVersion, previewType);
501    
502                    if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED &&
503                            !DLStoreUtil.hasFile(
504                                    fileVersion.getCompanyId(), REPOSITORY_ID, previewFilePath)) {
505    
506                            return true;
507                    }
508    
509                    return false;
510            }
511    
512            private boolean _isGenerateThumbnail(FileVersion fileVersion)
513                    throws Exception {
514    
515                    String thumbnailFilePath = getThumbnailFilePath(fileVersion);
516    
517                    if (PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED &&
518                            !DLStoreUtil.hasFile(
519                                    fileVersion.getCompanyId(), REPOSITORY_ID, thumbnailFilePath)) {
520    
521                            return true;
522                    }
523                    else {
524                            return false;
525                    }
526            }
527    
528            private void _queueGeneration(FileVersion fileVersion) {
529                    if (_fileVersionIds.contains(fileVersion.getFileVersionId()) ||
530                            !isSupported(fileVersion)) {
531    
532                            return;
533                    }
534    
535                    _fileVersionIds.add(fileVersion.getFileVersionId());
536    
537                    if (PropsValues.DL_FILE_ENTRY_PROCESSORS_TRIGGER_SYNCHRONOUSLY) {
538                            try {
539                                    MessageBusUtil.sendSynchronousMessage(
540                                            DestinationNames.DOCUMENT_LIBRARY_VIDEO_PROCESSOR,
541                                            fileVersion);
542                            }
543                            catch (MessageBusException mbe) {
544                                    if (_log.isWarnEnabled()) {
545                                            _log.warn(mbe, mbe);
546                                    }
547                            }
548                    }
549                    else {
550                            MessageBusUtil.sendMessage(
551                                    DestinationNames.DOCUMENT_LIBRARY_VIDEO_PROCESSOR, fileVersion);
552                    }
553            }
554    
555            private static final String[] _PREVIEW_TYPES =
556                    PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_CONTAINERS;
557    
558            private static Log _log = LogFactoryUtil.getLog(VideoProcessor.class);
559    
560            private static VideoProcessor _instance = new VideoProcessor();
561    
562            private Set<String> _videoMimeTypes = SetUtil.fromArray(
563                    PropsValues.DL_FILE_ENTRY_PREVIEW_VIDEO_MIME_TYPES);
564            private List<Long> _fileVersionIds = new Vector<Long>();
565    
566            private static class LiferayVideoProcessCallable
567                    implements ProcessCallable<String> {
568    
569                    public LiferayVideoProcessCallable(
570                            String serverId, String liferayHome,
571                            Map<String, String> customLogSettings, String inputURL,
572                            String outputURL, String tempFileName, Properties videoProperties,
573                            Properties ffpresetProperties) {
574    
575                            _serverId = serverId;
576                            _liferayHome = liferayHome;
577                            _customLogSettings = customLogSettings;
578                            _inputURL = inputURL;
579                            _outputURL = outputURL;
580                            _tempFileName = tempFileName;
581                            _videoProperties = videoProperties;
582                            _ffpresetProperties = ffpresetProperties;
583                    }
584    
585                    public String call() throws ProcessException {
586                            Class<?> clazz = getClass();
587    
588                            ClassLoader classLoader = clazz.getClassLoader();
589    
590                            Log4JUtil.initLog4J(
591                                    _serverId, _liferayHome, classLoader, new Log4jLogFactoryImpl(),
592                                    _customLogSettings);
593    
594                            try {
595                                    LiferayConverter liferayConverter = new LiferayVideoConverter(
596                                            _inputURL, _outputURL, _tempFileName, _videoProperties,
597                                            _ffpresetProperties);
598    
599                                    liferayConverter.convert();
600                            }
601                            catch (Exception e) {
602                                    throw new ProcessException(e);
603                            }
604    
605                            return StringPool.BLANK;
606                    }
607    
608                    private Map<String, String> _customLogSettings;
609                    private Properties _ffpresetProperties;
610                    private String _inputURL;
611                    private String _liferayHome;
612                    private String _outputURL;
613                    private String _serverId;
614                    private String _tempFileName;
615                    private Properties _videoProperties;
616    
617            }
618    
619            private static class LiferayVideoThumbnailProcessCallable
620                    implements ProcessCallable<String> {
621    
622                    public LiferayVideoThumbnailProcessCallable(
623                            String serverId, String liferayHome,
624                            Map<String, String> customLogSettings, String inputURL,
625                            File outputFile, String extension, int height, int width,
626                            int percentage) {
627    
628                            _serverId = serverId;
629                            _liferayHome = liferayHome;
630                            _customLogSettings = customLogSettings;
631                            _inputURL = inputURL;
632                            _outputFile = outputFile;
633                            _extension = extension;
634                            _height = height;
635                            _width = width;
636                            _percentage = percentage;
637                    }
638    
639                    public String call() throws ProcessException {
640                            Class<?> clazz = getClass();
641    
642                            ClassLoader classLoader = clazz.getClassLoader();
643    
644                            Log4JUtil.initLog4J(
645                                    _serverId, _liferayHome, classLoader, new Log4jLogFactoryImpl(),
646                                    _customLogSettings);
647    
648                            try {
649                                    LiferayConverter liferayConverter =
650                                            new LiferayVideoThumbnailConverter(
651                                                    _inputURL, _outputFile, _extension, _height, _width,
652                                                    _percentage);
653    
654                                    liferayConverter.convert();
655                            }
656                            catch (Exception e) {
657                                    throw new ProcessException(e);
658                            }
659    
660                            return StringPool.BLANK;
661                    }
662    
663                    private Map<String, String> _customLogSettings;
664                    private String _extension;
665                    private int _height;
666                    private String _inputURL;
667                    private String _liferayHome;
668                    private File _outputFile;
669                    private int _percentage;
670                    private String _serverId;
671                    private int _width;
672    
673            }
674    
675    }