org.apache.openmeetings.core.converter.BaseConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.openmeetings.core.converter.BaseConverter.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License") +  you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openmeetings.core.converter;

import static org.apache.commons.io.FileUtils.copyFile;
import static org.apache.commons.lang3.math.NumberUtils.toInt;
import static org.apache.openmeetings.util.CalendarHelper.formatMillis;
import static org.apache.openmeetings.util.OmFileHelper.EXTENSION_PNG;
import static org.apache.openmeetings.util.OmFileHelper.getPublicDir;
import static org.apache.openmeetings.util.OmFileHelper.getRecordingChunk;
import static org.apache.openmeetings.util.OmFileHelper.getStreamsSubDir;
import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_PATH_FFMPEG;
import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_PATH_IMAGEMAGIC;
import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_PATH_SOX;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getAudioBitrate;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getAudioRate;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getVideoPreset;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
import org.apache.openmeetings.db.dao.file.FileItemLogDao;
import org.apache.openmeetings.db.dao.record.RecordingChunkDao;
import org.apache.openmeetings.db.dao.record.RecordingDao;
import org.apache.openmeetings.db.entity.file.BaseFileItem;
import org.apache.openmeetings.db.entity.record.Recording;
import org.apache.openmeetings.db.entity.record.RecordingChunk;
import org.apache.openmeetings.db.entity.record.RecordingChunk.Status;
import org.apache.openmeetings.util.process.ProcessHelper;
import org.apache.openmeetings.util.process.ProcessResult;
import org.apache.openmeetings.util.process.ProcessResultList;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

public abstract class BaseConverter {
    private static final Logger log = LoggerFactory.getLogger(BaseConverter.class);
    private static final Pattern p = Pattern.compile("\\d{2,5}(x)\\d{2,5}");
    public static final String EXEC_EXT = System.getProperty("os.name").toUpperCase(Locale.ROOT)
            .indexOf("WINDOWS") < 0 ? "" : ".exe";
    private static final int MINUTE_MULTIPLIER = 60 * 1000;
    public static final int TIME_TO_WAIT_FOR_FRAME = 15 * MINUTE_MULTIPLIER;

    @Autowired
    protected ConfigurationDao cfgDao;
    @Autowired
    protected RecordingChunkDao chunkDao;
    @Autowired
    protected FileItemLogDao logDao;
    @Autowired
    protected RecordingDao recordingDao;

    protected static class Dimension {
        private final int width;
        private final int height;

        public Dimension(int width, int height) {
            this.width = width;
            this.height = height;
        }

        public int getWidth() {
            return width;
        }

        public int getHeight() {
            return height;
        }
    }

    private String getPath(String key, String app) {
        final String cfg = cfgDao.getString(key, "");
        StringBuilder path = new StringBuilder(cfg);
        if (!Strings.isEmpty(path) && !cfg.endsWith(File.separator)) {
            path.append(File.separator);
        }
        path.append(app).append(EXEC_EXT);
        return path.toString();
    }

    public String getPathToFFMPEG() {
        return getPath(CONFIG_PATH_FFMPEG, "ffmpeg");
    }

    protected String getPathToSoX() {
        return getPath(CONFIG_PATH_SOX, "sox");
    }

    protected String getPathToConvert() {
        return getPath(CONFIG_PATH_IMAGEMAGIC, "convert");
    }

    protected File getStreamFolder(Recording recording) {
        return getStreamsSubDir(recording.getRoomId());
    }

    protected long diff(Date from, Date to) {
        return from == null || to == null ? 0 : from.getTime() - to.getTime();
    }

    protected double diffSeconds(Date from, Date to) {
        return diffSeconds(diff(from, to));
    }

    protected double diffSeconds(long val) {
        return ((double) val) / 1000;
    }

    protected void updateDuration(Recording r) {
        r.setDuration(formatMillis(diff(r.getRecordEnd(), r.getRecordStart())));
    }

    protected void deleteFileIfExists(File f) {
        if (f.exists()) {
            f.delete();
        }
    }

    private String[] mergeAudioToWaves(List<File> waveFiles, File wav) throws IOException {
        List<String> argv = new ArrayList<>();

        argv.add(getPathToSoX());
        argv.add("-m");
        for (File arg : waveFiles) {
            argv.add(arg.getCanonicalPath());
        }
        argv.add(wav.getCanonicalPath());

        return argv.toArray(new String[0]);
    }

