| /* |
| * 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); |
| } |
| } |