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