blob: 564f4068db2c87a263a9b34304bb8d95fa4d8a88 [file] [log] [blame]
/*
* 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));
}
}