diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index 9b247408..c29d88f4 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -40,4 +40,5 @@ public class Settings { public boolean windowMaximized = false; public int windowX; public int windowY; + public int splitRecordings = 0; } diff --git a/src/main/java/ctbrec/recorder/LocalRecorder.java b/src/main/java/ctbrec/recorder/LocalRecorder.java index dad08c51..e1df34ea 100644 --- a/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -471,7 +471,13 @@ public class LocalRecorder implements Recorder { recording.setStatus(GENERATING_PLAYLIST); recording.setProgress(playlistGenerator.getProgress()); } else { - if (Recording.isMergedRecording(rec)) { + if (config.isServerMode()) { + if (recording.hasPlaylist()) { + recording.setStatus(FINISHED); + } else { + recording.setStatus(RECORDING); + } + } else { boolean dirUsedByRecordingProcess = false; for (Download download : recordingProcesses.values()) { if(rec.equals(download.getDirectory())) { @@ -484,12 +490,6 @@ public class LocalRecorder implements Recorder { } else { recording.setStatus(FINISHED); } - } else { - if (recording.hasPlaylist()) { - recording.setStatus(FINISHED); - } else { - recording.setStatus(RECORDING); - } } } recordings.add(recording); diff --git a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index 35976de9..0497ef68 100644 --- a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -14,7 +14,10 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.text.DecimalFormat; import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.ZonedDateTime; import java.util.Date; import java.util.LinkedList; import java.util.Objects; @@ -48,15 +51,20 @@ public class MergedHlsDownload extends AbstractHlsDownload { private BlockingMultiMTSSource multiSource; private Thread mergeThread; private Streamer streamer; + private ZonedDateTime startTime; + private Config config; + private File targetFile; + private DecimalFormat df = new DecimalFormat("00000"); + private int splitCounter = 0; public MergedHlsDownload(HttpClient client) { super(client); } - public void start(String segmentPlaylistUri, File target, ProgressListener progressListener) throws IOException { + public void start(String segmentPlaylistUri, File targetFile, ProgressListener progressListener) throws IOException { try { running = true; - mergeThread = createMergeThread(target, progressListener, false); + mergeThread = createMergeThread(targetFile, progressListener, false); mergeThread.start(); downloadSegments(segmentPlaylistUri, false); } catch(ParseException e) { @@ -72,8 +80,10 @@ public class MergedHlsDownload extends AbstractHlsDownload { @Override public void start(Model model, Config config) throws IOException { + this.config = config; try { running = true; + startTime = ZonedDateTime.now(); StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client); if(!Objects.equals(streamInfo.room_status, "public")) { throw new IOException(model.getName() +"'s room is not public"); @@ -87,8 +97,13 @@ public class MergedHlsDownload extends AbstractHlsDownload { Files.createDirectories(downloadDir); } - File targetFile = Recording.mergedFileFromDirectory(downloadDir.toFile()); - mergeThread = createMergeThread(targetFile, null, true); + targetFile = Recording.mergedFileFromDirectory(downloadDir.toFile()); + File target = targetFile; + if(config.getSettings().splitRecordings > 0) { + LOG.debug("Splitting recordings every {} seconds", config.getSettings().splitRecordings); + target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts")); + } + mergeThread = createMergeThread(target, null, true); mergeThread.start(); String segments = parseMaster(streamInfo.url, model.getStreamUrlIndex()); @@ -159,6 +174,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { } if(livestreamDownload) { + // download new segments while(!mergeQueue.isEmpty()) { try { writeSegment(mergeQueue.poll()); @@ -168,21 +184,32 @@ public class MergedHlsDownload extends AbstractHlsDownload { } } } - } - if (livestreamDownload) { - long wait = 0; - if (lastSegment == lsp.seq) { - // playlist didn't change -> wait for at least half the target duration - wait = (long) lsp.targetDuration * 1000 / 2; - LOG.trace("Playlist didn't change... waiting for {}ms", wait); - } else { - // playlist did change -> wait for at least last segment duration - wait = 1;// (long) lsp.lastSegDuration * 1000; - LOG.trace("Playlist changed... waiting for {}ms", wait); + // split up the recording, if configured + if(config.getSettings().splitRecordings > 0) { + Duration recordingDuration = Duration.between(startTime, ZonedDateTime.now()); + long seconds = recordingDuration.getSeconds(); + if(seconds >= config.getSettings().splitRecordings) { + streamer.stop(); + File target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-"+df.format(++splitCounter)+".ts")); + mergeThread = createMergeThread(target, null, true); + mergeThread.start(); + startTime = ZonedDateTime.now(); + } } + // wait some time until requesting the segment playlist again to not hammer the server try { + long wait = 0; + if (lastSegment == lsp.seq) { + // playlist didn't change -> wait for at least half the target duration + wait = (long) lsp.targetDuration * 1000 / 2; + LOG.trace("Playlist didn't change... waiting for {}ms", wait); + } else { + // playlist did change -> wait for at least last segment duration + wait = 1;// (long) lsp.lastSegDuration * 1000; + LOG.trace("Playlist changed... waiting for {}ms", wait); + } Thread.sleep(wait); } catch (InterruptedException e) { if (running) { diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index 94a8dc1a..35b4c1ea 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -2,11 +2,15 @@ package ctbrec.ui; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.sun.javafx.collections.ObservableListWrapper; + import ctbrec.Config; import ctbrec.Hmac; import javafx.beans.value.ChangeListener; @@ -18,6 +22,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.RadioButton; @@ -60,6 +65,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private ToggleGroup recordLocation; private ProxySettingsPane proxySettingsPane; private TitledPane ctb; + private ComboBox splitAfter; public SettingsTab() { setText("Settings"); @@ -75,6 +81,49 @@ public class SettingsTab extends Tab implements TabSelectionListener { setContent(mainLayout); GridPane layout = createGridLayout(); + Label l = new Label("Display stream resolution in overview"); + layout.add(l, 0, 0); + loadResolution = new CheckBox(); + loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); + loadResolution.setOnAction((e) -> { + Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); + if(!loadResolution.isSelected()) { + ThumbOverviewTab.queue.clear(); + } + }); + //GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(loadResolution, 1, 0); + + l = new Label("Manually select stream quality"); + layout.add(l, 0, 1); + chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); + chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected()); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(chooseStreamQuality, 1, 1); + + l = new Label("Split recordings after (minutes)"); + layout.add(l, 0, 2); + List options = new ArrayList<>(); + options.add(new SplitAfterOption("disabled", 0)); + options.add(new SplitAfterOption("10 min", 10 * 60)); + options.add(new SplitAfterOption("15 min", 15 * 60)); + options.add(new SplitAfterOption("20 min", 20 * 60)); + options.add(new SplitAfterOption("30 min", 30 * 60)); + options.add(new SplitAfterOption("60 min", 60 * 60)); + splitAfter = new ComboBox<>(new ObservableListWrapper<>(options)); + layout.add(splitAfter, 1, 2); + setSplitAfterValue(); + splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue()); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + + TitledPane general = new TitledPane("General", layout); + general.setCollapsible(false); + mainLayout.add(general, 0, 0); + + layout = createGridLayout(); layout.add(new Label("Recordings Directory"), 0, 0); recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir); recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener()); @@ -95,7 +144,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(mediaPlayer, 1, 1); layout.add(createMpvBrowseButton(), 3, 1); - Label l = new Label("Allow multiple players"); + l = new Label("Allow multiple players"); layout.add(l, 0, 2); multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected()); @@ -107,7 +156,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { TitledPane locations = new TitledPane("Locations", layout); locations.setCollapsible(false); - mainLayout.add(locations, 0, 0); + mainLayout.add(locations, 0, 1); proxySettingsPane = new ProxySettingsPane(); mainLayout.add(proxySettingsPane, 1, 0); @@ -158,33 +207,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { ctb = new TitledPane("Chaturbate", layout); ctb.setCollapsible(false); - mainLayout.add(ctb, 0, 1); - - layout = createGridLayout(); - l = new Label("Display stream resolution in overview"); - layout.add(l, 0, 0); - loadResolution = new CheckBox(); - loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); - loadResolution.setOnAction((e) -> { - Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); - if(!loadResolution.isSelected()) { - ThumbOverviewTab.queue.clear(); - } - }); - //GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(loadResolution, 1, 0); - - l = new Label("Manually select stream quality"); - layout.add(l, 0, 1); - chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); - chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected()); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(chooseStreamQuality, 1, 1); - TitledPane quality = new TitledPane("Stream Quality", layout); - quality.setCollapsible(false); - mainLayout.add(quality, 0, 2); + mainLayout.add(ctb, 0, 2); layout = createGridLayout(); l = new Label("Record Location"); @@ -272,6 +295,15 @@ public class SettingsTab extends Tab implements TabSelectionListener { setRecordingMode(recordLocal.isSelected()); } + private void setSplitAfterValue() { + int value = Config.getInstance().getSettings().splitRecordings; + for (SplitAfterOption option : splitAfter.getItems()) { + if(option.getValue() == value) { + splitAfter.getSelectionModel().select(option); + } + } + } + static void showRestartRequired() { Alert restart = new AutosizeAlert(AlertType.INFORMATION); restart.setTitle("Restart required"); @@ -295,6 +327,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { ctb.setDisable(!local); recordingsDirectory.setDisable(!local); recordingsDirectoryButton.setDisable(!local); + splitAfter.setDisable(!local); } private ChangeListener createRecordingsDirectoryFocusListener() { @@ -422,4 +455,24 @@ public class SettingsTab extends Tab implements TabSelectionListener { public void saveConfig() { proxySettingsPane.saveConfig(); } + + public static class SplitAfterOption { + private String label; + private int value; + + public SplitAfterOption(String label, int value) { + super(); + this.label = label; + this.value = value; + } + + public int getValue() { + return value; + } + + @Override + public String toString() { + return label; + } + } }