    protected void createWav(Recording r, ProcessResultList logs, File streamFolder, List<File> waveFiles, File wav,
            List<RecordingChunk> chunks) throws IOException {
        deleteFileIfExists(wav);
        stripAudioFirstPass(r, logs, waveFiles, streamFolder,
                chunks == null ? chunkDao.getNotScreenChunksByRecording(r.getId()) : chunks);
        if (waveFiles.isEmpty()) {
            // create default Audio to merge it. strip to content length
            String oneSecWav = new File(getPublicDir(), "one_second.wav").getCanonicalPath();

            // Calculate delta at beginning
            double duration = diffSeconds(r.getRecordEnd(), r.getRecordStart());

            String[] cmd = new String[] { getPathToSoX(), oneSecWav, wav.getCanonicalPath(), "pad", "0",
                    String.valueOf(duration) };

            logs.add(ProcessHelper.executeScript("generateSampleAudio", cmd));
        } else if (waveFiles.size() == 1) {
            copyFile(waveFiles.get(0), wav);
        } else {
            String[] soxArgs = mergeAudioToWaves(waveFiles, wav);

            logs.add(ProcessHelper.executeScript("mergeAudioToWaves", soxArgs));
        }
    }

    private String[] addSoxPad(ProcessResultList logs, String job, double length, double position, File inFile,
            File outFile) throws IOException {
        if (length < 0 || position < 0) {
            log.debug("::addSoxPad {} Invalid parameters: length = {}; position = {}; inFile = {}", job, length,
                    position, inFile);
        }
        String[] argv = new String[] { getPathToSoX(), inFile.getCanonicalPath(), outFile.getCanonicalPath(), "pad",
                String.valueOf(length < 0 ? 0 : length), String.valueOf(position < 0 ? 0 : position) };

        logs.add(ProcessHelper.executeScript(job, argv));
        return argv;
    }

    public static void printChunkInfo(RecordingChunk chunk, String prefix) {
        if (log.isDebugEnabled()) {
            log.debug("### {}:: recording id {}; stream with id {}; current status: {} ", prefix,
                    chunk.getRecording().getId(), chunk.getId(), chunk.getStreamStatus());
            File chunkFlv = getRecordingChunk(chunk.getRecording().getRoomId(), chunk.getStreamName());
            log.debug("### {}:: Flv file [{}] exists ? {}; size: {}, lastModified: {} ", prefix, chunkFlv.getPath(),
                    chunkFlv.exists(), chunkFlv.length(), chunkFlv.lastModified());
        }
    }

    protected RecordingChunk waitForTheStream(long chunkId) throws InterruptedException {
        RecordingChunk chunk = chunkDao.get(chunkId);
        if (chunk.getStreamStatus() != Status.STOPPED) {
            log.debug("### Chunk Stream not yet written to disk {}", chunkId);
            long counter = 0;
            long maxTimestamp = 0;
            while (true) {
                log.trace("### Stream not yet written Thread Sleep - {}", chunkId);

                chunk = chunkDao.get(chunkId);

                if (chunk.getStreamStatus() == Status.STOPPED) {
                    printChunkInfo(chunk, "Stream now written");
                    log.debug("### Thread continue ... ");
                    break;
                } else {
                    File chunkFlv = getRecordingChunk(chunk.getRecording().getRoomId(), chunk.getStreamName());
                    if (chunkFlv.exists() && maxTimestamp < chunkFlv.lastModified()) {
                        maxTimestamp = chunkFlv.lastModified();
                    }
                    if (maxTimestamp + TIME_TO_WAIT_FOR_FRAME < System.currentTimeMillis()) {
                        log.debug("### long time without any update, closing ... ");
                        chunk.setStreamStatus(Status.STOPPED);
                        chunkDao.update(chunk);
                        break;
                    }
                }
                if (++counter % 1000 == 0) {
                    printChunkInfo(chunk, "Still waiting");
                }

                Thread.sleep(100L);
            }
        }
        return chunk;
    }

