diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8d68ce..ce1a5a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 3.10.8 ======================== * Fixed Bongacams "New" tab +* Added setting to switch FFmpeg logging on/off (category Advanced/Devtools) 3.10.7 ======================== diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 40084cde..a7566dfd 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -115,6 +115,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private SimpleBooleanProperty onlineCheckSkipsPausedModels; private SimpleLongProperty leaveSpaceOnDevice; private SimpleStringProperty ffmpegParameters; + private SimpleBooleanProperty logFFmpegOutput; private SimpleStringProperty fileExtension; private SimpleStringProperty server; private SimpleIntegerProperty port; @@ -165,6 +166,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs); leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes)); ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs); + logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput); fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix); server = new SimpleStringProperty(null, "httpServer", settings.httpServer); port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort); @@ -254,6 +256,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { Setting.of("Username", proxyUser).needsRestart(), Setting.of("Password", proxyPassword).needsRestart() ) + ), + Category.of("Advanced / Devtools", + Group.of("Logging", + Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory") + ) ) ); Region preferencesView = prefs.getView(); diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 76eec655..48560ab0 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -151,8 +151,12 @@ public class Config { if (oldLocation.exists()) { File newLocation = new File(getConfigDir(), oldLocation.getName()); try { - LOG.debug("Moving minimal browser config {} --> {}", oldLocation, newLocation); - FileUtils.moveDirectory(oldLocation, newLocation); + if (!newLocation.exists()) { + LOG.debug("Moving minimal browser config {} --> {}", oldLocation, newLocation); + FileUtils.moveDirectory(oldLocation, newLocation); + } else { + LOG.debug("minimal browser settings have been migrated before"); + } } catch (IOException e) { LOG.error("Couldn't migrate minimal browser config location", e); } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 49cfa237..804d759b 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -80,6 +80,7 @@ public class Settings { public String livejasminUsername = ""; public boolean livePreviews = false; public boolean localRecording = true; + public boolean logFFmpegOutput = false; public int minimumResolution = 0; public int maximumResolution = 8640; public int maximumResolutionPlayer = 0; diff --git a/common/src/main/java/ctbrec/recorder/FFmpeg.java b/common/src/main/java/ctbrec/recorder/FFmpeg.java new file mode 100644 index 00000000..fdfa3bb1 --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/FFmpeg.java @@ -0,0 +1,135 @@ +package ctbrec.recorder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.DevNull; +import ctbrec.io.StreamRedirector; +import ctbrec.recorder.download.ProcessExitedUncleanException; + +public class FFmpeg { + + private static final Logger LOG = LoggerFactory.getLogger(FFmpeg.class); + + private Process process; + private boolean logOutput = false; + private Consumer startCallback; + private Consumer exitCallback; + private File ffmpegLog = null; + private OutputStream ffmpegLogStream; + private Thread stdout; + private Thread stderr; + + private FFmpeg() {} + + public void exec(String[] cmdline, String[] env, File executionDir) throws IOException, InterruptedException { + LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline)); + process = Runtime.getRuntime().exec(cmdline, env, executionDir); + afterStart(); + int exitCode = process.waitFor(); + afterExit(exitCode); + } + + private void afterStart() throws IOException { + notifyStartCallback(process); + setupLogging(); + } + + private void afterExit(int exitCode) throws InterruptedException, IOException { + LOG.debug("FFmpeg exit code was {}", exitCode); + notifyExitCallback(exitCode); + stdout.join(); + stderr.join(); + ffmpegLogStream.flush(); + ffmpegLogStream.close(); + if (exitCode != 1) { + if (ffmpegLog != null && ffmpegLog.exists()) { + Files.delete(ffmpegLog.toPath()); + } + } else { + throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode); + } + } + + private void setupLogging() throws IOException { + if (logOutput) { + if (ffmpegLog == null) { + ffmpegLog = File.createTempFile("ffmpeg_", ".log"); + } + LOG.debug("Logging FFmpeg output to {}", ffmpegLog); + ffmpegLog.deleteOnExit(); + ffmpegLogStream = new FileOutputStream(ffmpegLog); + } else { + ffmpegLogStream = new DevNull(); + } + stdout = new Thread(new StreamRedirector(process.getInputStream(), ffmpegLogStream)); + stderr = new Thread(new StreamRedirector(process.getErrorStream(), ffmpegLogStream)); + stdout.start(); + stderr.start(); + } + + private void notifyStartCallback(Process process) { + try { + startCallback.accept(process); + } catch(Exception e) { + LOG.error("Exception in onStart callback", e); + } + } + + private void notifyExitCallback(int exitCode) { + try { + exitCallback.accept(exitCode); + } catch(Exception e) { + LOG.error("Exception in onExit callback", e); + } + } + + public int waitFor() throws InterruptedException { + return process.waitFor(); + } + + public static class Builder { + private boolean logOutput = false; + private File logFile; + private Consumer startCallback; + private Consumer exitCallback; + + public Builder logOutput(boolean logOutput) { + this.logOutput = logOutput; + return this; + } + + public Builder logFile(File logFile) { + this.logFile = logFile; + return this; + } + + public Builder onStarted(Consumer callback) { + this.startCallback = callback; + return this; + } + + public Builder onExit(Consumer callback) { + this.exitCallback = callback; + return this; + } + + public FFmpeg build() { + FFmpeg instance = new FFmpeg(); + instance.logOutput = logOutput; + instance.startCallback = startCallback != null ? startCallback : p -> {}; + instance.exitCallback = exitCallback != null ? exitCallback : exitCode -> {}; + instance.ffmpegLog = logFile; + return instance; + } + } + +} diff --git a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java index 136a159f..00713d63 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java @@ -4,7 +4,6 @@ import static java.util.Optional.*; import java.io.EOFException; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -12,7 +11,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.time.Instant; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.Queue; @@ -38,7 +36,7 @@ import ctbrec.Recording; import ctbrec.io.BandwidthMeter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; -import ctbrec.io.StreamRedirector; +import ctbrec.recorder.FFmpeg; import ctbrec.recorder.ProgressListener; import ctbrec.recorder.download.HttpHeaderFactory; import ctbrec.recorder.download.ProcessExitedUncleanException; @@ -53,11 +51,13 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { private static final boolean IGNORE_CACHE = true; private File targetFile; private transient Config config; - private transient Process ffmpeg; + private transient Process ffmpegProcess; private transient OutputStream ffmpegStdIn; protected transient Thread ffmpegThread; private transient Object ffmpegStartMonitor = new Object(); private transient Queue> downloads = new LinkedList<>(); + private transient int lastSegment = 0; + private transient int nextSegment = 0; public MergedFfmpegHlsDownload(HttpClient client) { super(client); @@ -91,17 +91,17 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { startFfmpegProcess(targetFile); synchronized (ffmpegStartMonitor) { int tries = 0; - while (ffmpeg == null && tries++ < 15) { + while (ffmpegProcess == null && tries++ < 15) { LOG.debug("Waiting for FFmpeg to spawn to record {}", model.getName()); ffmpegStartMonitor.wait(1000); } } - if (ffmpeg == null) { + if (ffmpegProcess == null) { throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg"); } else { LOG.debug("Starting to download segments"); - downloadSegments(segments, true); + startDownloadLoop(segments, true); ffmpegThread.join(); LOG.debug("FFmpeg thread terminated"); } @@ -131,47 +131,20 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { private void startFfmpegProcess(File target) { ffmpegThread = new Thread(() -> { try { - String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" "); - String[] argsPlusFile = new String[args.length + 3]; - int i = 0; - argsPlusFile[i++] = "-i"; - argsPlusFile[i++] = "-"; - System.arraycopy(args, 0, argsPlusFile, i, args.length); - argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath(); - String[] cmdline = OS.getFFmpegCommand(argsPlusFile); - - LOG.debug("Command line: {}", Arrays.toString(cmdline)); - ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], target.getParentFile()); - synchronized (ffmpegStartMonitor) { - ffmpegStartMonitor.notifyAll(); - } - ffmpegStdIn = ffmpeg.getOutputStream(); - int exitCode = 1; - File ffmpegLog = File.createTempFile(target.getName(), ".log"); - ffmpegLog.deleteOnExit(); - try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) { - Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), mergeLogStream)); - Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), mergeLogStream)); - stdout.start(); - stderr.start(); - exitCode = ffmpeg.waitFor(); - LOG.debug("FFmpeg exited with code {}", exitCode); - stdout.join(); - stderr.join(); - mergeLogStream.flush(); - } - if (exitCode != 1) { - if (ffmpegLog.exists()) { - Files.delete(ffmpegLog.toPath()); - } - } else { - if (running) { - LOG.info("FFmpeg exit code was {}. Logfile: {}", exitCode, ffmpegLog.getAbsolutePath()); - throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode); - } - } + String[] cmdline = prepareCommandLine(target); + FFmpeg ffmpeg = new FFmpeg.Builder() + .logOutput(config.getSettings().logFFmpegOutput) + .onStarted(p -> { + ffmpegProcess = p; + ffmpegStdIn = ffmpegProcess.getOutputStream(); + synchronized (ffmpegStartMonitor) { + ffmpegStartMonitor.notifyAll(); + } + }) + .build(); + ffmpeg.exec(cmdline, new String[0], target.getParentFile()); } catch (IOException | ProcessExitedUncleanException e) { - LOG.error("Error in FFMpeg thread", e); + LOG.error("Error in FFmpeg thread", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); if (running) { @@ -184,48 +157,23 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { ffmpegThread.start(); } - protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException { - int lastSegment = 0; - int nextSegment = 0; + private String[] prepareCommandLine(File target) { + String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" "); + String[] argsPlusFile = new String[args.length + 3]; + int i = 0; + argsPlusFile[i++] = "-i"; + argsPlusFile[i++] = "-"; + System.arraycopy(args, 0, argsPlusFile, i, args.length); + argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath(); + return OS.getFFmpegCommand(argsPlusFile); + } + + protected void startDownloadLoop(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException { while (running) { try { - SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri); - emptyPlaylistCheck(lsp); - - // download new segments - long downloadStart = System.currentTimeMillis(); - if (livestreamDownload) { - downloadNewSegments(lsp, nextSegment); - } else { - downloadRecording(lsp); - } - long downloadTookMillis = System.currentTimeMillis() - downloadStart; - - // download segments, which might have been skipped - if (nextSegment > 0 && lsp.seq > nextSegment) { - LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url, - downloadTookMillis, lsp.totalDuration); - } - - if (livestreamDownload) { - splitRecordingIfNecessary(); - - // wait some time until requesting the segment playlist again to not hammer the server - waitForNewSegments(lsp, lastSegment, downloadTookMillis); - - lastSegment = lsp.seq; - nextSegment = lastSegment + lsp.segments.size(); - } else { - break; - } + downloadSegments(segmentPlaylistUri, livestreamDownload); } catch (HttpException e) { - if (e.getResponseCode() == 404) { - LOG.debug("Playlist not found (404). Model {} probably went offline", model); - } else if (e.getResponseCode() == 403) { - LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model); - } else { - LOG.info("Unexpected error while downloading {}", model, e); - } + logHttpException(e); running = false; } catch (MalformedURLException e) { LOG.info("Malformed URL {} - {}", model, segmentPlaylistUri, e); @@ -238,6 +186,48 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { ffmpegThread.interrupt(); } + private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException, ExecutionException { + SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri); + emptyPlaylistCheck(lsp); + + // download new segments + long downloadStart = System.currentTimeMillis(); + if (livestreamDownload) { + downloadNewSegments(lsp, nextSegment); + } else { + downloadRecording(lsp); + } + long downloadTookMillis = System.currentTimeMillis() - downloadStart; + + // download segments, which might have been skipped + if (nextSegment > 0 && lsp.seq > nextSegment) { + LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url, + downloadTookMillis, lsp.totalDuration); + } + + if (livestreamDownload) { + splitRecordingIfNecessary(); + + // wait some time until requesting the segment playlist again to not hammer the server + waitForNewSegments(lsp, lastSegment, downloadTookMillis); + + lastSegment = lsp.seq; + nextSegment = lastSegment + lsp.segments.size(); + } else { + running = false; + } + } + + private void logHttpException(HttpException e) { + if (e.getResponseCode() == 404) { + LOG.debug("Playlist not found (404). Model {} probably went offline", model); + } else if (e.getResponseCode() == 403) { + LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model); + } else { + LOG.info("Unexpected error while downloading {}", model, e); + } + } + protected void splitRecordingIfNecessary() { if (splittingStrategy.splitNecessary(this)) { internalStop(); @@ -291,33 +281,40 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { } catch (CancellationException e) { LOG.info("Segment download cancelled"); } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof MissingSegmentException) { - if (model != null && !isModelOnline()) { - LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName()); - running = false; - } else { - LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a")); - } - } else if (cause instanceof HttpException) { - HttpException he = (HttpException) cause; - if (model != null && !isModelOnline()) { - LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName()); - running = false; - } else { - if (he.getResponseCode() == 404) { - LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a")); - running = false; - } else if (he.getResponseCode() == 403) { - LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a")); - running = false; - } else { - throw he; - } - } - } else { - throw e; - } + handleExecutionExceptione(e); + } + } + } + + private void handleExecutionExceptione(ExecutionException e) throws HttpException, ExecutionException { + Throwable cause = e.getCause(); + if (cause instanceof MissingSegmentException) { + if (model != null && !isModelOnline()) { + LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName()); + running = false; + } else { + LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a")); + } + } else if (cause instanceof HttpException) { + handleHttpException((HttpException)cause); + } else { + throw e; + } + } + + private void handleHttpException(HttpException he) throws HttpException { + if (model != null && !isModelOnline()) { + LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName()); + running = false; + } else { + if (he.getResponseCode() == 404) { + LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a")); + running = false; + } else if (he.getResponseCode() == 403) { + LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a")); + running = false; + } else { + throw he; } } } @@ -396,15 +393,15 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { } } - if (ffmpeg != null) { + if (ffmpegProcess != null) { try { - boolean waitFor = ffmpeg.waitFor(45, TimeUnit.SECONDS); - if (!waitFor && ffmpeg.isAlive()) { - ffmpeg.destroy(); - if (ffmpeg.isAlive()) { + boolean waitFor = ffmpegProcess.waitFor(45, TimeUnit.SECONDS); + if (!waitFor && ffmpegProcess.isAlive()) { + ffmpegProcess.destroy(); + if (ffmpegProcess.isAlive()) { LOG.info("FFmpeg didn't terminate. Destroying the process with force!"); - ffmpeg.destroyForcibly(); - ffmpeg = null; + ffmpegProcess.destroyForcibly(); + ffmpegProcess = null; } } } catch (InterruptedException e) { diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java b/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java index 7db00d4c..f8a8bace 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java @@ -1,9 +1,7 @@ package ctbrec.recorder.postprocessing; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.nio.file.Files; import java.text.MessageFormat; import java.util.Arrays; import java.util.Locale; @@ -14,7 +12,7 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.OS; import ctbrec.Recording; -import ctbrec.io.StreamRedirector; +import ctbrec.recorder.FFmpeg; import ctbrec.recorder.RecordingManager; public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor { @@ -80,28 +78,15 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor { }; String[] cmdline = OS.getFFmpegCommand(args); LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir); - Process ffmpeg = Runtime.getRuntime().exec(cmdline, OS.getEnvironment(), executionDir); - int exitCode = 1; - File ffmpegLog = File.createTempFile("create_contact_sheet_" + rec.getId() + '_', ".log"); - ffmpegLog.deleteOnExit(); - try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) { - Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), mergeLogStream)); - Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), mergeLogStream)); - stdout.start(); - stderr.start(); - exitCode = ffmpeg.waitFor(); - LOG.debug("FFmpeg exited with code {}", exitCode); - stdout.join(); - stderr.join(); - mergeLogStream.flush(); - } + File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "create_contact_sheet_" + rec.getId() + ".log"); + FFmpeg ffmpeg = new FFmpeg.Builder() + .logOutput(config.getSettings().logFFmpegOutput) + .logFile(ffmpegLog) + .build(); + ffmpeg.exec(cmdline, OS.getEnvironment(), executionDir); + int exitCode = ffmpeg.waitFor(); rec.getAssociatedFiles().add(output.getCanonicalPath()); - if (exitCode != 1) { - if (ffmpegLog.exists()) { - Files.delete(ffmpegLog.toPath()); - } - } - return true; + return exitCode != 1; } private File getInputFile(Recording rec) { diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/Remux.java b/common/src/main/java/ctbrec/recorder/postprocessing/Remux.java index 941989bb..fa3f3bc2 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/Remux.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/Remux.java @@ -1,7 +1,6 @@ package ctbrec.recorder.postprocessing; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.util.Arrays; @@ -14,9 +13,8 @@ import ctbrec.Config; import ctbrec.OS; import ctbrec.Recording; import ctbrec.io.IoUtils; -import ctbrec.io.StreamRedirector; +import ctbrec.recorder.FFmpeg; import ctbrec.recorder.RecordingManager; -import ctbrec.recorder.download.ProcessExitedUncleanException; public class Remux extends AbstractPostProcessor { @@ -24,6 +22,8 @@ public class Remux extends AbstractPostProcessor { public static final String FFMPEG_ARGS = "ffmpeg.args"; public static final String FILE_EXT = "file.ext"; + private transient File inputFile; + private transient File remuxedFile; @Override public String getName() { @@ -32,70 +32,62 @@ public class Remux extends AbstractPostProcessor { @Override public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException { - String fileExt = getConfig().get(FILE_EXT); - String[] args = getConfig().get(FFMPEG_ARGS).split(" "); - String[] argsPlusFile = new String[args.length + 3]; - File inputFile = rec.getPostProcessedFile(); + inputFile = rec.getPostProcessedFile(); if (inputFile.isDirectory()) { inputFile = new File(inputFile, "playlist.m3u8"); } + String fileExt = getConfig().get(FILE_EXT); + remuxedFile = new File(rec.getPostProcessedFile().getAbsolutePath() + '.' + fileExt); + String[] cmdline = prepareCommandline(inputFile, remuxedFile); + File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile(); + LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir); + + File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "remux_" + rec.getId() + ".log"); + FFmpeg ffmpeg = new FFmpeg.Builder() + .logOutput(config.getSettings().logFFmpegOutput) + .logFile(ffmpegLog) + .onExit(exitCode -> finalizeStep(exitCode, rec)) + .build(); + ffmpeg.exec(cmdline, new String[0], executionDir); + int exitCode = ffmpeg.waitFor(); + return exitCode != 1; + } + + private void finalizeStep(int exitCode, Recording rec) { + if (exitCode != 1) { + try { + rec.setPostProcessedFile(remuxedFile); + if (inputFile.getName().equals("playlist.m3u8")) { + IoUtils.deleteDirectory(inputFile.getParentFile()); + if (Objects.equals(inputFile.getParentFile(), rec.getAbsoluteFile())) { + rec.setAbsoluteFile(remuxedFile); + } + } else { + Files.deleteIfExists(inputFile.toPath()); + if (Objects.equals(inputFile, rec.getAbsoluteFile())) { + rec.setAbsoluteFile(remuxedFile); + } + } + rec.setSingleFile(true); + rec.setSizeInByte(remuxedFile.length()); + IoUtils.deleteEmptyParents(inputFile.getParentFile()); + rec.getAssociatedFiles().remove(inputFile.getCanonicalPath()); + rec.getAssociatedFiles().add(remuxedFile.getCanonicalPath()); + } catch (IOException e) { + LOG.error("Couldn't finalize remux post-processing step", e); + } + } + } + + private String[] prepareCommandline(File inputFile, File remuxedFile) throws IOException { + String[] args = getConfig().get(FFMPEG_ARGS).split(" "); + String[] argsPlusFile = new String[args.length + 3]; int i = 0; argsPlusFile[i++] = "-i"; argsPlusFile[i++] = inputFile.getCanonicalPath(); System.arraycopy(args, 0, argsPlusFile, i, args.length); - File remuxedFile = new File(rec.getPostProcessedFile().getAbsolutePath() + '.' + fileExt); argsPlusFile[argsPlusFile.length - 1] = remuxedFile.getAbsolutePath(); - String[] cmdline = OS.getFFmpegCommand(argsPlusFile); - File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile(); - LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir); - Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], executionDir); - setupLogging(ffmpeg, rec); - rec.setPostProcessedFile(remuxedFile); - if (inputFile.getName().equals("playlist.m3u8")) { - IoUtils.deleteDirectory(inputFile.getParentFile()); - if (Objects.equals(inputFile.getParentFile(), rec.getAbsoluteFile())) { - rec.setAbsoluteFile(remuxedFile); - } - } else { - Files.deleteIfExists(inputFile.toPath()); - if (Objects.equals(inputFile, rec.getAbsoluteFile())) { - rec.setAbsoluteFile(remuxedFile); - } - } - rec.setSingleFile(true); - rec.setSizeInByte(remuxedFile.length()); - IoUtils.deleteEmptyParents(inputFile.getParentFile()); - rec.getAssociatedFiles().remove(inputFile.getCanonicalPath()); - rec.getAssociatedFiles().add(remuxedFile.getCanonicalPath()); - return true; - } - - private void setupLogging(Process ffmpeg, Recording rec) throws IOException, InterruptedException { - int exitCode = 1; - File video = rec.getPostProcessedFile(); - File ffmpegLog = new File(video.getParentFile(), video.getName() + ".ffmpeg.log"); - rec.getAssociatedFiles().add(ffmpegLog.getCanonicalPath()); - try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) { - Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), mergeLogStream)); - Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), mergeLogStream)); - stdout.start(); - stderr.start(); - exitCode = ffmpeg.waitFor(); - LOG.debug("FFmpeg exited with code {}", exitCode); - stdout.join(); - stderr.join(); - mergeLogStream.flush(); - } - if (exitCode != 1) { - if (ffmpegLog.exists()) { - Files.delete(ffmpegLog.toPath()); - rec.getAssociatedFiles().remove(ffmpegLog.getCanonicalPath()); - } - } else { - rec.getAssociatedFiles().add(ffmpegLog.getAbsolutePath()); - LOG.info("FFmpeg exit code was {}. Logfile: {}", exitCode, ffmpegLog.getAbsolutePath()); - throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode); - } + return OS.getFFmpegCommand(argsPlusFile); } @Override diff --git a/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java b/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java index 78a33af3..28537c45 100644 --- a/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java +++ b/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java @@ -28,7 +28,7 @@ public class ShowupMergedDownload extends MergedFfmpegHlsDownload { } @Override - protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException { + protected void startDownloadLoop(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException { try { SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri); emptyPlaylistCheck(lsp);