001    /**
002     * Copyright (c) 2000-2012 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.log.Log;
018    import com.liferay.portal.kernel.log.LogFactoryUtil;
019    import com.liferay.portal.kernel.util.GetterUtil;
020    import com.liferay.portal.kernel.util.PropsKeys;
021    import com.liferay.portal.kernel.util.StringPool;
022    import com.liferay.portal.kernel.util.StringUtil;
023    
024    import com.xuggle.xuggler.Configuration;
025    import com.xuggle.xuggler.IAudioResampler;
026    import com.xuggle.xuggler.IAudioSamples;
027    import com.xuggle.xuggler.ICodec;
028    import com.xuggle.xuggler.IContainer;
029    import com.xuggle.xuggler.IContainerFormat;
030    import com.xuggle.xuggler.IPacket;
031    import com.xuggle.xuggler.IPixelFormat.Type;
032    import com.xuggle.xuggler.IRational;
033    import com.xuggle.xuggler.IStream;
034    import com.xuggle.xuggler.IStreamCoder;
035    import com.xuggle.xuggler.IVideoPicture;
036    import com.xuggle.xuggler.IVideoResampler;
037    
038    import java.io.File;
039    
040    import java.util.HashMap;
041    import java.util.Map;
042    import java.util.Properties;
043    
044    /**
045     * @author Juan González
046     * @author Sergio González
047     * @author Brian Wing Shun Chan
048     * @author Alexander Chow
049     */
050    public class LiferayVideoConverter extends LiferayConverter {
051    
052            public LiferayVideoConverter(
053                    String inputURL, String outputURL, String tempFileName,
054                    Properties videoProperties, Properties ffpresetProperties) {
055    
056                    _inputURL = inputURL;
057                    _outputURL = outputURL;
058                    _tempFileName = tempFileName;
059                    _ffpresetProperties = ffpresetProperties;
060    
061                    _height = GetterUtil.getInteger(
062                            videoProperties.getProperty(
063                                    PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_HEIGHT), _height);
064                    _width = GetterUtil.getInteger(
065                            videoProperties.getProperty(
066                                    PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_WIDTH), _width);
067                    _previewVideoContainers = StringUtil.split(
068                            videoProperties.getProperty(
069                                    PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_CONTAINERS));
070    
071                    initVideoBitRateMap(videoProperties);
072                    initVideoFrameRateMap(videoProperties);
073            }
074    
075            @Override
076            public void convert() throws Exception {
077                    try {
078                            doConvert();
079                    }
080                    finally {
081                            if (_inputIContainer.isOpened()) {
082                                    _inputIContainer.close();
083                            }
084    
085                            if (_outputIContainer.isOpened()) {
086                                    _outputIContainer.close();
087                            }
088                    }
089    
090                    createMP4FastStart();
091            }
092    
093            protected void createMP4FastStart() {
094                    File videoFile = new File(_outputURL);
095    
096                    if (_outputVideoFormat.equals("mp4") && videoFile.exists()) {
097                            File tempFile = new File(_tempFileName);
098    
099                            try {
100                                    JQTFastStart.convert(videoFile, tempFile);
101    
102                                    if (tempFile.exists() && tempFile.length() > 0) {
103                                            videoFile.delete();
104    
105                                            tempFile.renameTo(videoFile);
106                                    }
107                            }
108                            catch (Exception e) {
109                                    if (_log.isWarnEnabled()) {
110                                            _log.warn("Unable to move MOOV atom to front of MP4 file");
111                                    }
112                            }
113                            finally {
114                                    tempFile.delete();
115                            }
116                    }
117            }
118    
119            protected void doConvert() throws Exception {
120                    _inputIContainer = IContainer.make();
121                    _outputIContainer = IContainer.make();
122    
123                    openContainer(_inputIContainer, _inputURL, false);
124                    openContainer(_outputIContainer, _outputURL, true);
125    
126                    int inputStreamsCount = _inputIContainer.getNumStreams();
127    
128                    if (inputStreamsCount < 0) {
129                            throw new RuntimeException("Input URL does not have any streams");
130                    }
131    
132                    IContainerFormat iContainerFormat =
133                            _outputIContainer.getContainerFormat();
134    
135                    _outputVideoFormat = iContainerFormat.getOutputFormatShortName();
136    
137                    IAudioResampler[] iAudioResamplers =
138                            new IAudioResampler[inputStreamsCount];
139                    IVideoResampler[] iVideoResamplers =
140                            new IVideoResampler[inputStreamsCount];
141    
142                    IAudioSamples[] inputIAudioSamples =
143                            new IAudioSamples[inputStreamsCount];
144                    IAudioSamples[] outputIAudioSamples =
145                            new IAudioSamples[inputStreamsCount];
146    
147                    IVideoPicture[] inputIVideoPictures =
148                            new IVideoPicture[inputStreamsCount];
149                    IVideoPicture[] outputIVideoPictures =
150                            new IVideoPicture[inputStreamsCount];
151    
152                    IStream[] outputIStreams = new IStream[inputStreamsCount];
153    
154                    IStreamCoder[] inputIStreamCoders = new IStreamCoder[inputStreamsCount];
155                    IStreamCoder[] outputIStreamCoders =
156                            new IStreamCoder[inputStreamsCount];
157    
158                    for (int i = 0; i < inputStreamsCount; i++) {
159                            IStream inputIStream = _inputIContainer.getStream(i);
160    
161                            IStreamCoder inputIStreamCoder = inputIStream.getStreamCoder();
162    
163                            inputIStreamCoders[i] = inputIStreamCoder;
164    
165                            ICodec.Type inputICodecType = inputIStreamCoder.getCodecType();
166    
167                            if (inputICodecType == ICodec.Type.CODEC_TYPE_AUDIO) {
168                                    prepareAudio(
169                                            iAudioResamplers, inputIAudioSamples, outputIAudioSamples,
170                                            inputIStreamCoder, outputIStreamCoders, _outputIContainer,
171                                            outputIStreams, inputICodecType, _outputURL, i);
172                            }
173                            else if (inputICodecType == ICodec.Type.CODEC_TYPE_VIDEO) {
174                                    prepareVideo(
175                                            iVideoResamplers, inputIVideoPictures, outputIVideoPictures,
176                                            inputIStreamCoder, outputIStreamCoders, _outputIContainer,
177                                            outputIStreams, inputICodecType, _outputURL, i);
178                            }
179    
180                            openStreamCoder(inputIStreamCoders[i]);
181                            openStreamCoder(outputIStreamCoders[i]);
182                    }
183    
184                    if (_outputIContainer.writeHeader() < 0) {
185                            throw new RuntimeException("Unable to write container header");
186                    }
187    
188                    boolean keyPacketFound = false;
189                    int nonKeyAfterKeyCount = 0;
190                    boolean onlyDecodeKeyPackets = false;
191                    int previousPacketSize = -1;
192    
193                    IPacket inputIPacket = IPacket.make();
194                    IPacket outputIPacket = IPacket.make();
195    
196                    while (_inputIContainer.readNextPacket(inputIPacket) == 0) {
197                            if (_log.isDebugEnabled()) {
198                                    _log.debug("Current packet size " + inputIPacket.getSize());
199                            }
200    
201                            int streamIndex = inputIPacket.getStreamIndex();
202    
203                            IStreamCoder inputIStreamCoder = inputIStreamCoders[streamIndex];
204                            IStreamCoder outputIStreamCoder = outputIStreamCoders[streamIndex];
205    
206                            if (outputIStreamCoder == null) {
207                                    continue;
208                            }
209    
210                            IStream iStream = _inputIContainer.getStream(streamIndex);
211    
212                            long timeStampOffset = getStreamTimeStampOffset(iStream);
213    
214                            if (inputIStreamCoder.getCodecType() ==
215                                            ICodec.Type.CODEC_TYPE_AUDIO) {
216    
217                                    decodeAudio(
218                                            iAudioResamplers[streamIndex],
219                                            inputIAudioSamples[streamIndex],
220                                            outputIAudioSamples[streamIndex], inputIPacket,
221                                            outputIPacket, inputIStreamCoder, outputIStreamCoder,
222                                            _outputIContainer, inputIPacket.getSize(),
223                                            previousPacketSize, streamIndex, timeStampOffset);
224                            }
225                            else if (inputIStreamCoder.getCodecType() ==
226                                                    ICodec.Type.CODEC_TYPE_VIDEO) {
227    
228                                    keyPacketFound = isKeyPacketFound(inputIPacket, keyPacketFound);
229    
230                                    nonKeyAfterKeyCount = countNonKeyAfterKey(
231                                            inputIPacket, keyPacketFound, nonKeyAfterKeyCount);
232    
233                                    if (isStartDecoding(
234                                                    inputIPacket, inputIStreamCoder, keyPacketFound,
235                                                    nonKeyAfterKeyCount, onlyDecodeKeyPackets)) {
236    
237                                            int value = decodeVideo(
238                                                    iVideoResamplers[streamIndex],
239                                                    inputIVideoPictures[streamIndex],
240                                                    outputIVideoPictures[streamIndex], inputIPacket,
241                                                    outputIPacket, inputIStreamCoder, outputIStreamCoder,
242                                                    _outputIContainer, null, null, 0, 0, timeStampOffset);
243    
244                                            if (value <= 0) {
245                                                    if (inputIPacket.isKey()) {
246                                                            throw new RuntimeException(
247                                                                    "Unable to decode video stream " + streamIndex);
248                                                    }
249    
250                                                    onlyDecodeKeyPackets = true;
251    
252                                                    continue;
253                                            }
254                                    }
255                                    else {
256                                            if (_log.isDebugEnabled()) {
257                                                    _log.debug("Do not decode video stream " + streamIndex);
258                                            }
259                                    }
260                            }
261    
262                            previousPacketSize = inputIPacket.getSize();
263                    }
264    
265                    flush(outputIStreamCoders, _outputIContainer);
266    
267                    if (_outputIContainer.writeTrailer() < 0) {
268                            throw new RuntimeException(
269                                    "Unable to write trailer to output file");
270                    }
271    
272                    cleanUp(iAudioResamplers, iVideoResamplers);
273                    cleanUp(inputIAudioSamples, outputIAudioSamples);
274                    cleanUp(inputIVideoPictures, outputIVideoPictures);
275                    cleanUp(inputIStreamCoders, outputIStreamCoders);
276                    cleanUp(inputIPacket, outputIPacket);
277            }
278    
279            @Override
280            protected int getAudioEncodingChannels(
281                    IContainer outputIContainer, int channels) {
282    
283                    IContainerFormat iContainerFormat =
284                            outputIContainer.getContainerFormat();
285    
286                    String outputFormat = iContainerFormat.getOutputFormatShortName();
287    
288                    if (outputFormat.equals("ogg")) {
289                            return 2;
290                    }
291    
292                    return super.getAudioEncodingChannels(outputIContainer, channels);
293            }
294    
295            @Override
296            protected ICodec getAudioEncodingICodec(IContainer outputIContainer) {
297                    IContainerFormat iContainerFormat =
298                            outputIContainer.getContainerFormat();
299    
300                    String outputFormat = iContainerFormat.getOutputFormatShortName();
301    
302                    if (outputFormat.equals("ogg")) {
303                            return ICodec.findEncodingCodec(ICodec.ID.CODEC_ID_VORBIS);
304                    }
305    
306                    return super.getAudioEncodingICodec(outputIContainer);
307            }
308    
309            @Override
310            protected IContainer getInputIContainer() {
311                    return _inputIContainer;
312            }
313    
314            protected void initVideoBitRateMap(Properties videoProperties) {
315                    _videoBitRateMap = new HashMap<String, Integer>();
316    
317                    for (String previewVideoContainer : _previewVideoContainers) {
318                            int videoBitRate = GetterUtil.getInteger(
319                                    videoProperties.getProperty(
320                                            PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_BIT_RATE
321                                                    + "[" + previewVideoContainer + "]"));
322    
323                            if (videoBitRate > _VIDEO_BIT_RATE_MAX) {
324                                    videoBitRate = _VIDEO_BIT_RATE_MAX;
325                            }
326    
327                            if (videoBitRate > 0) {
328                                    _videoBitRateMap.put(previewVideoContainer, videoBitRate);
329    
330                                    if (_log.isInfoEnabled()) {
331                                            _log.info(
332                                                    "Bit rate for " + previewVideoContainer + " set to " +
333                                                            videoBitRate);
334                                    }
335                            }
336                    }
337            }
338    
339            protected void initVideoFrameRateMap(Properties videoProperties) {
340                    _videoFrameRateMap = new HashMap<String, IRational>();
341    
342                    for (String previewVideoContainer : _previewVideoContainers) {
343                            int numerator = GetterUtil.getInteger(
344                                    videoProperties.getProperty(
345                                            PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_FRAME_RATE_NUMERATOR +
346                                                    "[" + previewVideoContainer + "]"));
347                            int denominator = GetterUtil.getInteger(
348                                    videoProperties.getProperty(
349                                            PropsKeys.DL_FILE_ENTRY_PREVIEW_VIDEO_FRAME_RATE_DENOMINATOR
350                                                    + StringPool.OPEN_BRACKET + previewVideoContainer
351                                                            + StringPool.CLOSE_BRACKET));
352    
353                            if ((numerator > 0) && (denominator > 0)) {
354                                    IRational iRational = IRational.make(numerator, denominator);
355    
356                                    _videoFrameRateMap.put(previewVideoContainer, iRational);
357    
358                                    if (_log.isInfoEnabled()) {
359                                            _log.info(
360                                                    "Frame rate for " + previewVideoContainer + " set to " +
361                                                            iRational.getNumerator() + "/" +
362                                                                    iRational.getDenominator());
363                                    }
364                            }
365                    }
366            }
367    
368            protected void prepareVideo(
369                            IVideoResampler[] iVideoResamplers,
370                            IVideoPicture[] inputIVideoPictures,
371                            IVideoPicture[] outputIVideoPictures,
372                            IStreamCoder inputIStreamCoder, IStreamCoder[] outputIStreamCoders,
373                            IContainer outputIContainer, IStream[] outputIStreams,
374                            ICodec.Type inputICodecType, String outputURL, int index)
375                    throws Exception {
376    
377                    IStream outputIStream = outputIContainer.addNewStream(index);
378    
379                    outputIStreams[index] = outputIStream;
380    
381                    IStreamCoder outputIStreamCoder = outputIStream.getStreamCoder();
382    
383                    outputIStreamCoders[index] = outputIStreamCoder;
384    
385                    int bitRate = inputIStreamCoder.getBitRate();
386    
387                    if (_log.isInfoEnabled()) {
388                            _log.info("Original video bitrate " + bitRate);
389                    }
390    
391                    if (bitRate == 0) {
392                            bitRate = GetterUtil.getInteger(
393                                    _videoBitRateMap.get(_outputVideoFormat),
394                                    _VIDEO_BIT_RATE_DEFAULT);
395                    }
396                    else if (bitRate > _VIDEO_BIT_RATE_MAX) {
397                            bitRate = _VIDEO_BIT_RATE_MAX;
398                    }
399    
400                    if (_log.isInfoEnabled()) {
401                            _log.info("Modified video bitrate " + bitRate);
402                    }
403    
404                    outputIStreamCoder.setBitRate(bitRate);
405    
406                    ICodec iCodec = ICodec.guessEncodingCodec(
407                            null, null, outputURL, null, inputICodecType);
408    
409                    if (_outputVideoFormat.equals("mp4")) {
410                            iCodec = ICodec.findEncodingCodec(ICodec.ID.CODEC_ID_H264);
411                    }
412    
413                    if (iCodec == null) {
414                            throw new RuntimeException(
415                                    "Unable to determine " + inputICodecType + " encoder for " +
416                                            outputURL);
417                    }
418    
419                    outputIStreamCoder.setCodec(iCodec);
420    
421                    IRational iRational = inputIStreamCoder.getFrameRate();
422    
423                    if (_log.isInfoEnabled()) {
424                            _log.info(
425                                    "Original frame rate " + iRational.getNumerator() + "/" +
426                                            iRational.getDenominator());
427                    }
428    
429                    if (_videoFrameRateMap.containsKey(_outputVideoFormat)) {
430                            iRational = _videoFrameRateMap.get(_outputVideoFormat);
431                    }
432    
433                    if (_log.isInfoEnabled()) {
434                            _log.info(
435                                    "Modified frame rate " + iRational.getNumerator() + "/" +
436                                            iRational.getDenominator());
437                    }
438    
439                    outputIStreamCoder.setFrameRate(iRational);
440    
441                    if (inputIStreamCoder.getHeight() <= 0) {
442                            throw new RuntimeException(
443                                    "Unable to determine height for " + _inputURL);
444                    }
445    
446                    outputIStreamCoder.setHeight(_height);
447    
448                    outputIStreamCoder.setPixelType(Type.YUV420P);
449                    outputIStreamCoder.setTimeBase(
450                            IRational.make(
451                                    iRational.getDenominator(), iRational.getNumerator()));
452    
453                    if (inputIStreamCoder.getWidth() <= 0) {
454                            throw new RuntimeException(
455                                    "Unable to determine width for " + _inputURL);
456                    }
457    
458                    outputIStreamCoder.setWidth(_width);
459    
460                    iVideoResamplers[index] = createIVideoResampler(
461                            inputIStreamCoder, outputIStreamCoder, _height, _width);
462    
463                    inputIVideoPictures[index] = IVideoPicture.make(
464                            inputIStreamCoder.getPixelType(), inputIStreamCoder.getWidth(),
465                            inputIStreamCoder.getHeight());
466                    outputIVideoPictures[index] = IVideoPicture.make(
467                            outputIStreamCoder.getPixelType(), outputIStreamCoder.getWidth(),
468                            outputIStreamCoder.getHeight());
469    
470                    ICodec.ID iCodecID = iCodec.getID();
471    
472                    if (iCodecID.equals(ICodec.ID.CODEC_ID_H264)) {
473                            Configuration.configure(_ffpresetProperties, outputIStreamCoder);
474                    }
475            }
476    
477            private static final int _VIDEO_BIT_RATE_DEFAULT = 250000;
478    
479            private static final int _VIDEO_BIT_RATE_MAX = 1200000;
480    
481            private static Log _log = LogFactoryUtil.getLog(
482                    LiferayVideoConverter.class);
483    
484            private Properties _ffpresetProperties;
485            private int _height = 240;
486            private IContainer _inputIContainer;
487            private String _inputURL;
488            private IContainer _outputIContainer;
489            private String _outputURL;
490            private String _outputVideoFormat;
491            private String[] _previewVideoContainers;
492            private String _tempFileName;
493            private Map<String, Integer> _videoBitRateMap;
494            private Map<String, IRational> _videoFrameRateMap;
495            private int _width = 320;
496    
497    }