This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/master by this push:
     new f1b65ef  [OPENMEETINGS-2592] recording in interview room should work, 
code clean-up
     new 84b0783  Merge branch 'master' of github.com:apache/openmeetings
f1b65ef is described below

commit f1b65efd2daaf8654ca531ae8eece5a47a5abb79
Author: Maxim Solodovnik <solomax...@gmail.com>
AuthorDate: Fri Mar 12 07:58:54 2021 +0700

    [OPENMEETINGS-2592] recording in interview room should work, code clean-up
---
 .../openmeetings/core/converter/BaseConverter.java |  54 +++----
 .../core/converter/ImageConverter.java             |  31 ++--
 .../core/converter/InterviewConverter.java         | 161 ++++++++++++---------
 .../core/converter/RecordingConverter.java         |   2 +-
 .../core/converter/VideoConverter.java             |  12 +-
 .../apache/openmeetings/core/remote/KStream.java   | 128 +++++++++-------
 openmeetings-db/pom.xml                            |   1 -
 .../openmeetings/util/process/ProcessHelper.java   |  21 ++-
 .../web/pages/install/InstallWizard.java           |  11 +-
 9 files changed, 231 insertions(+), 190 deletions(-)

diff --git 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/BaseConverter.java
 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/BaseConverter.java
index 941c31d..e19df90 100644
--- 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/BaseConverter.java
+++ 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/BaseConverter.java
@@ -62,7 +62,7 @@ public abstract class BaseConverter {
        private static final Pattern p = Pattern.compile("\\d{2,5}(x)\\d{2,5}");
        public static final String EXEC_EXT = 
System.getProperty("os.name").toUpperCase(Locale.ROOT).indexOf("WINDOWS") < 0 ? 
"" : ".exe";
        private static final int MINUTE_MULTIPLIER = 60 * 1000;
-       public static final int TIME_TO_WAIT_FOR_FRAME = 15 * MINUTE_MULTIPLIER;
+       public static final int TIME_TO_WAIT_FOR_FRAME = 5 * MINUTE_MULTIPLIER;
        public static final double HALF_STEP = 1. / 2;
 
        @Autowired
@@ -140,7 +140,7 @@ public abstract class BaseConverter {
                }
        }
 
