| /* |
| * 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.converter; |
| |
| import static org.apache.openmeetings.data.flvrecord.listener.async.BaseStreamWriter.TIME_TO_WAIT_FOR_FRAME; |
| import static org.apache.openmeetings.util.OmFileHelper.MP4_EXTENSION; |
| import static org.apache.openmeetings.util.OmFileHelper.OGG_EXTENSION; |
| import static org.apache.openmeetings.util.OmFileHelper.getRecording; |
| import static org.apache.openmeetings.util.OmFileHelper.getRecordingMetaData; |
| import static org.apache.openmeetings.util.OmFileHelper.getStreamsSubDir; |
| import static org.apache.openmeetings.util.OmFileHelper.isRecordingExists; |
| import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.apache.openmeetings.db.dao.basic.ConfigurationDao; |
| import org.apache.openmeetings.db.dao.record.FlvRecordingMetaDataDao; |
| import org.apache.openmeetings.db.dao.record.FlvRecordingMetaDeltaDao; |
| import org.apache.openmeetings.db.entity.record.FlvRecording; |
| import org.apache.openmeetings.db.entity.record.FlvRecordingMetaData; |
| import org.apache.openmeetings.db.entity.record.FlvRecordingMetaData.Status; |
| import org.apache.openmeetings.db.entity.record.FlvRecordingMetaDelta; |
| import org.apache.openmeetings.util.process.ConverterProcessResult; |
| import org.apache.openmeetings.util.process.ProcessHelper; |
| import org.red5.io.flv.impl.FLVWriter; |
| import org.red5.logging.Red5LoggerFactory; |
| import org.slf4j.Logger; |
| import org.springframework.beans.factory.annotation.Autowired; |
| |
| public abstract class BaseConverter { |
| private static final Logger log = Red5LoggerFactory.getLogger(BaseConverter.class, webAppRootKey); |
| |
| @Autowired |
| private ConfigurationDao configurationDao; |
| @Autowired |
| private FlvRecordingMetaDataDao metaDataDao; |
| @Autowired |
| private FlvRecordingMetaDeltaDao metaDeltaDao; |
| |
| private String getPath(String key, String app) { |
| String path = configurationDao.getConfValue(key, String.class, ""); |
| if (!"".equals(path) && !path.endsWith(File.separator)) { |
| path += File.separator; |
| } |
| path += app; |
| return path; |
| } |
| |
| public String getPathToFFMPEG() { |
| return getPath("ffmpeg_path", "ffmpeg"); |
| } |
| |
| protected String getPathToSoX() { |
| return getPath("sox_path", "sox"); |
| } |
| |
| protected String getPathToImageMagick() { |
| return getPath("imagemagick_path", "convert") + GenerateSWF.execExt; |
| } |
| |
| protected boolean isUseOldStyleFfmpegMap() { |
| return "1".equals(configurationDao.getConfValue("use.old.style.ffmpeg.map.option", String.class, "0")); |
| } |
| |
| protected File getStreamFolder(FlvRecording flvRecording) { |
| return getStreamsSubDir(flvRecording.getRoom_id()); |
| } |
| |
| protected long diff(Date from, Date to) { |
| return 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 String formatMillis(long millis) { |
| long hours = TimeUnit.MILLISECONDS.toHours(millis); |
| millis -= TimeUnit.HOURS.toMillis(hours); |
| long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); |
| millis -= TimeUnit.MINUTES.toMillis(minutes); |
| long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); |
| millis -= TimeUnit.SECONDS.toMillis(seconds); |
| return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis); |
| } |
| |
| protected void updateDuration(FlvRecording r) { |
| r.setDuration(formatMillis(diff(r.getRecordEnd(), r.getRecordStart()))); |
| } |
| |
| protected void deleteFileIfExists(String name) { |
| File f = new File(name); |
| |
| if (f.exists()) { |
| f.delete(); |
| } |
| } |
| |
| protected String[] mergeAudioToWaves(List<String> listOfFullWaveFiles, String outputFullWav) { |
| List<String> argv = new ArrayList<String>(); |
| |
| argv.add(getPathToSoX()); |
| argv.add("-m"); |
| for (String arg : listOfFullWaveFiles) { |
| argv.add(arg); |
| } |
| argv.add(outputFullWav); |
| |
| return argv.toArray(new String[0]); |
| } |
| |
| protected void stripAudioFirstPass(FlvRecording flvRecording, List<ConverterProcessResult> returnLog, |
| List<String> listOfFullWaveFiles, File streamFolder) |
| { |
| stripAudioFirstPass(flvRecording, returnLog, listOfFullWaveFiles, streamFolder |
| , metaDataDao.getAudioMetaDataByRecording(flvRecording.getFlvRecordingId())); |
| } |
| |
| private String[] addSoxPad(List<ConverterProcessResult> returnLog, String job, double length, double position, File inFile, File outFile) throws IOException { |
| //FIXME need to check this |
| if (length < 0 || position < 0) { |
| log.debug("::addSoxPad " + job + " Invalid parameters: " |
| + " length = " + length + "; position = " + position + "; inFile = " + inFile); |
| } |
| length = length < 0 ? 0 : length; |
| position = position < 0 ? 0 : position; |
| |
| String[] argv = new String[] { getPathToSoX(), inFile.getCanonicalPath(), outFile.getCanonicalPath(), "pad", "" + length, "" + position }; |
| |
| returnLog.add(ProcessHelper.executeScript(job, argv)); |
| return argv; |
| } |
| |
| private static File getMetaFlvSer(FlvRecordingMetaData metaData) { |
| File metaDir = getStreamsSubDir(metaData.getFlvRecording().getRoom_id()); |
| return new File(metaDir, metaData.getStreamName() + ".flv.ser"); |
| } |
| |
| public static void printMetaInfo(FlvRecordingMetaData metaData, String prefix) { |
| if (log.isDebugEnabled()) { |
| log.debug(String.format("### %s:: recording id %s; stream with id %s; current status: %s ", prefix, metaData.getFlvRecording().getFlvRecordingId() |
| , metaData.getFlvRecordingMetaDataId(), metaData.getStreamStatus())); |
| File metaFlv = getRecordingMetaData(metaData.getFlvRecording().getRoom_id(), metaData.getStreamName()); |
| File metaSer = getMetaFlvSer(metaData); |
| log.debug(String.format("### %s:: Flv file [%s] exists ? %s; size: %s, lastModified: %s ", prefix, metaFlv.getPath(), metaFlv.exists(), metaFlv.length(), metaFlv.lastModified())); |
| log.debug(String.format("### %s:: Ser file [%s] exists ? %s; size: %s, lastModified: %s ", prefix, metaSer.getPath(), metaSer.exists(), metaSer.length(), metaSer.lastModified())); |
| } |
| } |
| |
| protected FlvRecordingMetaData waitForTheStream(long metaId) throws InterruptedException { |
| FlvRecordingMetaData metaData = metaDataDao.get(metaId); |
| if (metaData.getStreamStatus() != Status.STOPPED) { |
| log.debug("### meta Stream not yet written to disk " + metaId); |
| long counter = 0; |
| long maxTimestamp = 0; |
| while(true) { |
| log.trace("### Stream not yet written Thread Sleep - " + metaId); |
| |
| metaData = metaDataDao.get(metaId); |
| |
| if (metaData.getStreamStatus() == Status.STOPPED) { |
| printMetaInfo(metaData, "Stream now written"); |
| log.debug("### Thread continue ... " ); |
| break; |
| } else { |
| File metaFlv = getRecordingMetaData(metaData.getFlvRecording().getRoom_id(), metaData.getStreamName()); |
| if (metaFlv.exists() && maxTimestamp < metaFlv.lastModified()) { |
| maxTimestamp = metaFlv.lastModified(); |
| } |
| File metaSer = getMetaFlvSer(metaData); |
| if (metaSer.exists() && maxTimestamp < metaSer.lastModified()) { |
| maxTimestamp = metaSer.lastModified(); |
| } |
| if (maxTimestamp + TIME_TO_WAIT_FOR_FRAME < System.currentTimeMillis()) { |
| if (metaSer.exists()) { |
| log.debug("### long time without any update, trying to repair ... "); |
| try { |
| if (FLVWriter.repair(metaSer.getCanonicalPath(), null, null) && !metaSer.exists()) { |
| log.debug("### Repairing was successful ... "); |
| } else { |
| log.debug("### Repairing was NOT successful ... "); |
| } |
| } catch (IOException e) { |
| log.error("### Error while file repairing ... ", e); |
| } |
| } else { |
| log.debug("### long time without any update, closing ... "); |
| } |
| metaData.setStreamStatus(Status.STOPPED); |
| metaDataDao.update(metaData); |
| break; |
| } |
| } |
| if (++counter % 1000 == 0) { |
| printMetaInfo(metaData, "Still waiting"); |
| } |
| |
| Thread.sleep(100L); |
| } |
| } |
| return metaData; |
| } |
| |
| protected void stripAudioFirstPass(FlvRecording flvRecording, |
| List<ConverterProcessResult> returnLog, |
| List<String> listOfFullWaveFiles, File streamFolder, |
| List<FlvRecordingMetaData> metaDataList) { |
| try { |
| // Init variables |
| log.debug("### meta Data Number - " + metaDataList.size()); |
| log.debug("###################################################"); |
| |
| for (FlvRecordingMetaData metaData : metaDataList) { |
| long metaId = metaData.getFlvRecordingMetaDataId(); |
| log.debug("### processing metadata: " + metaId); |
| if (metaData.getStreamStatus() == Status.NONE) { |
| log.debug("Stream has not been started, error in recording " + metaId); |
| continue; |
| } |
| |
| metaData = waitForTheStream(metaId); |
| |
| File inputFlvFile = new File(streamFolder, metaData.getStreamName() + ".flv"); |
| |
| File outputWav = new File(streamFolder, metaData.getStreamName() + "_WAVE.wav"); |
| |
| metaData.setWavAudioData(outputWav.getName()); |
| |
| |
| log.debug("FLV File Name: {} Length: {} ", inputFlvFile.getName(), inputFlvFile.length()); |
| |
| if (inputFlvFile.exists()) { |
| String[] argv = new String[] { |
| getPathToFFMPEG(), "-y" |
| , "-i", inputFlvFile.getCanonicalPath() |
| , "-af", "aresample=32k:min_comp=0.001:min_hard_comp=0.100000" |
| , outputWav.getCanonicalPath()}; |
| |
| returnLog.add(ProcessHelper.executeScript("stripAudioFromFLVs", argv)); |
| } |
| |
| if (outputWav.exists() && outputWav.length() != 0) { |
| metaData.setAudioValid(true); |
| // Strip Wave to Full Length |
| File outputGapFullWav = outputWav; |
| |
| // Fix Start/End in Audio |
| List<FlvRecordingMetaDelta> flvRecordingMetaDeltas = metaDeltaDao.getFlvRecordingMetaDeltaByMetaId(metaId); |
| |
| int counter = 0; |
| |
| for (FlvRecordingMetaDelta metaDelta : flvRecordingMetaDeltas) { |
| File inputFile = outputGapFullWav; |
| |
| // Strip Wave to Full Length |
| String hashFileGapsFullName = metaData.getStreamName() + "_GAP_FULL_WAVE_" + counter + ".wav"; |
| outputGapFullWav = new File(streamFolder, hashFileGapsFullName); |
| |
| metaDelta.setWaveOutPutName(hashFileGapsFullName); |
| |
| String[] argv_sox = null; |
| |
| if (metaDelta.getDeltaTime() != null) { |
| double gapSeconds = diffSeconds(metaDelta.getDeltaTime()); |
| if (metaDelta.isStartPadding()) { |
| argv_sox = addSoxPad(returnLog, "fillGap", gapSeconds, 0, inputFile, outputGapFullWav); |
| } else if (metaDelta.isEndPadding()) { |
| argv_sox = addSoxPad(returnLog, "fillGap", 0, gapSeconds, inputFile, outputGapFullWav); |
| } |
| } |
| |
| if (argv_sox != null) { |
| log.debug("START fillGap ################# Delta-ID :: " + metaDelta.getFlvRecordingMetaDeltaId()); |
| |
| metaDeltaDao.updateFlvRecordingMetaDelta(metaDelta); |
| counter++; |
| } else { |
| outputGapFullWav = inputFile; |
| } |
| } |
| |
| // Strip Wave to Full Length |
| String hashFileFullName = metaData.getStreamName() + "_FULL_WAVE.wav"; |
| File outputFullWav = new File(streamFolder, hashFileFullName); |
| |
| // Calculate delta at beginning |
| double startPad = diffSeconds(metaData.getRecordStart(), flvRecording.getRecordStart()); |
| |
| // Calculate delta at ending |
| double endPad = diffSeconds(flvRecording.getRecordEnd(), metaData.getRecordEnd()); |
| |
| addSoxPad(returnLog, "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 Exception("Audio File does not exist , could not extract the Audio correctly"); |
| } |
| metaData.setFullWavAudioData(hashFileFullName); |
| |
| // Finally add it to the row! |
| listOfFullWaveFiles.add(outputFullWav.getCanonicalPath()); |
| } |
| |
| metaDataDao.update(metaData); |
| } |
| } catch (Exception err) { |
| log.error("[stripAudioFirstPass]", err); |
| } |
| } |
| |
| public void convertToMp4(FlvRecording r, List<ConverterProcessResult> returnLog) throws IOException { |
| //TODO add faststart, move filepaths to helpers |
| if (!isRecordingExists(r.getFileHash())) { |
| return; |
| } |
| File file = getRecording(r.getFileHash()); |
| String path = file.getCanonicalPath(); |
| String mp4path = path + MP4_EXTENSION; |
| String[] argv = new String[] { |
| getPathToFFMPEG(), "-y", |
| "-i", path, |
| "-c:v", "libx264", |
| "-crf", "24", |
| "-pix_fmt", "yuv420p", |
| "-preset", "medium", |
| "-profile:v", "baseline", |
| "-c:a", "libfaac", |
| "-c:a", "libfdk_aac", "-b:a", "32k", //FIXME add quality constants |
| "-s", r.getFlvWidth() + "x" + r.getFlvHeight(), // |
| mp4path |
| }; |
| returnLog.add(ProcessHelper.executeScript("generate MP4", argv)); |
| |
| argv = new String[] { |
| getPathToFFMPEG(), "-y", |
| "-i", mp4path, |
| "-vcodec", "libtheora", |
| "-acodec", "libvorbis", |
| path + OGG_EXTENSION |
| }; |
| |
| returnLog.add(ProcessHelper.executeScript("generate OGG", argv)); |
| } |
| } |