Add support for hlsdl
This commit is contained in:
parent
4421c6f9c3
commit
8e22112603
|
@ -9,6 +9,7 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -117,6 +118,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private SimpleLongProperty leaveSpaceOnDevice;
|
private SimpleLongProperty leaveSpaceOnDevice;
|
||||||
private SimpleStringProperty ffmpegParameters;
|
private SimpleStringProperty ffmpegParameters;
|
||||||
private SimpleBooleanProperty logFFmpegOutput;
|
private SimpleBooleanProperty logFFmpegOutput;
|
||||||
|
private SimpleBooleanProperty loghlsdlOutput;
|
||||||
private SimpleStringProperty fileExtension;
|
private SimpleStringProperty fileExtension;
|
||||||
private SimpleStringProperty server;
|
private SimpleStringProperty server;
|
||||||
private SimpleIntegerProperty port;
|
private SimpleIntegerProperty port;
|
||||||
|
@ -126,6 +128,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private SimpleBooleanProperty totalModelCountInTitle;
|
private SimpleBooleanProperty totalModelCountInTitle;
|
||||||
private SimpleBooleanProperty transportLayerSecurity;
|
private SimpleBooleanProperty transportLayerSecurity;
|
||||||
private SimpleBooleanProperty fastScrollSpeed;
|
private SimpleBooleanProperty fastScrollSpeed;
|
||||||
|
private SimpleBooleanProperty useHlsdl;
|
||||||
|
private SimpleFileProperty hlsdlExecutable;
|
||||||
private ExclusiveSelectionProperty recordLocal;
|
private ExclusiveSelectionProperty recordLocal;
|
||||||
private SimpleIntegerProperty postProcessingThreads;
|
private SimpleIntegerProperty postProcessingThreads;
|
||||||
private IgnoreList ignoreList;
|
private IgnoreList ignoreList;
|
||||||
|
@ -170,6 +174,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
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);
|
logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
|
||||||
|
loghlsdlOutput = new SimpleBooleanProperty(null, "loghlsdlOutput", settings.loghlsdlOutput);
|
||||||
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);
|
||||||
|
@ -184,6 +189,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
|
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
|
||||||
fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed);
|
fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed);
|
||||||
confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
|
confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
|
||||||
|
useHlsdl = new SimpleBooleanProperty(null, "useHlsdl", settings.useHlsdl);
|
||||||
|
hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createGui() {
|
private void createGui() {
|
||||||
|
@ -269,6 +276,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Category.of("Advanced / Devtools",
|
Category.of("Advanced / Devtools",
|
||||||
Group.of("Logging",
|
Group.of("Logging",
|
||||||
Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory")
|
Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory")
|
||||||
|
),
|
||||||
|
Group.of("hlsdl (experimental)",
|
||||||
|
Setting.of("Use hlsdl (if possible)", useHlsdl, "Use hlsdl to record the live streams. Some features might not work correctly."),
|
||||||
|
Setting.of("hlsdl executable", hlsdlExecutable, "Path to the hlsdl executable"),
|
||||||
|
Setting.of("Log hlsdl output", loghlsdlOutput, "Log hlsdl output to files in the system's temp directory")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -300,7 +312,6 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
setContent(stackPane);
|
setContent(stackPane);
|
||||||
prefs.expandTree();
|
prefs.expandTree();
|
||||||
|
|
||||||
|
|
||||||
prefs.getSetting("httpServer").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
prefs.getSetting("httpServer").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||||
prefs.getSetting("httpPort").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
prefs.getSetting("httpPort").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||||
prefs.getSetting("servletContext").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
prefs.getSetting("servletContext").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||||
|
@ -321,10 +332,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||||
|
prefs.getSetting("hlsdlExecutable").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
|
||||||
|
prefs.getSetting("loghlsdlOutput").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
|
||||||
postProcessingStepPanel.disableProperty().bind(recordLocal.not());
|
postProcessingStepPanel.disableProperty().bind(recordLocal.not());
|
||||||
variablesHelpButton.disableProperty().bind(recordLocal);
|
variablesHelpButton.disableProperty().bind(recordLocal);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
|
private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
|
||||||
|
@ -425,11 +436,13 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveConfig() {
|
public void saveConfig() {
|
||||||
try {
|
CompletableFuture.runAsync(() -> {
|
||||||
Config.getInstance().save();
|
try {
|
||||||
} catch (IOException e) {
|
Config.getInstance().save();
|
||||||
LOG.error("Couldn't save config", e);
|
} catch (IOException e) {
|
||||||
}
|
LOG.error("Couldn't save config", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,6 +28,8 @@ until a recording is finished. 0 means unlimited.
|
||||||
|
|
||||||
- **generatePlaylist** (server only) - [`true`,`false`] Generate a playlist once a recording terminates.
|
- **generatePlaylist** (server only) - [`true`,`false`] Generate a playlist once a recording terminates.
|
||||||
|
|
||||||
|
- **hlsdlExecutable** - Path to the hlsdl executable, which is used, if `useHlsdl` is set to true
|
||||||
|
|
||||||
- **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this will set
|
- **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this will set
|
||||||
the port ctbrec tries to connect to, if it is run in remote mode.
|
the port ctbrec tries to connect to, if it is run in remote mode.
|
||||||
|
|
||||||
|
@ -43,7 +45,9 @@ the port ctbrec tries to connect to, if it is run in remote mode.
|
||||||
|
|
||||||
- **livePreviews** (app only) - Enables the live preview feature in the app.
|
- **livePreviews** (app only) - Enables the live preview feature in the app.
|
||||||
|
|
||||||
- **logFFmpegOutput** - The output from FFmpeg (from recordings or post-processing steps) will be logged in temporary files.
|
- **logFFmpegOutput** - [`true`,`false`] The output from FFmpeg (from recordings or post-processing steps) will be logged in temporary files.
|
||||||
|
|
||||||
|
- **loghlsdlOutput** - [`true`,`false`] The output from hlsdl will be logged in temporary files. Only in effect, if `useHlsdl` is set to true
|
||||||
|
|
||||||
- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream.
|
- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream.
|
||||||
|
|
||||||
|
@ -73,5 +77,7 @@ which have the defined length (roughly). Has to be activated with `splitStrategy
|
||||||
- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings,
|
- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings,
|
||||||
which have the defined size (roughly). Has to be activated with `splitStrategy`.
|
which have the defined size (roughly). Has to be activated with `splitStrategy`.
|
||||||
|
|
||||||
|
- **useHlsdl** - [`true`,`false`] Use hlsdl to record the live streams. You also have to set `hlsdlExecutable`, if hlsdl is not globally available on your system. hlsdl won't be used for MV Live, LiveJasmin and Showup.
|
||||||
|
|
||||||
- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on
|
- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on
|
||||||
a machine, which can be accessed from the internet, because this is totally unprotected at the moment.
|
a machine, which can be accessed from the internet, because this is totally unprotected at the moment.
|
||||||
|
|
|
@ -17,6 +17,7 @@ import ctbrec.recorder.download.Download;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||||
import ctbrec.recorder.download.hls.HlsDownload;
|
import ctbrec.recorder.download.hls.HlsDownload;
|
||||||
|
import ctbrec.recorder.download.hls.HlsdlDownload;
|
||||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
|
@ -275,10 +276,14 @@ public abstract class AbstractModel implements Model {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Download createDownload() {
|
public Download createDownload() {
|
||||||
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
if (Config.getInstance().getSettings().useHlsdl) {
|
||||||
return new HlsDownload(getSite().getHttpClient());
|
return new HlsdlDownload();
|
||||||
} else {
|
} else {
|
||||||
return new MergedFfmpegHlsDownload(getSite().getHttpClient());
|
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
||||||
|
return new HlsDownload(getSite().getHttpClient());
|
||||||
|
} else {
|
||||||
|
return new MergedFfmpegHlsDownload(getSite().getHttpClient());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,7 +213,7 @@ public class Config {
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() throws IOException {
|
public synchronized void save() throws IOException {
|
||||||
Moshi moshi = new Moshi.Builder()
|
Moshi moshi = new Moshi.Builder()
|
||||||
.add(Model.class, new ModelJsonAdapter())
|
.add(Model.class, new ModelJsonAdapter())
|
||||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||||
|
|
|
@ -69,6 +69,7 @@ public class Settings {
|
||||||
public String flirt4freePassword;
|
public String flirt4freePassword;
|
||||||
public String flirt4freeUsername;
|
public String flirt4freeUsername;
|
||||||
public boolean generatePlaylist = true;
|
public boolean generatePlaylist = true;
|
||||||
|
public String hlsdlExecutable = "hlsdl";
|
||||||
public int httpPort = 8080;
|
public int httpPort = 8080;
|
||||||
public int httpSecurePort = 8443;
|
public int httpSecurePort = 8443;
|
||||||
public String httpServer = "localhost";
|
public String httpServer = "localhost";
|
||||||
|
@ -84,6 +85,7 @@ public class Settings {
|
||||||
public boolean livePreviews = false;
|
public boolean livePreviews = false;
|
||||||
public boolean localRecording = true;
|
public boolean localRecording = true;
|
||||||
public boolean logFFmpegOutput = false;
|
public boolean logFFmpegOutput = false;
|
||||||
|
public boolean loghlsdlOutput = 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;
|
||||||
|
@ -156,6 +158,7 @@ public class Settings {
|
||||||
public boolean transportLayerSecurity = true;
|
public boolean transportLayerSecurity = true;
|
||||||
public int thumbWidth = 180;
|
public int thumbWidth = 180;
|
||||||
public boolean updateThumbnails = true;
|
public boolean updateThumbnails = true;
|
||||||
|
public boolean useHlsdl = false;
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public String username = "";
|
public String username = "";
|
||||||
public int windowHeight = 800;
|
public int windowHeight = 800;
|
||||||
|
|
|
@ -9,15 +9,15 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class FfmpegStreamRedirector implements Runnable {
|
public class ProcessStreamRedirector implements Runnable {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(FfmpegStreamRedirector.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ProcessStreamRedirector.class);
|
||||||
|
|
||||||
private InputStream in;
|
private InputStream in;
|
||||||
private OutputStream out;
|
private OutputStream out;
|
||||||
private boolean keepGoing = true;
|
private boolean keepGoing = true;
|
||||||
private ScheduledExecutorService executor;
|
private ScheduledExecutorService executor;
|
||||||
|
|
||||||
public FfmpegStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
|
public ProcessStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
|
||||||
super();
|
super();
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.in = in;
|
this.in = in;
|
||||||
|
@ -37,7 +37,7 @@ public class FfmpegStreamRedirector implements Runnable {
|
||||||
executor.schedule(this, 100, MILLISECONDS);
|
executor.schedule(this, 100, MILLISECONDS);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.debug("Error while reading from FFmpeg output stream: {}", e.getLocalizedMessage());
|
LOG.debug("Error while reading from process output stream: {}", e.getLocalizedMessage());
|
||||||
keepGoing = false;
|
keepGoing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.io.DevNull;
|
import ctbrec.io.DevNull;
|
||||||
import ctbrec.io.FfmpegStreamRedirector;
|
import ctbrec.io.ProcessStreamRedirector;
|
||||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
|
||||||
public class FFmpeg {
|
public class FFmpeg {
|
||||||
|
@ -30,8 +30,8 @@ public class FFmpeg {
|
||||||
private Consumer<Integer> exitCallback;
|
private Consumer<Integer> exitCallback;
|
||||||
private File ffmpegLog = null;
|
private File ffmpegLog = null;
|
||||||
private OutputStream ffmpegLogStream;
|
private OutputStream ffmpegLogStream;
|
||||||
private FfmpegStreamRedirector stdoutRedirector;
|
private ProcessStreamRedirector stdoutRedirector;
|
||||||
private FfmpegStreamRedirector stderrRedirector;
|
private ProcessStreamRedirector stderrRedirector;
|
||||||
|
|
||||||
private FFmpeg() {}
|
private FFmpeg() {}
|
||||||
|
|
||||||
|
@ -85,8 +85,8 @@ public class FFmpeg {
|
||||||
} else {
|
} else {
|
||||||
ffmpegLogStream = new DevNull();
|
ffmpegLogStream = new DevNull();
|
||||||
}
|
}
|
||||||
stdoutRedirector = new FfmpegStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
|
stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
|
||||||
stderrRedirector = new FfmpegStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
|
stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
|
||||||
processOutputReader.submit(stdoutRedirector);
|
processOutputReader.submit(stdoutRedirector);
|
||||||
processOutputReader.submit(stderrRedirector);
|
processOutputReader.submit(stderrRedirector);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
package ctbrec.recorder.download.hls;
|
||||||
|
|
||||||
|
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.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.io.DevNull;
|
||||||
|
import ctbrec.io.ProcessStreamRedirector;
|
||||||
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
|
||||||
|
public class Hlsdl {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Hlsdl.class);
|
||||||
|
|
||||||
|
private static ScheduledExecutorService processOutputReader = Executors.newScheduledThreadPool(2, createThreadFactory("hlsdl output stream reader"));
|
||||||
|
|
||||||
|
private Process process;
|
||||||
|
private boolean logOutput = false;
|
||||||
|
private Consumer<Process> startCallback;
|
||||||
|
private Consumer<Integer> exitCallback;
|
||||||
|
private File processLog = null;
|
||||||
|
private OutputStream processLogStream;
|
||||||
|
private ProcessStreamRedirector stdoutRedirector;
|
||||||
|
private ProcessStreamRedirector stderrRedirector;
|
||||||
|
|
||||||
|
private Hlsdl() {}
|
||||||
|
|
||||||
|
private static ThreadFactory createThreadFactory(String name) {
|
||||||
|
return r -> {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setName(name);
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException, InterruptedException {
|
||||||
|
LOG.debug("hlsdl 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 IOException {
|
||||||
|
LOG.debug("hlsdl exit code was {}", exitCode);
|
||||||
|
processLogStream.flush();
|
||||||
|
processLogStream.close();
|
||||||
|
stdoutRedirector.setKeepGoing(false);
|
||||||
|
stderrRedirector.setKeepGoing(false);
|
||||||
|
notifyExitCallback(exitCode);
|
||||||
|
if (exitCode != 1) {
|
||||||
|
if (processLog != null && processLog.exists()) {
|
||||||
|
Files.delete(processLog.toPath());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ProcessExitedUncleanException("hlsdl exit code was " + exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupLogging() throws IOException {
|
||||||
|
if (logOutput) {
|
||||||
|
if (processLog == null) {
|
||||||
|
processLog = File.createTempFile("hlsdl_", ".log");
|
||||||
|
}
|
||||||
|
LOG.debug("Logging hlsdl output to {}", processLog);
|
||||||
|
processLog.deleteOnExit();
|
||||||
|
processLogStream = new FileOutputStream(processLog);
|
||||||
|
} else {
|
||||||
|
processLogStream = new DevNull();
|
||||||
|
}
|
||||||
|
stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), processLogStream);
|
||||||
|
stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), processLogStream);
|
||||||
|
processOutputReader.submit(stdoutRedirector);
|
||||||
|
processOutputReader.submit(stderrRedirector);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Hlsdl build() {
|
||||||
|
Hlsdl instance = new Hlsdl();
|
||||||
|
instance.logOutput = logOutput;
|
||||||
|
instance.startCallback = startCallback != null ? startCallback : p -> {};
|
||||||
|
instance.exitCallback = exitCallback != null ? exitCallback : exitCode -> {};
|
||||||
|
instance.processLog = logFile;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package ctbrec.recorder.download.hls;
|
||||||
|
|
||||||
|
import static ctbrec.recorder.download.StreamSource.*;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.OS;
|
||||||
|
import ctbrec.Recording;
|
||||||
|
import ctbrec.recorder.download.AbstractDownload;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
|
||||||
|
public class HlsdlDownload extends AbstractDownload {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(HlsdlDownload.class);
|
||||||
|
|
||||||
|
protected Model model;
|
||||||
|
protected File targetFile;
|
||||||
|
|
||||||
|
protected transient Config config;
|
||||||
|
protected transient Process hlsdlProcess;
|
||||||
|
protected transient boolean running;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config config, Model model, Instant startTime) {
|
||||||
|
super.startTime = startTime;
|
||||||
|
this.config = config;
|
||||||
|
this.model = model;
|
||||||
|
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||||
|
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
|
||||||
|
// TODO splittingStrategy = initSplittingStrategy(config.getSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws IOException {
|
||||||
|
try {
|
||||||
|
running = true;
|
||||||
|
Thread.currentThread().setName("Download " + model.getName());
|
||||||
|
Files.createDirectories(targetFile.getParentFile().toPath());
|
||||||
|
Hlsdl hlsdl = new Hlsdl.Builder()
|
||||||
|
.logOutput(config.getSettings().loghlsdlOutput)
|
||||||
|
.onStarted(p -> hlsdlProcess = p)
|
||||||
|
.build();
|
||||||
|
String[] cmdline = createCommandLine();
|
||||||
|
hlsdl.exec(cmdline, OS.getEnvironment(), targetFile.getParentFile());
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new IOException("Couldn't parse stream information", e);
|
||||||
|
} catch (PlaylistException e) {
|
||||||
|
throw new IOException("Couldn't parse HLS playlist", e);
|
||||||
|
} catch (EOFException e) {
|
||||||
|
// end of playlist reached
|
||||||
|
LOG.debug("Reached end of playlist for model {}", model);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException("Exception while downloading segments", e);
|
||||||
|
} finally {
|
||||||
|
stop();
|
||||||
|
running = false;
|
||||||
|
LOG.debug("Download for {} terminated", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] createCommandLine() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
|
||||||
|
String playlistUrl = getSegmentPlaylistUrl(model);
|
||||||
|
Map<String, String> headers = model.getHttpHeaderFactory().createSegmentPlaylistHeaders();
|
||||||
|
String[] cmdline = new String[9 + headers.size() * 2];
|
||||||
|
int idx = 0;
|
||||||
|
cmdline[idx++] = config.getSettings().hlsdlExecutable;
|
||||||
|
cmdline[idx++] = "-c";
|
||||||
|
cmdline[idx++] = "-r";
|
||||||
|
cmdline[idx++] = "3";
|
||||||
|
cmdline[idx++] = "-w";
|
||||||
|
cmdline[idx++] = "3";
|
||||||
|
for (Entry<String, String> header : headers.entrySet()) {
|
||||||
|
cmdline[idx++] = "-h";
|
||||||
|
cmdline[idx++] = header.getKey() + ": " + header.getValue();
|
||||||
|
}
|
||||||
|
cmdline[idx++] = "-o";
|
||||||
|
cmdline[idx++] = targetFile.getCanonicalPath();
|
||||||
|
cmdline[idx] = playlistUrl;
|
||||||
|
return cmdline;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
|
||||||
|
LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
|
||||||
|
List<StreamSource> streamSources = model.getStreamSources();
|
||||||
|
Collections.sort(streamSources);
|
||||||
|
for (StreamSource streamSource : streamSources) {
|
||||||
|
LOG.debug("{} src {}", model.getName(), streamSource);
|
||||||
|
}
|
||||||
|
String url = null;
|
||||||
|
if (model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
|
||||||
|
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
|
||||||
|
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
|
||||||
|
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||||
|
} else {
|
||||||
|
// filter out stream resolutions, which are out of range of the configured min and max
|
||||||
|
int minRes = Config.getInstance().getSettings().minimumResolution;
|
||||||
|
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||||
|
List<StreamSource> filteredStreamSources = streamSources.stream()
|
||||||
|
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height)
|
||||||
|
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (filteredStreamSources.isEmpty()) {
|
||||||
|
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
|
||||||
|
} else {
|
||||||
|
LOG.debug("{} selected {}", model.getName(), filteredStreamSources.get(filteredStreamSources.size() - 1));
|
||||||
|
url = filteredStreamSources.get(filteredStreamSources.size() - 1).getMediaPlaylistUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG.debug("Segment playlist url {}", url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
if (running) {
|
||||||
|
running = false;
|
||||||
|
if (hlsdlProcess != null) {
|
||||||
|
hlsdlProcess.destroy();
|
||||||
|
if (hlsdlProcess.isAlive()) {
|
||||||
|
LOG.info("hlsdl didn't terminate. Destroying the process with force!");
|
||||||
|
hlsdlProcess.destroyForcibly();
|
||||||
|
hlsdlProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess(Recording recording) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getTarget() {
|
||||||
|
return targetFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPath(Model model) {
|
||||||
|
String absolutePath = targetFile.getAbsolutePath();
|
||||||
|
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||||
|
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
|
||||||
|
return relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleFile() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSizeInByte() {
|
||||||
|
return getTarget().length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model getModel() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ctbrec.sites.fc2live;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.recorder.download.hls.HlsdlDownload;
|
||||||
|
|
||||||
|
public class Fc2HlsdlDownload extends HlsdlDownload {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Fc2HlsdlDownload.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws IOException {
|
||||||
|
Fc2Model fc2Model = (Fc2Model) model;
|
||||||
|
try {
|
||||||
|
fc2Model.openWebsocket();
|
||||||
|
super.start();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.error("Couldn't start download for {}", model, e);
|
||||||
|
} finally {
|
||||||
|
fc2Model.closeWebsocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -381,10 +381,14 @@ public class Fc2Model extends AbstractModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Download createDownload() {
|
public Download createDownload() {
|
||||||
if(Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
if (Config.getInstance().getSettings().useHlsdl) {
|
||||||
return new Fc2HlsDownload(getSite().getHttpClient());
|
return new Fc2HlsdlDownload();
|
||||||
} else {
|
} else {
|
||||||
return new Fc2MergedHlsDownload(getSite().getHttpClient());
|
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
||||||
|
return new Fc2HlsDownload(getSite().getHttpClient());
|
||||||
|
} else {
|
||||||
|
return new Fc2MergedHlsDownload(getSite().getHttpClient());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue