Java tutorial
/* * 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 java.util.UUID.randomUUID; import static org.apache.openmeetings.util.CalendarHelper.formatMillis; import static org.apache.openmeetings.util.OmFileHelper.EXTENSION_MP4; import static org.apache.openmeetings.util.OmFileHelper.getRecordingChunk; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; import org.apache.openmeetings.db.entity.record.Recording; import org.apache.openmeetings.db.entity.record.RecordingChunk; import org.apache.openmeetings.util.OmFileHelper; 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.stereotype.Component; @Component public class InterviewConverter extends BaseConverter implements IRecordingConverter { private static final Logger log = LoggerFactory.getLogger(InterviewConverter.class); @Override public void startConversion(Recording r) { if (r == null) { log.warn("Conversion is NOT started. Recording passed is NULL"); return; } ProcessResultList logs = new ProcessResultList(); List<File> waveFiles = new ArrayList<>(); try { // Default Image for empty interview video pods final File interviewCamFile = new File(OmFileHelper.getImagesDir(), "interview_webcam.png"); if (!interviewCamFile.exists()) { throw new ConversionException("defaultInterviewImageFile does not exist!"); } log.debug("recording {}", r.getId()); if (Strings.isEmpty(r.getHash())) { r.setHash(randomUUID().toString()); } r.setStatus(Recording.Status.CONVERTING); r = recordingDao.update(r); File streamFolder = getStreamFolder(r); List<RecordingChunk> chunks = chunkDao.getByRecording(r.getId()); File wav = new File(streamFolder, String.format("INTERVIEW_%s_FINAL_WAVE.wav", r.getId())); createWav(r, logs, streamFolder, waveFiles, wav, chunks); final String interviewCam = interviewCamFile.getCanonicalPath(); final int width = 320; final int height = 260; // Merge Audio with Video / Calculate resulting video // group by sid first to get all pods Map<String, List<RecordingChunk>> cunksBySid = chunks.stream() .collect(Collectors.groupingBy(RecordingChunk::getSid, LinkedHashMap::new, Collectors.collectingAndThen(Collectors.toList(), l -> l.stream().sorted(Comparator.comparing(RecordingChunk::getStart)) .collect(Collectors.toList())))); List<String> pods = new ArrayList<>(); int N = pods.size(); for (Entry<String, List<RecordingChunk>> e : cunksBySid.entrySet()) { Date pStart = r.getRecordStart(); List<PodPart> parts = new ArrayList<>(); pStart = processParts(r.getRoomId(), e.getValue(), logs, N, parts, pStart); if (!parts.isEmpty()) { String podX = new File(streamFolder, String.format("rec_%s_pod_%s.%s", r.getId(), N, EXTENSION_MP4)).getCanonicalPath(); long diff = diff(r.getRecordEnd(), pStart); PodPart.add(parts, diff); /* create continuous pod * ffmpeg \ * -loop 1 -framerate 24 -t 10 -i image1.jpg \ * -i video.mp4 \ * -loop 1 -framerate 24 -t 10 -i image2.jpg \ * -loop 1 -framerate 24 -t 10 -i image3.jpg \ * -filter_complex "[0][1][2][3]concat=n=4:v=1:a=0" out.mp4 */ List<String> args = new ArrayList<>(); args.add(getPathToFFMPEG()); args.add("-y"); StringBuilder videos = new StringBuilder(); StringBuilder concat = new StringBuilder(); for (int i = 0; i < parts.size(); ++i) { PodPart p = parts.get(i); if (p.getFile() == null) { args.add("-loop"); args.add("1"); args.add("-t"); args.add(formatMillis(p.getDuration())); args.add("-i"); args.add(interviewCam); } else { args.add("-t"); args.add(formatMillis(p.getDuration())); args.add("-i"); args.add(p.getFile()); } videos.append('[').append(i).append(']').append("scale=").append(width).append(':') .append(height).append(",setsar=1:1").append("[v").append(i).append("]; "); concat.append("[v").append(i).append(']'); } args.add("-filter_complex"); args.add(concat.insert(0, videos).append("concat=n=").append(parts.size()).append(":v=1:a=0") .toString()); args.add("-an"); args.add(podX); ProcessResult res = ProcessHelper.executeScript(String.format("Full video pod_%s", N), args.toArray(new String[0]), true); logs.add(res); if (res.isWarn()) { throw new ConversionException("Fail to create pod"); } pods.add(podX); N = pods.size(); } } if (N == 0) { ProcessResult res = new ProcessResult(); res.setProcess("CheckStreamFilesExists"); res.setError("No valid pods found"); res.setExitCode(-1); logs.add(res); return; } double ratio = Math.sqrt(N / Math.sqrt(2)); int w = ratio < 1 ? N : (int) Math.round(ratio); w = Math.max(w, (int) Math.round(1. * N / w)); r.setWidth(w * width); r.setHeight((N / w) * height); String mp4path = convertToMp4(r, getFinalArgs(N, pods, wav, w), logs); finalizeRec(r, mp4path, logs); } catch (Exception err) { log.error("[startConversion]", err); r.setStatus(Recording.Status.ERROR); } finally { if (Recording.Status.CONVERTING == r.getStatus()) { r.setStatus(Recording.Status.ERROR); } postProcess(r, logs); postProcess(waveFiles); recordingDao.update(r); } } private Date processParts(Long roomId, List<RecordingChunk> chunks, ProcessResultList logs, int N, List<PodPart> parts, Date pStart) throws IOException { for (RecordingChunk chunk : chunks) { File chunkStream = getRecordingChunk(roomId, chunk.getStreamName()); if (!chunkStream.exists()) { log.debug("Chunk stream doesn't exist: {}", chunkStream); continue; } String path = chunkStream.getCanonicalPath(); /* CHECK FILE: * ffmpeg -i rec_316_stream_567_2013_08_28_11_51_45.webm -v error -f null file.null */ String[] args = new String[] { getPathToFFMPEG(), "-y", "-i", path, "-v", "error", "-f", "null", "file.null" }; ProcessResult res = ProcessHelper .executeScript(String.format("Check chunk pod video_%s_%s", N, parts.size()), args, true); logs.add(res); if (!res.isWarn()) { long diff = diff(chunk.isAudioOnly() ? chunk.getEnd() : chunk.getStart(), pStart); PodPart.add(parts, diff); if (!chunk.isAudioOnly()) { parts.add(new PodPart(path, diff(chunk.getEnd(), chunk.getStart()))); } pStart = chunk.getEnd(); } } return pStart; } private static List<String> getFinalArgs(int N, List<String> pods, File wav, int w) throws IOException { List<String> args = new ArrayList<>(); if (N == 1) { args.add("-i"); args.add(pods.get(0)); args.add("-i"); args.add(wav.getCanonicalPath()); args.add("-map"); args.add("0:v"); } else { /* Creating grid * ffmpeg -i top_l.mp4 -i top_r.mp4 -i bottom_l.mp4 -i bottom_r.mp4 -i audio.mp4 \ * -filter_complex "[0:v][1:v]hstack=inputs=2[t];[2:v][3:v]hstack=inputs=2[b];[t][b]vstack=inputs=2[v]" \ * -map "[v]" -map 4:a -c:a copy -shortest output.mp4 */ StringBuilder cols = new StringBuilder(); StringBuilder rows = new StringBuilder(); for (int i = 0, j = 0; i < N; ++i) { args.add("-i"); args.add(pods.get(i)); cols.append('[').append(i).append(":v]"); if (i != 0 && (i + 1) % w == 0) { cols.append("hstack=inputs=").append(w); if (j == 0 && i == N - 1) { cols.append("[v]"); } else { cols.append("[c").append(j).append("];"); } rows.append("[c").append(j).append(']'); j++; } if (i == N - 1) { if (j > 1) { rows.append("vstack=inputs=").append(j).append("[v]"); } else { rows.setLength(0); } } } args.add("-i"); args.add(wav.getCanonicalPath()); args.add("-filter_complex"); args.add(cols.append(rows).toString()); args.add("-map"); args.add("[v]"); } args.add("-map"); args.add(String.format("%s:a", N)); args.add("-qmax"); args.add("1"); args.add("-qmin"); args.add("1"); return args; } private static class PodPart { final String file; final long duration; public PodPart(String file, long duration) { this.file = file; this.duration = duration; } public PodPart(long duration) { this(null, duration); } public String getFile() { return file; } public long getDuration() { return duration; } public static void add(List<PodPart> parts, long duration) { if (duration > 19L) { // ffmpeg ignores durations less than 19ms, can hang parts.add(new PodPart(duration)); } else { log.warn("PodPart with duration less than 19ms found: {}", duration); } } } }