forked from j62/ctbrec
Add setting to switch FFmpeg logging on / off
This commit is contained in:
parent
ae14844170
commit
605269b4a0
|
@ -1,6 +1,7 @@
|
||||||
3.10.8
|
3.10.8
|
||||||
========================
|
========================
|
||||||
* Fixed Bongacams "New" tab
|
* Fixed Bongacams "New" tab
|
||||||
|
* Added setting to switch FFmpeg logging on/off (category Advanced/Devtools)
|
||||||
|
|
||||||
3.10.7
|
3.10.7
|
||||||
========================
|
========================
|
||||||
|
|
|
@ -115,6 +115,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private SimpleBooleanProperty onlineCheckSkipsPausedModels;
|
private SimpleBooleanProperty onlineCheckSkipsPausedModels;
|
||||||
private SimpleLongProperty leaveSpaceOnDevice;
|
private SimpleLongProperty leaveSpaceOnDevice;
|
||||||
private SimpleStringProperty ffmpegParameters;
|
private SimpleStringProperty ffmpegParameters;
|
||||||
|
private SimpleBooleanProperty logFFmpegOutput;
|
||||||
private SimpleStringProperty fileExtension;
|
private SimpleStringProperty fileExtension;
|
||||||
private SimpleStringProperty server;
|
private SimpleStringProperty server;
|
||||||
private SimpleIntegerProperty port;
|
private SimpleIntegerProperty port;
|
||||||
|
@ -165,6 +166,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs);
|
onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs);
|
||||||
leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
|
leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
|
||||||
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
|
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
|
||||||
|
logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
|
||||||
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
|
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
|
||||||
server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
|
server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
|
||||||
port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
|
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("Username", proxyUser).needsRestart(),
|
||||||
Setting.of("Password", proxyPassword).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();
|
Region preferencesView = prefs.getView();
|
||||||
|
|
|
@ -151,8 +151,12 @@ public class Config {
|
||||||
if (oldLocation.exists()) {
|
if (oldLocation.exists()) {
|
||||||
File newLocation = new File(getConfigDir(), oldLocation.getName());
|
File newLocation = new File(getConfigDir(), oldLocation.getName());
|
||||||
try {
|
try {
|
||||||
LOG.debug("Moving minimal browser config {} --> {}", oldLocation, newLocation);
|
if (!newLocation.exists()) {
|
||||||
FileUtils.moveDirectory(oldLocation, newLocation);
|
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) {
|
} catch (IOException e) {
|
||||||
LOG.error("Couldn't migrate minimal browser config location", e);
|
LOG.error("Couldn't migrate minimal browser config location", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,7 @@ public class Settings {
|
||||||
public String livejasminUsername = "";
|
public String livejasminUsername = "";
|
||||||
public boolean livePreviews = false;
|
public boolean livePreviews = false;
|
||||||
public boolean localRecording = true;
|
public boolean localRecording = true;
|
||||||
|
public boolean logFFmpegOutput = false;
|
||||||
public int minimumResolution = 0;
|
public int minimumResolution = 0;
|
||||||
public int maximumResolution = 8640;
|
public int maximumResolution = 8640;
|
||||||
public int maximumResolutionPlayer = 0;
|
public int maximumResolutionPlayer = 0;
|
||||||
|
|
|
@ -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<Process> startCallback;
|
||||||
|
private Consumer<Integer> 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<Process> startCallback;
|
||||||
|
private Consumer<Integer> 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<Process> callback) {
|
||||||
|
this.startCallback = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder onExit(Consumer<Integer> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import static java.util.Optional.*;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
@ -12,7 +11,6 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
@ -38,7 +36,7 @@ import ctbrec.Recording;
|
||||||
import ctbrec.io.BandwidthMeter;
|
import ctbrec.io.BandwidthMeter;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.io.StreamRedirector;
|
import ctbrec.recorder.FFmpeg;
|
||||||
import ctbrec.recorder.ProgressListener;
|
import ctbrec.recorder.ProgressListener;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
@ -53,11 +51,13 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
private static final boolean IGNORE_CACHE = true;
|
private static final boolean IGNORE_CACHE = true;
|
||||||
private File targetFile;
|
private File targetFile;
|
||||||
private transient Config config;
|
private transient Config config;
|
||||||
private transient Process ffmpeg;
|
private transient Process ffmpegProcess;
|
||||||
private transient OutputStream ffmpegStdIn;
|
private transient OutputStream ffmpegStdIn;
|
||||||
protected transient Thread ffmpegThread;
|
protected transient Thread ffmpegThread;
|
||||||
private transient Object ffmpegStartMonitor = new Object();
|
private transient Object ffmpegStartMonitor = new Object();
|
||||||
private transient Queue<Future<byte[]>> downloads = new LinkedList<>();
|
private transient Queue<Future<byte[]>> downloads = new LinkedList<>();
|
||||||
|
private transient int lastSegment = 0;
|
||||||
|
private transient int nextSegment = 0;
|
||||||
|
|
||||||
public MergedFfmpegHlsDownload(HttpClient client) {
|
public MergedFfmpegHlsDownload(HttpClient client) {
|
||||||
super(client);
|
super(client);
|
||||||
|
@ -91,17 +91,17 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
startFfmpegProcess(targetFile);
|
startFfmpegProcess(targetFile);
|
||||||
synchronized (ffmpegStartMonitor) {
|
synchronized (ffmpegStartMonitor) {
|
||||||
int tries = 0;
|
int tries = 0;
|
||||||
while (ffmpeg == null && tries++ < 15) {
|
while (ffmpegProcess == null && tries++ < 15) {
|
||||||
LOG.debug("Waiting for FFmpeg to spawn to record {}", model.getName());
|
LOG.debug("Waiting for FFmpeg to spawn to record {}", model.getName());
|
||||||
ffmpegStartMonitor.wait(1000);
|
ffmpegStartMonitor.wait(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ffmpeg == null) {
|
if (ffmpegProcess == null) {
|
||||||
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("Starting to download segments");
|
LOG.debug("Starting to download segments");
|
||||||
downloadSegments(segments, true);
|
startDownloadLoop(segments, true);
|
||||||
ffmpegThread.join();
|
ffmpegThread.join();
|
||||||
LOG.debug("FFmpeg thread terminated");
|
LOG.debug("FFmpeg thread terminated");
|
||||||
}
|
}
|
||||||
|
@ -131,47 +131,20 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
private void startFfmpegProcess(File target) {
|
private void startFfmpegProcess(File target) {
|
||||||
ffmpegThread = new Thread(() -> {
|
ffmpegThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" ");
|
String[] cmdline = prepareCommandLine(target);
|
||||||
String[] argsPlusFile = new String[args.length + 3];
|
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||||
int i = 0;
|
.logOutput(config.getSettings().logFFmpegOutput)
|
||||||
argsPlusFile[i++] = "-i";
|
.onStarted(p -> {
|
||||||
argsPlusFile[i++] = "-";
|
ffmpegProcess = p;
|
||||||
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
ffmpegStdIn = ffmpegProcess.getOutputStream();
|
||||||
argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath();
|
synchronized (ffmpegStartMonitor) {
|
||||||
String[] cmdline = OS.getFFmpegCommand(argsPlusFile);
|
ffmpegStartMonitor.notifyAll();
|
||||||
|
}
|
||||||
LOG.debug("Command line: {}", Arrays.toString(cmdline));
|
})
|
||||||
ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], target.getParentFile());
|
.build();
|
||||||
synchronized (ffmpegStartMonitor) {
|
ffmpeg.exec(cmdline, new String[0], target.getParentFile());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException | ProcessExitedUncleanException e) {
|
} catch (IOException | ProcessExitedUncleanException e) {
|
||||||
LOG.error("Error in FFMpeg thread", e);
|
LOG.error("Error in FFmpeg thread", e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
if (running) {
|
if (running) {
|
||||||
|
@ -184,48 +157,23 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
ffmpegThread.start();
|
ffmpegThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
private String[] prepareCommandLine(File target) {
|
||||||
int lastSegment = 0;
|
String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" ");
|
||||||
int nextSegment = 0;
|
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) {
|
while (running) {
|
||||||
try {
|
try {
|
||||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
downloadSegments(segmentPlaylistUri, livestreamDownload);
|
||||||
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;
|
|
||||||
}
|
|
||||||
} catch (HttpException e) {
|
} catch (HttpException e) {
|
||||||
if (e.getResponseCode() == 404) {
|
logHttpException(e);
|
||||||
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);
|
|
||||||
}
|
|
||||||
running = false;
|
running = false;
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
LOG.info("Malformed URL {} - {}", model, segmentPlaylistUri, e);
|
LOG.info("Malformed URL {} - {}", model, segmentPlaylistUri, e);
|
||||||
|
@ -238,6 +186,48 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
ffmpegThread.interrupt();
|
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() {
|
protected void splitRecordingIfNecessary() {
|
||||||
if (splittingStrategy.splitNecessary(this)) {
|
if (splittingStrategy.splitNecessary(this)) {
|
||||||
internalStop();
|
internalStop();
|
||||||
|
@ -291,33 +281,40 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
} catch (CancellationException e) {
|
} catch (CancellationException e) {
|
||||||
LOG.info("Segment download cancelled");
|
LOG.info("Segment download cancelled");
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
Throwable cause = e.getCause();
|
handleExecutionExceptione(e);
|
||||||
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 {
|
private void handleExecutionExceptione(ExecutionException e) throws HttpException, ExecutionException {
|
||||||
LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a"));
|
Throwable cause = e.getCause();
|
||||||
}
|
if (cause instanceof MissingSegmentException) {
|
||||||
} else if (cause instanceof HttpException) {
|
if (model != null && !isModelOnline()) {
|
||||||
HttpException he = (HttpException) cause;
|
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
||||||
if (model != null && !isModelOnline()) {
|
running = false;
|
||||||
LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName());
|
} else {
|
||||||
running = false;
|
LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
} else {
|
}
|
||||||
if (he.getResponseCode() == 404) {
|
} else if (cause instanceof HttpException) {
|
||||||
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
handleHttpException((HttpException)cause);
|
||||||
running = false;
|
} else {
|
||||||
} else if (he.getResponseCode() == 403) {
|
throw e;
|
||||||
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
}
|
||||||
running = false;
|
}
|
||||||
} else {
|
|
||||||
throw he;
|
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());
|
||||||
} else {
|
running = false;
|
||||||
throw e;
|
} 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 {
|
try {
|
||||||
boolean waitFor = ffmpeg.waitFor(45, TimeUnit.SECONDS);
|
boolean waitFor = ffmpegProcess.waitFor(45, TimeUnit.SECONDS);
|
||||||
if (!waitFor && ffmpeg.isAlive()) {
|
if (!waitFor && ffmpegProcess.isAlive()) {
|
||||||
ffmpeg.destroy();
|
ffmpegProcess.destroy();
|
||||||
if (ffmpeg.isAlive()) {
|
if (ffmpegProcess.isAlive()) {
|
||||||
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
||||||
ffmpeg.destroyForcibly();
|
ffmpegProcess.destroyForcibly();
|
||||||
ffmpeg = null;
|
ffmpegProcess = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package ctbrec.recorder.postprocessing;
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -14,7 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.OS;
|
import ctbrec.OS;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.io.StreamRedirector;
|
import ctbrec.recorder.FFmpeg;
|
||||||
import ctbrec.recorder.RecordingManager;
|
import ctbrec.recorder.RecordingManager;
|
||||||
|
|
||||||
public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
|
@ -80,28 +78,15 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
};
|
};
|
||||||
String[] cmdline = OS.getFFmpegCommand(args);
|
String[] cmdline = OS.getFFmpegCommand(args);
|
||||||
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
||||||
Process ffmpeg = Runtime.getRuntime().exec(cmdline, OS.getEnvironment(), executionDir);
|
File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "create_contact_sheet_" + rec.getId() + ".log");
|
||||||
int exitCode = 1;
|
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||||
File ffmpegLog = File.createTempFile("create_contact_sheet_" + rec.getId() + '_', ".log");
|
.logOutput(config.getSettings().logFFmpegOutput)
|
||||||
ffmpegLog.deleteOnExit();
|
.logFile(ffmpegLog)
|
||||||
try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) {
|
.build();
|
||||||
Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), mergeLogStream));
|
ffmpeg.exec(cmdline, OS.getEnvironment(), executionDir);
|
||||||
Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), mergeLogStream));
|
int exitCode = ffmpeg.waitFor();
|
||||||
stdout.start();
|
|
||||||
stderr.start();
|
|
||||||
exitCode = ffmpeg.waitFor();
|
|
||||||
LOG.debug("FFmpeg exited with code {}", exitCode);
|
|
||||||
stdout.join();
|
|
||||||
stderr.join();
|
|
||||||
mergeLogStream.flush();
|
|
||||||
}
|
|
||||||
rec.getAssociatedFiles().add(output.getCanonicalPath());
|
rec.getAssociatedFiles().add(output.getCanonicalPath());
|
||||||
if (exitCode != 1) {
|
return exitCode != 1;
|
||||||
if (ffmpegLog.exists()) {
|
|
||||||
Files.delete(ffmpegLog.toPath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getInputFile(Recording rec) {
|
private File getInputFile(Recording rec) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ctbrec.recorder.postprocessing;
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -14,9 +13,8 @@ import ctbrec.Config;
|
||||||
import ctbrec.OS;
|
import ctbrec.OS;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.io.IoUtils;
|
import ctbrec.io.IoUtils;
|
||||||
import ctbrec.io.StreamRedirector;
|
import ctbrec.recorder.FFmpeg;
|
||||||
import ctbrec.recorder.RecordingManager;
|
import ctbrec.recorder.RecordingManager;
|
||||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
|
||||||
|
|
||||||
public class Remux extends AbstractPostProcessor {
|
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 FFMPEG_ARGS = "ffmpeg.args";
|
||||||
public static final String FILE_EXT = "file.ext";
|
public static final String FILE_EXT = "file.ext";
|
||||||
|
private transient File inputFile;
|
||||||
|
private transient File remuxedFile;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
@ -32,70 +32,62 @@ public class Remux extends AbstractPostProcessor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||||
String fileExt = getConfig().get(FILE_EXT);
|
inputFile = rec.getPostProcessedFile();
|
||||||
String[] args = getConfig().get(FFMPEG_ARGS).split(" ");
|
|
||||||
String[] argsPlusFile = new String[args.length + 3];
|
|
||||||
File inputFile = rec.getPostProcessedFile();
|
|
||||||
if (inputFile.isDirectory()) {
|
if (inputFile.isDirectory()) {
|
||||||
inputFile = new File(inputFile, "playlist.m3u8");
|
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;
|
int i = 0;
|
||||||
argsPlusFile[i++] = "-i";
|
argsPlusFile[i++] = "-i";
|
||||||
argsPlusFile[i++] = inputFile.getCanonicalPath();
|
argsPlusFile[i++] = inputFile.getCanonicalPath();
|
||||||
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||||
File remuxedFile = new File(rec.getPostProcessedFile().getAbsolutePath() + '.' + fileExt);
|
|
||||||
argsPlusFile[argsPlusFile.length - 1] = remuxedFile.getAbsolutePath();
|
argsPlusFile[argsPlusFile.length - 1] = remuxedFile.getAbsolutePath();
|
||||||
String[] cmdline = OS.getFFmpegCommand(argsPlusFile);
|
return 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,7 +28,7 @@ public class ShowupMergedDownload extends MergedFfmpegHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
protected void startDownloadLoop(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||||
try {
|
try {
|
||||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||||
emptyPlaylistCheck(lsp);
|
emptyPlaylistCheck(lsp);
|
||||||
|
|
Loading…
Reference in New Issue