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