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