blob: 026945bd18138dc6bd775556a0c72cffc7bff68f [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.util.OmFileHelper.getRecordingMetaData;
import static org.apache.openmeetings.util.OmFileHelper.getStreamsHibernateDir;
import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.apache.openmeetings.db.dao.record.FlvRecordingDao;
import org.apache.openmeetings.db.dao.record.FlvRecordingLogDao;
import org.apache.openmeetings.db.dao.record.FlvRecordingMetaDataDao;
import org.apache.openmeetings.db.entity.record.FlvRecording;
import org.apache.openmeetings.db.entity.record.FlvRecordingMetaData;
import org.apache.openmeetings.util.process.ConverterProcessResult;
import org.apache.openmeetings.util.process.ProcessHelper;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
public class FlvInterviewConverter extends BaseConverter implements IRecordingConverter {
private static final Logger log = Red5LoggerFactory.getLogger(FlvInterviewConverter.class, webAppRootKey);
private static class ReConverterParams {
private int leftSideLoud = 1;
private int rightSideLoud = 1;
@SuppressWarnings("unused")
private Integer leftSideTime = 0;
@SuppressWarnings("unused")
private Integer rightSideTime = 0;
}
// Spring loaded Beans
@Autowired
private FlvRecordingDao recordingDao;
@Autowired
private FlvRecordingMetaDataDao metaDataDao;
@Autowired
private FlvRecordingLogDao logDao;
private String[] mergeAudioToWaves(List<String> listOfFullWaveFiles, String outputFullWav,
List<FlvRecordingMetaData> metaDataList, ReConverterParams rcv) {
String[] argv_full_sox = new String[listOfFullWaveFiles.size() + 5];
argv_full_sox[0] = this.getPathToSoX();
argv_full_sox[1] = "-m";
int counter = 2;
for (int i = 0; i < listOfFullWaveFiles.size(); i++) {
for (FlvRecordingMetaData flvRecordingMetaData : metaDataList) {
String hashFileFullNameStored = flvRecordingMetaData.getFullWavAudioData();
String fullFilePath = listOfFullWaveFiles.get(i);
String fileNameOnly = new File(fullFilePath).getName();
if (hashFileFullNameStored.equals(fileNameOnly)) {
if (flvRecordingMetaData.getInteriewPodId() == 1) {
argv_full_sox[counter] = "-v " + rcv.leftSideLoud;
counter++;
}
if (flvRecordingMetaData.getInteriewPodId() == 2) {
argv_full_sox[counter] = "-v " + rcv.rightSideLoud;
counter++;
}
}
}
argv_full_sox[counter] = listOfFullWaveFiles.get(i);
counter++;
}
argv_full_sox[counter] = outputFullWav;
return argv_full_sox;
}
public void startConversion(Long flvRecordingId) {
startConversion(flvRecordingId, false, new ReConverterParams());
}
public void startConversion(Long flvRecordingId, boolean reconversion, ReConverterParams rcv) {
FlvRecording flvRecording = null;
try {
flvRecording = recordingDao.get(flvRecordingId);
log.debug("flvRecording " + flvRecording.getFlvRecordingId());
flvRecording.setStatus(FlvRecording.Status.CONVERTING);
flvRecording = recordingDao.update(flvRecording);
List<ConverterProcessResult> returnLog = new ArrayList<ConverterProcessResult>();
List<String> listOfFullWaveFiles = new LinkedList<String>();
File streamFolder = getStreamFolder(flvRecording);
List<FlvRecordingMetaData> metaDataList = metaDataDao.getAudioMetaDataByRecording(flvRecording.getFlvRecordingId());
stripAudioFirstPass(flvRecording, returnLog, listOfFullWaveFiles, streamFolder, metaDataList);
// Merge Wave to Full Length
File streamFolderGeneral = getStreamsHibernateDir();
String hashFileFullName = "INTERVIEW_" + flvRecording.getFlvRecordingId() + "_FINAL_WAVE.wav";
String outputFullWav = streamFolder.getAbsolutePath() + File.separatorChar + hashFileFullName;
deleteFileIfExists(outputFullWav);
if (listOfFullWaveFiles.size() == 1) {
outputFullWav = listOfFullWaveFiles.get(0);
} else if (listOfFullWaveFiles.size() > 0) {
String[] argv_full_sox;
if (reconversion) {
argv_full_sox = mergeAudioToWaves(listOfFullWaveFiles, outputFullWav, metaDataList, rcv);
} else {
argv_full_sox = mergeAudioToWaves(listOfFullWaveFiles, outputFullWav);
}
returnLog.add(ProcessHelper.executeScript("mergeAudioToWaves", argv_full_sox));
} else {
// create default Audio to merge it.
// strip to content length
File outputWav = new File(streamFolderGeneral, "one_second.wav");
// Calculate delta at beginning
double deltaPadding = diffSeconds(flvRecording.getRecordEnd(), flvRecording.getRecordStart());
String[] argv_full_sox = new String[] { getPathToSoX(), outputWav.getCanonicalPath(), outputFullWav, "pad", "0", "" + deltaPadding };
returnLog.add(ProcessHelper.executeScript("generateSampleAudio", argv_full_sox));
}
// Default Image for empty interview video pods
final File defaultInterviewImageFile = new File(streamFolderGeneral, "default_interview_image.png");
if (!defaultInterviewImageFile.exists()) {
throw new Exception("defaultInterviewImageFile does not exist!");
}
final int flvWidth = 320;
final int flvHeight = 260;
final int frameRate = 25;
// Merge Audio with Video / Calculate resulting FLV
String[] pods = new String[2];
boolean found = false;
for (FlvRecordingMetaData meta : metaDataList) {
File flv = getRecordingMetaData(flvRecording.getRoom_id(), meta.getStreamName());
Integer pod = meta.getInteriewPodId();
if (flv.exists() && pod != null && pod > 0 && pod < 3) {
String path = flv.getCanonicalPath();
/*
* CHECK FILE:
* ffmpeg -i rec_316_stream_567_2013_08_28_11_51_45.flv -v error -f null file.null
*/
String[] args = new String[] {getPathToFFMPEG(), "-y"
, "-i", path
, "-an" // only input files with video will be treated as video sources
, "-v", "error"
, "-f", "null"
, "file.null"};
ConverterProcessResult r = ProcessHelper.executeScript("checkFlvPod_" + pod , args);
returnLog.add(r);
if ("0".equals(r.getExitValue())) {
//TODO need to remove smallest gap
long diff = diff(meta.getRecordStart(), meta.getFlvRecording().getRecordStart());
if (diff != 0L) {
// stub to add
// ffmpeg -y -loop 1 -i /home/solomax/work/openmeetings/branches/3.0.x/dist/red5/webapps/openmeetings/streams/hibernate/default_interview_image.jpg -filter_complex '[0:0]scale=320:260' -c:v libx264 -t 00:00:29.059 -pix_fmt yuv420p out.flv
File podFB = new File(streamFolder, meta.getStreamName() + "_pod_" + pod + "_blank.flv");
String podPB = podFB.getCanonicalPath();
String[] argsPodB = new String[] { getPathToFFMPEG(), "-y" //
, "-loop", "1", "-i", defaultInterviewImageFile.getCanonicalPath() //
, "-filter_complex", String.format("[0:0]scale=%1$d:%2$d", flvWidth, flvHeight) //
, "-c:v", "libx264" //
, "-t", formatMillis(diff) //
, "-pix_fmt", "yuv420p" //
, podPB };
returnLog.add(ProcessHelper.executeScript("blankFlvPod_" + pod , argsPodB));
//ffmpeg -y -i out.flv -i rec_15_stream_4_2014_07_15_20_41_03.flv -filter_complex '[0:0]setsar=1/1[sarfix];[1:0]scale=320:260,setsar=1/1[scale];[sarfix] [scale] concat=n=2:v=1:a=0 [v]' -map '[v]' output1.flv
File podF = new File(streamFolder, meta.getStreamName() + "_pod_" + pod + ".flv");
String podP = podF.getCanonicalPath();
String[] argsPod = new String[] { getPathToFFMPEG(), "-y"//
, "-i", podPB //
, "-i", path //
, "-filter_complex", String.format("[0:0]setsar=1/1[sarfix];[1:0]scale=%1$d:%2$d,setsar=1/1[scale];[sarfix] [scale] concat=n=2:v=1:a=0 [v]", flvWidth, flvHeight) //
, "-map", "[v]" //
, podP };
returnLog.add(ProcessHelper.executeScript("shiftedFlvPod_" + pod , argsPod));
pods[pod - 1] = podP;
} else {
pods[pod - 1] = path;
}
}
found = true;
}
}
if (!found) {
ConverterProcessResult r = new ConverterProcessResult();
r.setProcess("CheckFlvFilesExists");
r.setError("No valid pods found");
returnLog.add(r);
return;
}
boolean shortest = false;
List<String> args = new ArrayList<String>();
args.add(getPathToFFMPEG());
args.add("-y");
for (int i = 0; i < 2; ++i) {
/*
* INSERT BLANK INSTEAD OF BAD PAD:
* ffmpeg -loop 1 -i default_interview_image.jpg -i rec_316_stream_569_2013_08_28_11_51_45.flv -filter_complex '[0:v]scale=320:260,pad=2*320:260[left];[1:v]scale=320:260[right];[left][right]overlay=main_w/2:0' -shortest -y out4.flv
*
* JUST MERGE:
* ffmpeg -i rec_316_stream_569_2013_08_28_11_51_45.flv -i rec_316_stream_569_2013_08_28_11_51_45.flv -filter_complex '[0:v]scale=320:260,pad=2*320:260[left];[1:v]scale=320:260[right];[left][right]overlay=main_w/2:0' -y out4.flv
*/
if (pods[i] == null) {
shortest = true;
args.add("-loop"); args.add("1");
args.add("-i"); args.add(defaultInterviewImageFile.getCanonicalPath());
} else {
args.add("-i"); args.add(pods[i]);
}
}
args.add("-i"); args.add(outputFullWav);
args.add("-ar"); args.add("22050");
args.add("-ab"); args.add("32k");
args.add("-filter_complex");
args.add(String.format("[0:v]scale=%1$d:%2$d,pad=2*%1$d:%2$d[left];[1:v]scale=%1$d:%2$d[right];[left][right]overlay=main_w/2:0%3$s"
, flvWidth, flvHeight, shortest ? ":shortest=1" : ""));
if (shortest) {
args.add("-shortest");
}
args.add("-map"); args.add("0:0");
args.add("-map"); args.add("1:0");
args.add("-map"); args.add("2:0");
args.add("-r"); args.add("" + frameRate);
args.add("-qmax"); args.add("1");
args.add("-qmin"); args.add("1");
args.add("-y");
String hashFileFullNameFlv = "flvRecording_" + flvRecording.getFlvRecordingId() + ".flv";
String outputFullFlv = new File(streamFolderGeneral, hashFileFullNameFlv).getCanonicalPath();
args.add(outputFullFlv);
// TODO additional flag to 'quiet' output should be added
returnLog.add(ProcessHelper.executeScript("generateFullBySequenceFLV", args.toArray(new String[]{})));
flvRecording.setFlvWidth(2 * flvWidth);
flvRecording.setFlvHeight(flvHeight);
flvRecording.setFileHash(hashFileFullNameFlv);
// Extract first Image for preview purpose
// ffmpeg -i movie.flv -vcodec mjpeg -vframes 1 -an -f rawvideo -s
// 320x240 movie.jpg
String hashFileFullNameJPEG = "flvRecording_" + flvRecording.getFlvRecordingId() + ".jpg";
String outPutJpeg = new File(streamFolderGeneral, hashFileFullNameJPEG).getCanonicalPath();
deleteFileIfExists(outPutJpeg);
flvRecording.setPreviewImage(hashFileFullNameJPEG);
String[] argv_previewFLV = new String[] { //
getPathToFFMPEG(), "-y", //
"-i", outputFullFlv, //
"-vcodec", "mjpeg", //
"-vframes", "100", "-an", //
"-f", "rawvideo", //
"-s", (2 * flvWidth) + "x" + flvHeight, //
outPutJpeg };
returnLog.add(ProcessHelper.executeScript("generateFullFLV", argv_previewFLV));
String alternateDownloadName = "flvRecording_" + flvRecording.getFlvRecordingId() + ".avi";
String alternateDownloadFullName = new File(streamFolderGeneral, alternateDownloadName).getCanonicalPath();
deleteFileIfExists(alternateDownloadFullName);
String[] argv_alternateDownload = new String[] { getPathToFFMPEG(), "-y", "-i", outputFullFlv, alternateDownloadFullName };
returnLog.add(ProcessHelper.executeScript("alternateDownload", argv_alternateDownload));
flvRecording.setAlternateDownload(alternateDownloadName);
updateDuration(flvRecording);
convertToMp4(flvRecording, returnLog);
flvRecording.setStatus(FlvRecording.Status.PROCESSED);
logDao.deleteByRecordingId(flvRecording.getFlvRecordingId());
for (ConverterProcessResult returnMap : returnLog) {
logDao.addFLVRecordingLog("generateFFMPEG", flvRecording, returnMap);
}
// Delete Wave Files
for (String fileName : listOfFullWaveFiles) {
File audio = new File(fileName);
if (audio.exists()) {
audio.delete();
}
}
} catch (Exception err) {
log.error("[startConversion]", err);
flvRecording.setStatus(FlvRecording.Status.ERROR);
}
recordingDao.update(flvRecording);
}
}