-       private String[] mergeAudioToWaves(List<File> waveFiles, File wav) 
throws IOException {
+       private List<String> mergeAudioToWaves(List<File> waveFiles, File wav) 
throws IOException {
                List<String> argv = new ArrayList<>();
 
                argv.add(getPathToSoX());
@@ -150,7 +150,7 @@ public abstract class BaseConverter {
                }
                argv.add(wav.getCanonicalPath());
 
-               return argv.toArray(new String[0]);
+               return argv;
        }
 
        protected void createWav(Recording r, ProcessResultList logs, File 
streamFolder, List<File> waveFiles, File wav, List<RecordingChunk> chunks) 
throws IOException {
@@ -163,27 +163,25 @@ public abstract class BaseConverter {
                        // Calculate delta at beginning
                        double duration = diffSeconds(r.getRecordEnd(), 
r.getRecordStart());
 
-                       String[] cmd = new String[] { getPathToSoX(), 
oneSecWav, wav.getCanonicalPath(), "pad", "0", String.valueOf(duration) };
+                       List<String> cmd = List.of(getPathToSoX(), oneSecWav, 
wav.getCanonicalPath(), "pad", "0", String.valueOf(duration));
 
-                       
logs.add(ProcessHelper.executeScript("generateSampleAudio", cmd));
+                       logs.add(ProcessHelper.exec("generateSampleAudio", 
cmd));
                } else if (waveFiles.size() == 1) {
                        copyFile(waveFiles.get(0), wav);
                } else {
-                       String[] soxArgs = mergeAudioToWaves(waveFiles, wav);
-
-                       
logs.add(ProcessHelper.executeScript("mergeAudioToWaves", soxArgs));
+                       logs.add(ProcessHelper.exec("mergeAudioToWaves", 
mergeAudioToWaves(waveFiles, wav)));
                }
        }
 
-       private String[] addSoxPad(ProcessResultList logs, String job, double 
length, double position, File inFile, File outFile) throws IOException {
+       private List<String> addSoxPad(ProcessResultList logs, String job, 
double length, double position, File inFile, File outFile) throws IOException {
                if (length < 0 || position < 0) {
                        log.debug("::addSoxPad {} Invalid parameters: length = 
{}; position = {}; inFile = {}", job, length, position, inFile);
                }
-               String[] argv = new String[] { getPathToSoX(), 
inFile.getCanonicalPath(), outFile.getCanonicalPath(), "pad"
+               List<String> argv = List.of(getPathToSoX(), 
inFile.getCanonicalPath(), outFile.getCanonicalPath(), "pad"
                                , String.valueOf(length < 0 ? 0 : length)
-                               , String.valueOf(position < 0 ? 0 : position) };
+                               , String.valueOf(position < 0 ? 0 : position));
 
-               logs.add(ProcessHelper.executeScript(job, argv));
+               logs.add(ProcessHelper.exec(job, argv));
                return argv;
        }
 
@@ -192,7 +190,7 @@ public abstract class BaseConverter {
                        log.debug("### {}:: recording id {}; stream with id {}; 
current status: {} ", prefix, chunk.getRecording().getId()
                                        , chunk.getId(), 
chunk.getStreamStatus());
                        File chunkFlv = 
getRecordingChunk(chunk.getRecording().getRoomId(), chunk.getStreamName());
-                       log.debug("### {}:: Flv file [{}] exists ? {}; size: 
{}, lastModified: {} ", prefix, chunkFlv.getPath(), chunkFlv.exists(), 
chunkFlv.length(), chunkFlv.lastModified());
+                       log.debug("### {}:: Chunk file [{}] exists ? {}; size: 
{}, lastModified: {} ", prefix, chunkFlv.getPath(), chunkFlv.exists(), 
chunkFlv.length(), chunkFlv.lastModified());
                }
        }
 
@@ -259,13 +257,13 @@ public abstract class BaseConverter {
                                log.debug("FLV File Name: {} Length: {} ", 
inputFlvFile.getName(), inputFlvFile.length());
 
                                if (inputFlvFile.exists()) {
-                                       String[] argv = new String[] {
+                                       List<String> argv = List.of(
                                                        getPathToFFMPEG(), "-y"
                                                        , "-i", 
inputFlvFile.getCanonicalPath()
                                                        , "-af", 
String.format("aresample=%s:min_comp=0.001:min_hard_comp=0.100000", 
getAudioBitrate())
-                                                       , 
outputWav.getCanonicalPath()};
+                                                       , 
outputWav.getCanonicalPath());
                                        //there might be no audio in the stream
-                                       
logs.add(ProcessHelper.executeScript("stripAudioFromFLVs", argv, true));
+                                       
logs.add(ProcessHelper.exec("stripAudioFromFLVs", argv, true));
                                }
 
                                if (outputWav.exists() && outputWav.length() != 
0) {
@@ -319,13 +317,12 @@ public abstract class BaseConverter {
                return List.of();
        }
 
-       private List<String> addMp4OutParams(Recording r, List<String> argv, 
String mp4path) {
+       private List<String> addMp4OutParams(Recording r, List<String> argv, 
boolean interview, String mp4path) {
                argv.addAll(List.of(
                                "-c:v", "h264" //
                                , "-crf", "24"
                                , "-vsync", "0"
                                , "-pix_fmt", "yuv420p"
-                               , "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2"
                                , "-preset", getVideoPreset()
                                , "-profile:v", "baseline"
                                , "-level", "3.0"
@@ -334,16 +331,19 @@ public abstract class BaseConverter {
                                , "-ar", String.valueOf(getAudioRate())
                                , "-b:a", getAudioBitrate()
                                ));
+               if (!interview) {
+                       argv.addAll(List.of("-vf", 
"pad=ceil(iw/2)*2:ceil(ih/2)*2"));
+               }
                argv.addAll(additionalMp4OutParams(r));
                argv.add(mp4path);
                return argv;
        }
 
-       protected String convertToMp4(Recording r, List<String> inArgv, 
ProcessResultList logs) throws IOException {
+       protected String convertToMp4(Recording r, List<String> inArgv, boolean 
interview, ProcessResultList logs) throws IOException {
                String mp4path = r.getFile().getCanonicalPath();
                List<String> argv = new ArrayList<>(List.of(getPathToFFMPEG(), 
"-y"));
                argv.addAll(inArgv);
-               logs.add(ProcessHelper.executeScript("generate MP4", 
addMp4OutParams(r, argv, mp4path).toArray(new String[]{})));
+               logs.add(ProcessHelper.exec("generate MP4", addMp4OutParams(r, 
argv, interview, mp4path)));
                return mp4path;
        }
 
@@ -351,13 +351,13 @@ public abstract class BaseConverter {
                // Extract first Image for preview purpose
                // ffmpeg -i movie.mp4 -vf  "thumbnail,scale=640:-1" -frames:v 
1 movie.png
                File png = f.getFile(EXTENSION_PNG);
-               String[] argv = new String[] { //
-                               getPathToFFMPEG(), "-y" //
-                               , "-i", mp4path //
-                               , "-vf", "thumbnail,scale=640:-1" //
-                               , "-frames:v", "1" //
-                               , png.getCanonicalPath() };
-               logs.add(ProcessHelper.executeScript(String.format("generate 
preview PNG :: %s", f.getHash()), argv));
+               List<String> argv = List.of(
+                               getPathToFFMPEG(), "-y"
+                               , "-i", mp4path
+                               , "-vf", "thumbnail,scale=640:-1"
+                               , "-frames:v", "1"
+                               , png.getCanonicalPath());
+               logs.add(ProcessHelper.exec("generate preview PNG :: " + 
f.getHash(), argv));
        }
 
        /**
diff --git 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/ImageConverter.java
 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/ImageConverter.java
index c327dda..a6dc993 100644
--- 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/ImageConverter.java
+++ 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/ImageConverter.java
@@ -33,6 +33,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
 import java.util.Optional;
 import java.util.function.DoubleConsumer;
 
@@ -152,17 +153,16 @@ public class ImageConverter extends BaseConverter {
         *
         */
        private ProcessResult convertSinglePng(File in, File out) throws 
IOException {
-               String[] argv = new String[] { getPathToConvert(), 
in.getCanonicalPath(), out.getCanonicalPath() };
+               List<String> argv = List.of(getPathToConvert(), 
in.getCanonicalPath(), out.getCanonicalPath());
 
-               return ProcessHelper.executeScript("convertSinglePng", argv);
+               return ProcessHelper.exec("convertSinglePng", argv);
        }
 
        public ProcessResult resize(File in, File out, Integer width, Integer 
height) throws IOException {
-               String[] argv = new String[] { getPathToConvert()
+               List<String> argv = List.of(getPathToConvert()
                                , "-resize", (width == null ? "" : width) + 
(height == null ? "" : "x" + height)
-                               , in.getCanonicalPath(), out.getCanonicalPath()
-                               };
-               return ProcessHelper.executeScript("resize", argv);
+                               , in.getCanonicalPath(), 
out.getCanonicalPath());
+               return ProcessHelper.exec("resize", argv);
        }
 
        /**
@@ -176,16 +176,15 @@ public class ImageConverter extends BaseConverter {
         */
        public ProcessResultList convertDocument(FileItem f, File pdf, 
ProcessResultList logs, Optional<DoubleConsumer> progress) throws IOException {
                log.debug("convertDocument");
-               String[] argv = new String[] {
-                       getPathToConvert()
-                       , "-density", getDpi()
-                       , "-define", "pdf:use-cropbox=true"
-                       , pdf.getCanonicalPath()
-                       , "+profile", "'*'"
-                       , "-quality", getQuality()
-                       , new File(pdf.getParentFile(), 
PAGE_TMPLT).getCanonicalPath()
-                       };
-               ProcessResult res = ProcessHelper.executeScript("convert PDF to 
images", argv);
+               List<String> argv = List.of(
+                               getPathToConvert()
+                               , "-density", getDpi()
+                               , "-define", "pdf:use-cropbox=true"
+                               , pdf.getCanonicalPath()
+                               , "+profile", "'*'"
+                               , "-quality", getQuality()
+                               , new File(pdf.getParentFile(), 
PAGE_TMPLT).getCanonicalPath());
+               ProcessResult res = ProcessHelper.exec("convert PDF to images", 
argv);
                logs.add(res);
                progress.ifPresent(theProgress -> theProgress.accept(1. / 4));
                if (res.isOk()) {
diff --git 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/InterviewConverter.java
 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/InterviewConverter.java
index 7e17d93..d74c287 100644
--- 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/InterviewConverter.java
+++ 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/InterviewConverter.java
@@ -48,6 +48,24 @@ import org.springframework.stereotype.Component;
 @Component
 public class InterviewConverter extends BaseConverter implements 
IRecordingConverter {
        private static final Logger log = 
LoggerFactory.getLogger(InterviewConverter.class);
+       private static final int WIDTH = 320;
+       private static final int HEIGHT = 260;
+       private String interviewCam;
+       private String interviewBlank;
+
+       private void init() throws ConversionException, IOException {
+               // 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!");
+               }
+               interviewCam = interviewCamFile.getCanonicalPath();
+               final File interviewBlankFile = new 
File(OmFileHelper.getImagesDir(), "blank.png");
+               if (!interviewBlankFile.exists()) {
+                       throw new 
ConversionException("defaultInterviewImageFile does not exist!");
+               }
+               interviewBlank = interviewBlankFile.getCanonicalPath();
+       }
 
        @Override
        public void startConversion(Recording r) {
@@ -58,13 +76,10 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                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 (interviewCam == null) {
+                               init();
+                       }
                        if (Strings.isEmpty(r.getHash())) {
                                r.setHash(randomUUID().toString());
                        }
@@ -77,10 +92,6 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                        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
@@ -89,61 +100,21 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                                        , LinkedHashMap::new
                                        , 
Collectors.collectingAndThen(Collectors.toList(), l -> 
l.stream().sorted(Comparator.comparing(RecordingChunk::getStart)).collect(Collectors.toList()))));
                        List<String> pods = new ArrayList<>();
-                       int numPods = pods.size();
                        for (Entry<String, List<RecordingChunk>> e : 
cunksBySid.entrySet()) {
+                               int podIdx = pods.size();
                                Date pStart = r.getRecordStart();
                                List<PodPart> parts = new ArrayList<>();
-                               pStart = processParts(r.getRoomId(), 
e.getValue(), logs, numPods, parts, pStart);
+                               pStart = processParts(r.getRoomId(), 
e.getValue(), logs, podIdx, parts, pStart);
                                if (!parts.isEmpty()) {
-                                       String podX = new File(streamFolder, 
String.format("rec_%s_pod_%s.%s", r.getId(), numPods, 
EXTENSION_MP4)).getCanonicalPath();
+                                       String podX = new File(streamFolder, 
String.format("rec_%s_pod_%s.%s", r.getId(), podIdx, 
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", numPods), 
args.toArray(new String[0]), true);
-                                       logs.add(res);
-                                       if (res.isWarn()) {
-                                               throw new 
ConversionException("Fail to create pod");
-                                       }
+
+                                       createPod(podX, interviewCam, podIdx, 
parts, logs);
                                        pods.add(podX);
-                                       numPods = pods.size();
                                }
                        }
+                       int numPods = pods.size();
                        if (numPods == 0) {
                                ProcessResult res = new ProcessResult();
                                res.setProcess("CheckStreamFilesExists");
@@ -155,11 +126,18 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                        double ratio = Math.sqrt(numPods / Math.sqrt(2));
                        int w = ratio < 1 ? numPods : (int)Math.round(ratio);
                        w = Math.max(w, (int)Math.round(1. * numPods / w));
+                       List<PodPart> missingParts = new ArrayList<>();
+                       PodPart.add(missingParts, diff(r.getRecordEnd(), 
r.getRecordStart()));
+                       String missingPod = new File(streamFolder, 
String.format("rec_%s_pod_%s.%s", r.getId(), numPods, 
EXTENSION_MP4)).getCanonicalPath();
+                       createPod(missingPod, interviewBlank, numPods, 
missingParts, logs);
+                       for (int i = numPods % w; i < w; ++i) {
+                               pods.add(missingPod);
+                       }
 
-                       r.setWidth(w * width);
-                       r.setHeight((numPods / w) * height);
+                       r.setWidth(w * WIDTH);
+                       r.setHeight((numPods / w) * HEIGHT);
 
-                       String mp4path = convertToMp4(r, getFinalArgs(numPods, 
pods, wav, w), logs);
+                       String mp4path = convertToMp4(r, getFinalArgs(pods, 
wav, w), numPods != 1, logs);
 
                        finalizeRec(r, mp4path, logs);
                } catch (Exception err) {
@@ -175,6 +153,51 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                }
        }
 
+       private void createPod(String podX, String image, int podIdx, 
List<PodPart> parts, ProcessResultList logs) throws ConversionException {
+               /* 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(image);
+                       } 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.exec("Full video pod_" + 
podIdx, args, true);
+               logs.add(res);
+               if (res.isWarn()) {
+                       throw new ConversionException("Fail to create pod");
+               }
+       }
+
        private Date processParts(Long roomId, List<RecordingChunk> chunks, 
ProcessResultList logs, int numPods, List<PodPart> parts, Date pStart) throws 
IOException {
                for (RecordingChunk chunk : chunks) {
                        File chunkStream = getRecordingChunk(roomId, 
chunk.getStreamName());
@@ -186,12 +209,12 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                        /* 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"
+                       List<String> args = List.of(getPathToFFMPEG(), "-y"
                                        , "-i", path
                                        , "-v", "error"
                                        , "-f", "null"
-                                       , "file.null"};
-                       ProcessResult res = 
ProcessHelper.executeScript(String.format("Check chunk pod video_%s_%s", 
numPods, parts.size()), args, true);
+                                       , "file.null");
+                       ProcessResult res = 
ProcessHelper.exec(String.format("Check chunk pod video_%s_%s", numPods, 
parts.size()), args, true);
                        logs.add(res);
                        if (!res.isWarn()) {
                                long diff = diff(chunk.isAudioOnly() ? 
chunk.getEnd() : chunk.getStart(), pStart);
@@ -205,7 +228,8 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                return pStart;
        }
 
-       private static List<String> getFinalArgs(int numPods, List<String> 
pods, File wav, int w) throws IOException {
+       private static List<String> getFinalArgs(List<String> pods, File wav, 
int w) throws IOException {
+               final int numPods = pods.size();
                List<String> args = new ArrayList<>();
                if (numPods == 1) {
                        args.add("-i");
@@ -222,12 +246,14 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                         */
                        StringBuilder cols = new StringBuilder();
                        StringBuilder rows = new StringBuilder();
+                       int colCount = 0;
                        for (int i = 0, j = 0; i < numPods; ++i) {
+                               colCount++;
                                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 (i != 0 && colCount % w == 0) {
+                                       
cols.append("hstack=inputs=").append(colCount);
                                        if (j == 0 && i == numPods - 1) {
                                                cols.append("[v]");
                                        } else {
@@ -235,10 +261,11 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                                        }
                                        rows.append("[c").append(j).append(']');
                                        j++;
+                                       colCount = 0;
                                }
                                if (i == numPods - 1) {
                                        if (j > 1) {
-                                               
rows.append("vstack=inputs=").append(j).append("[v]");
+                                               
rows.append("vstack=inputs=").append(j).append("[out];[out]pad=ceil(iw/2)*2:ceil(ih/2)*2[v]");
                                        } else {
                                                rows.setLength(0);
                                        }
@@ -252,7 +279,7 @@ public class InterviewConverter extends BaseConverter 
implements IRecordingConve
                        args.add("[v]");
                }
                args.add("-map");
-               args.add(String.format("%s:a", numPods));
+               args.add(numPods + ":a");
                args.add("-qmax"); args.add("1");
                args.add("-qmin"); args.add("1");
                return args;
diff --git 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/RecordingConverter.java
 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/RecordingConverter.java
index f1afb49..80ccbae 100644
--- 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/RecordingConverter.java
+++ 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/RecordingConverter.java
@@ -88,7 +88,7 @@ public class RecordingConverter extends BaseConverter 
implements IRecordingConve
                        String mp4path = convertToMp4(r, List.of(
                                        "-itsoffset", 
formatMillis(diff(screenChunk.getStart(), r.getRecordStart())),
                                        "-i", inputScreenFullFlv, "-i", 
wav.getCanonicalPath()
-                                       ), logs);
+                                       ), false, logs);
                        Dimension dim = getDimension(logs.getLast().getError(), 
null); // will return 100x100 for non-video to be able to play
                        if (dim != null) {
                                r.setWidth(dim.getWidth());
diff --git 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/VideoConverter.java
 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/VideoConverter.java
index 0f444ae..96be9f5 100644
--- 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/VideoConverter.java
+++ 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/converter/VideoConverter.java
@@ -62,13 +62,13 @@ public class VideoConverter extends BaseConverter {
                        List<String> args = new 
ArrayList<>(List.of(getPathToFFMPEG(), "-y"));
                        if (sf.isAudio()) {
                                // need to add background image, it should be 
jpg since black on transparent will be invisible
-                               args.addAll(List.of("-loop", "1"//
-                                               , "-framerate", "24"//
+                               args.addAll(List.of("-loop", "1"
+                                               , "-framerate", "24"
                                                , "-i", new 
File(getCssImagesDir(), "audio.jpg").getCanonicalPath()));
                        }
-                       args.addAll(List.of("-i", input //
-                                       , "-c:v", "h264" //
-                                       , "-c:a", "aac" //
+                       args.addAll(List.of("-i", input
+                                       , "-c:v", "h264"
+                                       , "-c:a", "aac"
                                        , "-pix_fmt", "yuv420p"
                                        , "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2"
                                        ));
@@ -76,7 +76,7 @@ public class VideoConverter extends BaseConverter {
                                args.add("-shortest");
                        }
                        args.add(mp4.getCanonicalPath());
-                       ProcessResult res = 
ProcessHelper.executeScript("convert to MP4 :: " + f.getHash(), 
args.toArray(new String[0]));
+                       ProcessResult res = ProcessHelper.exec("convert to MP4 
:: " + f.getHash(), args);
                        logs.add(res);
                        progress.ifPresent(theProgress -> 
theProgress.accept(STEP));
                        if (sameExt && tmp != null) {
diff --git 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
index aaef0ca..56f2a58 100644
--- 
a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
+++ 
b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
@@ -339,7 +339,10 @@ public class KStream extends AbstractStream implements 
ISipCallbacks {
                setTags(recorder, uid);
 
                recorder.addRecordingListener(evt -> chunkId = 
kRoom.getChunkDao().start(kRoom.getRecordingId(), type, chunkUid, sid));
-               recorder.addStoppedListener(evt -> 
kRoom.getChunkDao().stop(chunkId));
+               recorder.addStoppedListener(evt -> {
+                       kRoom.getChunkDao().stop(chunkId);
+                       chunkId = null;
+               });
                switch (profile) {
                        case WEBM:
                                outgoingMedia.connect(recorder, 
MediaType.AUDIO);
@@ -367,8 +370,7 @@ public class KStream extends AbstractStream implements 
ISipCallbacks {
        }
 
        public void stopRecord() {
-               releaseRecorder(true);
-               chunkId = null;
+               stopRecorder(true, () -> {});
        }
 
        public void remove(final Client c) {
@@ -423,77 +425,93 @@ public class KStream extends AbstractStream implements 
ISipCallbacks {
        public void release(boolean remove) {
                if (outgoingMedia != null) {
                        releaseListeners();
-                       releaseRecorder(false);
-                       releaseRtp();
-                       outgoingMedia.release(new Continuation<Void>() {
-                               @Override
-                               public void onSuccess(Void result) throws 
Exception {
-                                       log.trace("PARTICIPANT {}: Released 
successfully", KStream.this.uid);
-                               }
+                       stopRecorder(false, () -> {
+                               releaseRtp();
+                               outgoingMedia.release(new Continuation<Void>() {
+                                       @Override
+                                       public void onSuccess(Void result) 
throws Exception {
+                                               log.trace("PARTICIPANT {}: 
Released successfully", KStream.this.uid);
+                                       }
 
-                               @Override
-                               public void onError(Throwable cause) throws 
Exception {
-                                       log.warn("PARTICIPANT {}: Could not 
release", KStream.this.uid, cause);
-                               }
-                       });
-                       pipeline.release(new Continuation<Void>() {
-                               @Override
-                               public void onSuccess(Void result) throws 
Exception {
-                                       log.trace("PARTICIPANT {}: Released 
Pipeline", KStream.this.uid);
-                               }
+                                       @Override
+                                       public void onError(Throwable cause) 
throws Exception {
+                                               log.warn("PARTICIPANT {}: Could 
not release", KStream.this.uid, cause);
+                                       }
+                               });
+                               pipeline.release(new Continuation<Void>() {
+                                       @Override
+                                       public void onSuccess(Void result) 
throws Exception {
+                                               log.trace("PARTICIPANT {}: 
Released Pipeline", KStream.this.uid);
+                                       }
 
-                               @Override
-                               public void onError(Throwable cause) throws 
Exception {
-                                       log.warn("PARTICIPANT {}: Could not 
release Pipeline", KStream.this.uid, cause);
-                               }
+                                       @Override
+                                       public void onError(Throwable cause) 
throws Exception {
+                                               log.warn("PARTICIPANT {}: Could 
not release Pipeline", KStream.this.uid, cause);
+                                       }
+                               });
+                               outgoingMedia = null;
+                               doRemove(remove);
                        });
-                       outgoingMedia = null;
+               } else {
+                       doRemove(remove);
                }
+       }
+
+       private void doRemove(boolean remove) {
                if (remove) {
                        kHandler.getStreamProcessor().release(this, false);
                }
        }
 
-       private void releaseRecorder(boolean wait) {
-               if (recorder != null) {
-                       if (wait) {
-                               recorder.stopAndWait();
-                       } else {
-                               recorder.stop(new Continuation<Void>() {
-                                       @Override
-                                       public void onSuccess(Void result) 
throws Exception {
-                                               log.trace("PARTICIPANT {}: 
Recording stopped", KStream.this.uid);
-                                       }
+       private void releaseRecorder(Runnable then) {
+               outgoingMedia.disconnect(recorder, new Continuation<Void>() {
+                       @Override
+                       public void onSuccess(Void result) throws Exception {
+                               log.trace("PARTICIPANT {}: Recorder 
disconnected successfully", KStream.this.uid);
+                       }
 
-                                       @Override
-                                       public void onError(Throwable cause) 
throws Exception {
-                                               log.warn("PARTICIPANT {}: Could 
not stop recording", KStream.this.uid, cause);
-                                       }
-                               });
+                       @Override
+                       public void onError(Throwable cause) throws Exception {
+                               log.warn("PARTICIPANT {}: Could not disconnect 
recorder", KStream.this.uid, cause);
+                       }
+               });
+               recorder.release(new Continuation<Void>() {
+                       @Override
+                       public void onSuccess(Void result) throws Exception {
+                               log.trace("PARTICIPANT {}: Recorder released 
successfully", KStream.this.uid);
                        }
-                       outgoingMedia.disconnect(recorder, new 
Continuation<Void>() {
-                               @Override
-                               public void onSuccess(Void result) throws 
Exception {
-                                       log.trace("PARTICIPANT {}: Recorder 
disconnected successfully", KStream.this.uid);
-                               }
 
-                               @Override
-                               public void onError(Throwable cause) throws 
Exception {
-                                       log.warn("PARTICIPANT {}: Could not 
disconnect recorder", KStream.this.uid, cause);
-                               }
-                       });
-                       recorder.release(new Continuation<Void>() {
+                       @Override
+                       public void onError(Throwable cause) throws Exception {
+                               log.warn("PARTICIPANT {}: Could not release 
recorder", KStream.this.uid, cause);
+                       }
+               });
+               recorder = null;
+               then.run();
+       }
+
+       private void stopRecorder(boolean wait, Runnable then) {
+               if (recorder != null) {
+                       final Continuation<Void> stop = new Continuation<>() {
                                @Override
                                public void onSuccess(Void result) throws 
Exception {
-                                       log.trace("PARTICIPANT {}: Recorder 
released successfully", KStream.this.uid);
+                                       log.trace("PARTICIPANT {}: Recording 
stopped", KStream.this.uid);
+                                       releaseRecorder(then);
                                }
 
                                @Override
                                public void onError(Throwable cause) throws 
Exception {
-                                       log.warn("PARTICIPANT {}: Could not 
release recorder", KStream.this.uid, cause);
+                                       log.warn("PARTICIPANT {}: Could not 
stop recording", KStream.this.uid, cause);
+                                       releaseRecorder(then);
                                }
-                       });
-                       recorder = null;
+                       };
+                       if (wait) {
+                               recorder.stopAndWait(stop);
+                       } else {
+                               recorder.stop(stop);
+                       }
+               } else {
+                       then.run();
                }
        }
 
diff --git a/openmeetings-db/pom.xml b/openmeetings-db/pom.xml
index d64fccb..af04d2f 100644
--- a/openmeetings-db/pom.xml
+++ b/openmeetings-db/pom.xml
@@ -60,7 +60,6 @@
                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-context</artifactId>
-                       <version>${spring.version}</version>
                </dependency>
                <dependency>
                        <groupId>org.springframework</groupId>
diff --git 
a/openmeetings-util/src/main/java/org/apache/openmeetings/util/process/ProcessHelper.java
 
b/openmeetings-util/src/main/java/org/apache/openmeetings/util/process/ProcessHelper.java
index 262b60d..4c0f596 100644
--- 
a/openmeetings-util/src/main/java/org/apache/openmeetings/util/process/ProcessHelper.java
+++ 
b/openmeetings-util/src/main/java/org/apache/openmeetings/util/process/ProcessHelper.java
@@ -26,6 +26,7 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -71,15 +72,11 @@ public class ProcessHelper {
 
        private ProcessHelper() {}
 
-       private static String getCommand(String[] argv) {
-               StringBuilder tString = new StringBuilder();
-               for (int i = 0; i < argv.length; i++) {
-                       tString.append(argv[i]).append(" ");
-               }
-               return tString.toString();
+       private static String getCommand(List<String> argv) {
+               return String.join(" ", argv);
        }
 
-       private static void debugCommandStart(String desc, String[] argv) {
+       private static void debugCommandStart(String desc, List<String> argv) {
                if (log.isDebugEnabled()) {
                        log.debug("START {} ################# ", desc);
                        log.debug(getCommand(argv));
@@ -92,15 +89,15 @@ public class ProcessHelper {
                }
        }
 
-       public static ProcessResult executeScript(String process, String[] 
argv) {
-               return executeScript(process, argv, false);
+       public static ProcessResult exec(String process, List<String> argv) {
+               return exec(process, argv, false);
        }
 
-       public static ProcessResult executeScript(String process, String[] 
argv, boolean optional) {
-               return executeScript(process, argv, Map.of(), optional);
+       public static ProcessResult exec(String process, List<String> argv, 
boolean optional) {
+               return exec(process, argv, Map.of(), optional);
        }
 
-       private static ProcessResult executeScript(String process, String[] 
argv, Map<? extends String, ? extends String> env, boolean optional) {
+       private static ProcessResult exec(String process, List<String> argv, 
Map<? extends String, ? extends String> env, boolean optional) {
                ProcessResult res = new ProcessResult()
                                .setProcess(process)
                                .setOptional(optional);
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/install/InstallWizard.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/install/InstallWizard.java
index 0f8f322..fb52961 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/install/InstallWizard.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/install/InstallWizard.java
@@ -618,8 +618,9 @@ public class InstallWizard extends BootstrapWizard {
                private void reportSuccess(TextField<String> path) {
                        path.success(path.getLabel().getObject() + " - " + 
getString("54"));
                }
-               private boolean checkToolPath(TextField<String> path, String[] 
args) {
-                       ProcessResult result = 
ProcessHelper.executeScript(path.getInputName() + " path:: '" + path.getValue() 
+ "'", args);
+
+               private boolean checkToolPath(TextField<String> path, 
List<String> args) {
+                       ProcessResult result = 
ProcessHelper.exec(path.getInputName() + " path:: '" + path.getValue() + "'", 
args);
                        if (!result.isOk()) {
                                path.error(result.getError().replaceAll(REGEX, 
""));
                        } else {
@@ -629,15 +630,15 @@ public class InstallWizard extends BootstrapWizard {
                }
 
                private boolean checkMagicPath() {
-                       return checkToolPath(imageMagicPath, new String[] 
{getToolPath(imageMagicPath.getValue(), "convert" + EXEC_EXT), OPT_VERSION});
+                       return checkToolPath(imageMagicPath, 
List.of(getToolPath(imageMagicPath.getValue(), "convert" + EXEC_EXT), 
OPT_VERSION));
                }
 
                private boolean checkFfmpegPath() {
-                       return checkToolPath(ffmpegPath, new String[] 
{getToolPath(ffmpegPath.getValue(), "ffmpeg" + EXEC_EXT), OPT_VERSION});
+                       return checkToolPath(ffmpegPath, 
List.of(getToolPath(ffmpegPath.getValue(), "ffmpeg" + EXEC_EXT), OPT_VERSION));
                }
 
                private boolean checkSoxPath() {
-                       return checkToolPath(soxPath, new String[] 
{getToolPath(soxPath.getValue(), "sox" + EXEC_EXT), "--version"});
+                       return checkToolPath(soxPath, 
List.of(getToolPath(soxPath.getValue(), "sox" + EXEC_EXT), "--version"));
                }
 
                private boolean checkOfficePath() {

Reply via email to