    private void stripAudioFirstPass(Recording recording, ProcessResultList logs, List<File> waveFiles,
            File streamFolder, List<RecordingChunk> chunks) {
        try {
            // Init variables
            log.debug("### Chunks count - {}", chunks.size());
            log.debug("###################################################");

            for (RecordingChunk chunk : chunks) {
                long chunkId = chunk.getId();
                log.debug("### processing chunk: {}", chunkId);
                if (chunk.getStreamStatus() == Status.NONE) {
                    log.debug("Stream has not been started, error in recording {}", chunkId);
                    continue;
                }

                chunk = waitForTheStream(chunkId);

                File inputFlvFile = getRecordingChunk(chunk.getRecording().getRoomId(), chunk.getStreamName());

                File outputWav = new File(streamFolder, chunk.getStreamName() + "_WAVE.wav");

                log.debug("FLV File Name: {} Length: {} ", inputFlvFile.getName(), inputFlvFile.length());

                if (inputFlvFile.exists()) {
                    String[] argv = new String[] { getPathToFFMPEG(), "-y", "-i", inputFlvFile.getCanonicalPath(),
                            "-af",
                            String.format("aresample=%s:min_comp=0.001:min_hard_comp=0.100000", getAudioBitrate()),
                            outputWav.getCanonicalPath() };
                    //there might be no audio in the stream
                    logs.add(ProcessHelper.executeScript("stripAudioFromFLVs", argv, true));
                }

                if (outputWav.exists() && outputWav.length() != 0) {
                    // Strip Wave to Full Length
                    File outputGapFullWav = outputWav;

                    // Strip Wave to Full Length
                    String hashFileFullName = chunk.getStreamName() + "_FULL_WAVE.wav";
                    File outputFullWav = new File(streamFolder, hashFileFullName);

                    // Calculate delta at beginning
                    double startPad = diffSeconds(chunk.getStart(), recording.getRecordStart());

                    // Calculate delta at ending
                    double endPad = diffSeconds(recording.getRecordEnd(), chunk.getEnd());

                    addSoxPad(logs, "addStartEndToAudio", startPad, endPad, outputGapFullWav, outputFullWav);

                    // Fix for Audio Length - Invalid Audio Length in Recorded Files
                    // Audio must match 100% the Video
                    log.debug("############################################");
                    log.debug("Trim Audio to Full Length -- Start");

                    if (!outputFullWav.exists()) {
                        throw new ConversionException(
                                "Audio File does not exist , could not extract the Audio correctly");
                    }

                    // Finally add it to the row!
                    waveFiles.add(outputFullWav);
                }
                chunkDao.update(chunk);
            }
        } catch (Exception err) {
            log.error("[stripAudioFirstPass]", err);
        }
    }

    protected String getDimensions(Recording r, char delim) {
        return String.format("%s%s%s", r.getWidth(), delim, r.getHeight());
    }

    protected String getDimensions(Recording r) {
        return getDimensions(r, 'x');
    }

    protected List<String> addMp4OutParams(Recording r, List<String> argv, String mp4path) {
        argv.addAll(Arrays.asList("-c:v", "h264", //
                "-crf", "24", "-vsync", "0", "-pix_fmt", "yuv420p", "-preset", getVideoPreset(), "-profile:v",
                "baseline", "-level", "3.0", "-movflags", "faststart", "-c:a", "aac", "-ar",
                String.valueOf(getAudioRate()), "-b:a", getAudioBitrate(), "-s", getDimensions(r), //
                mp4path));
        return argv;
    }

    protected String convertToMp4(Recording r, List<String> _argv, ProcessResultList logs) throws IOException {
        String mp4path = r.getFile().getCanonicalPath();
        List<String> argv = new ArrayList<>(Arrays.asList(getPathToFFMPEG(), "-y"));
        argv.addAll(_argv);
        logs.add(ProcessHelper.executeScript("generate MP4",
                addMp4OutParams(r, argv, mp4path).toArray(new String[] {})));
        return mp4path;
    }

    protected void convertToPng(BaseFileItem f, String mp4path, ProcessResultList logs) throws IOException {
        // Extract first Image for preview purpose
        // ffmpeg -i movie.mp4 -vf  "thumbnail,scale=640:-1" -frames:v 1 movie.png
        File png = f.getFile(EXTENSION_PNG);
        String[] argv = new String[] { //
                getPathToFFMPEG(), "-y" //
                , "-i", mp4path //
                , "-vf", "thumbnail,scale=640:-1" //
                , "-frames:v", "1" //
                , png.getCanonicalPath() };
        logs.add(ProcessHelper.executeScript(String.format("generate preview PNG :: %s", f.getHash()), argv));
    }

    protected static Dimension getDimension(String txt) {
        Matcher matcher = p.matcher(txt);

        while (matcher.find()) {
            String foundResolution = txt.substring(matcher.start(), matcher.end());
            String[] resultions = foundResolution.split("x");
            return new Dimension(toInt(resultions[0]), toInt(resultions[1]));
        }

        return new Dimension(100, 100); // will return 100x100 for non-video to be able to play
    }

    protected void finalizeRec(Recording r, String mp4path, ProcessResultList logs) throws IOException {
        convertToPng(r, mp4path, logs);

        updateDuration(r);
        r.setStatus(Recording.Status.PROCESSED);
    }

    protected void postProcess(Recording r, ProcessResultList logs) {
        logDao.delete(r);
        for (ProcessResult res : logs.getJobs()) {
            logDao.add("generateFFMPEG", r, res);
        }
    }

    protected void postProcess(List<File> waveFiles) {
        // Delete Wave Files
        for (File audio : waveFiles) {
            if (audio.exists()) {
                audio.delete();
            }
        }
    }
}