Merge branch 'dev' into v4
This commit is contained in:
commit
e709e2d45d
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,7 +1,16 @@
|
|||
NEXT
|
||||
3.12.1
|
||||
========================
|
||||
* Fix: "Resume all" started the recordings of models marked for later recording
|
||||
|
||||
3.12.0
|
||||
========================
|
||||
* Added "record later" tab to "bookmark" models
|
||||
* Added config option to show the total number of models in the title bar
|
||||
* Added support for hlsdl. Some sites (MV Live, LiveJasmin, Showup) are
|
||||
excluded, because they need some special behavior while the download is
|
||||
running. hlsdl can be activated in the settings under "Advanced" or with
|
||||
the config properties "useHlsdl", "hlsdlExecutable" and "loghlsdlOutput".
|
||||
The used bandwidth calculation does not work with hlsdl.
|
||||
* Fixed problem with Cam4 playlist URLs, thanks @gohufrapoc
|
||||
|
||||
3.11.0
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
<plugin>
|
||||
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
||||
<artifactId>launch4j-maven-plugin</artifactId>
|
||||
<version>1.7.22</version>
|
||||
<version>1.7.25</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>l4j-win</id>
|
||||
|
@ -132,7 +132,9 @@
|
|||
<bundledJre64Bit>true</bundledJre64Bit>
|
||||
<minVersion>15</minVersion>
|
||||
<maxHeapSize>512</maxHeapSize>
|
||||
<opt>-Dfile.encoding=utf-8</opt>
|
||||
<opts>
|
||||
<opt>-Dfile.encoding=utf-8</opt>
|
||||
</opts>
|
||||
</jre>
|
||||
<versionInfo>
|
||||
<fileVersion>4.0.0.0</fileVersion>
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
import ctbrec.ui.PauseIcon;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
public class PausedIndicator extends StackPane {
|
||||
|
||||
private PauseIcon pausedIcon;
|
||||
private Rectangle clickPanel;
|
||||
|
||||
public PausedIndicator(int size, Color color) {
|
||||
setMaxSize(size, size);
|
||||
|
||||
pausedIcon = new PauseIcon(color, size);
|
||||
pausedIcon.setVisible(false);
|
||||
clickPanel = new Rectangle(size, size);
|
||||
clickPanel.setCursor(Cursor.HAND);
|
||||
clickPanel.setFill(Paint.valueOf("#00000000"));
|
||||
getChildren().add(pausedIcon);
|
||||
getChildren().add(clickPanel);
|
||||
|
||||
pausedIcon.visibleProperty().bindBidirectional(visibleProperty());
|
||||
clickPanel.onMouseClickedProperty().bindBidirectional(onMouseClickedProperty());
|
||||
|
||||
Tooltip tooltip = new Tooltip("Resume Recording");
|
||||
Tooltip.install(clickPanel, tooltip);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
public class RecordingIndicator extends StackPane {
|
||||
|
||||
private ImageView icon;
|
||||
private Rectangle clickPanel;
|
||||
|
||||
public RecordingIndicator(int size) {
|
||||
setMaxSize(size, size);
|
||||
|
||||
icon = new ImageView();
|
||||
icon.setVisible(false);
|
||||
icon.prefHeight(size);
|
||||
icon.prefWidth(size);
|
||||
icon.maxHeight(size);
|
||||
icon.maxWidth(size);
|
||||
clickPanel = new Rectangle(size, size);
|
||||
clickPanel.setCursor(Cursor.HAND);
|
||||
clickPanel.setFill(Paint.valueOf("#00000000"));
|
||||
getChildren().add(icon);
|
||||
getChildren().add(clickPanel);
|
||||
|
||||
icon.visibleProperty().bindBidirectional(visibleProperty());
|
||||
}
|
||||
|
||||
public void setImage(Image img) {
|
||||
icon.setImage(img);
|
||||
}
|
||||
}
|
||||
|
|
@ -290,6 +290,7 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
ComboBox<Object> comboBox = new ComboBox(listProp);
|
||||
Field field = Settings.class.getField(setting.getKey());
|
||||
Object value = field.get(settings);
|
||||
LOG.debug("{} {} {}", setting.getName(), value, setting.getProperty().getValue());
|
||||
if (StringUtil.isNotBlank(value.toString())) {
|
||||
if (setting.getConverter() != null) {
|
||||
comboBox.getSelectionModel().select(setting.getConverter().convertTo(value));
|
||||
|
|
|
@ -9,6 +9,7 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
@ -117,6 +118,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleLongProperty leaveSpaceOnDevice;
|
||||
private SimpleStringProperty ffmpegParameters;
|
||||
private SimpleBooleanProperty logFFmpegOutput;
|
||||
private SimpleBooleanProperty loghlsdlOutput;
|
||||
private SimpleStringProperty fileExtension;
|
||||
private SimpleStringProperty server;
|
||||
private SimpleIntegerProperty port;
|
||||
|
@ -126,6 +128,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleBooleanProperty totalModelCountInTitle;
|
||||
private SimpleBooleanProperty transportLayerSecurity;
|
||||
private SimpleBooleanProperty fastScrollSpeed;
|
||||
private SimpleBooleanProperty useHlsdl;
|
||||
private SimpleFileProperty hlsdlExecutable;
|
||||
private ExclusiveSelectionProperty recordLocal;
|
||||
private SimpleIntegerProperty postProcessingThreads;
|
||||
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));
|
||||
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
|
||||
logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
|
||||
loghlsdlOutput = new SimpleBooleanProperty(null, "loghlsdlOutput", settings.loghlsdlOutput);
|
||||
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
|
||||
server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
|
||||
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);
|
||||
fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed);
|
||||
confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
|
||||
useHlsdl = new SimpleBooleanProperty(null, "useHlsdl", settings.useHlsdl);
|
||||
hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
|
||||
}
|
||||
|
||||
private void createGui() {
|
||||
|
@ -269,6 +276,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
Category.of("Advanced / Devtools",
|
||||
Group.of("Logging",
|
||||
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")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -320,13 +332,17 @@ 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("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());
|
||||
variablesHelpButton.disableProperty().bind(recordLocal);
|
||||
}
|
||||
|
||||
private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
|
||||
boolean splitAfterSet = settings.splitRecordingsAfterSecs > 0;
|
||||
LOG.debug("after {}", settings.splitRecordingsAfterSecs);
|
||||
boolean splitBiggerThanSet = settings.splitRecordingsBiggerThanBytes > 0;
|
||||
LOG.debug("bigger {}", settings.splitRecordingsBiggerThanBytes);
|
||||
if (splitAfterSet && splitBiggerThanSet) {
|
||||
settings.splitStrategy = TIME_OR_SIZE;
|
||||
} else if (splitAfterSet) {
|
||||
|
@ -336,6 +352,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
} else {
|
||||
settings.splitStrategy = DONT;
|
||||
}
|
||||
LOG.debug("strats {}", settings.splitStrategy);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
|
@ -422,11 +439,13 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
public void saveConfig() {
|
||||
try {
|
||||
Config.getInstance().save();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't save config", e);
|
||||
}
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
Config.getInstance().save();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't save config", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,13 +1,58 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import static ctbrec.Recording.State.*;
|
||||
import static ctbrec.ui.UnicodeEmoji.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.ui.*;
|
||||
import ctbrec.ui.action.*;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
import ctbrec.ui.JavaFxModel;
|
||||
import ctbrec.ui.PreviewPopupHandler;
|
||||
import ctbrec.ui.StreamSourceSelectionDialog;
|
||||
import ctbrec.ui.action.CheckModelAccountAction;
|
||||
import ctbrec.ui.action.EditNotesAction;
|
||||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.IgnoreModelsAction;
|
||||
import ctbrec.ui.action.OpenRecordingsDir;
|
||||
import ctbrec.ui.action.PauseAction;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.action.RemoveTimeLimitAction;
|
||||
import ctbrec.ui.action.ResumeAction;
|
||||
import ctbrec.ui.action.SetStopDateAction;
|
||||
import ctbrec.ui.action.StartRecordingAction;
|
||||
import ctbrec.ui.action.StopRecordingAction;
|
||||
import ctbrec.ui.action.ToggleRecordingAction;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.DateTimeCellFactory;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
|
@ -26,13 +71,33 @@ import javafx.concurrent.WorkerStateEvent;
|
|||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableColumn.CellEditEvent;
|
||||
import javafx.scene.control.TableColumn.SortType;
|
||||
import javafx.scene.control.TableRow;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
@ -41,30 +106,6 @@ import javafx.util.Callback;
|
|||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import javafx.util.converter.NumberStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ctbrec.Recording.State.RECORDING;
|
||||
import static ctbrec.ui.UnicodeEmoji.CLOCK;
|
||||
import static ctbrec.ui.UnicodeEmoji.HEAVY_CHECK_MARK;
|
||||
|
||||
public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
|
||||
|
@ -409,14 +450,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
private void pauseAll(ActionEvent evt) {
|
||||
boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene());
|
||||
if (yes) {
|
||||
new PauseAction(getTabPane(), recorder.getModels(), recorder).execute();
|
||||
List<Model> models = recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).collect(Collectors.toList());
|
||||
new PauseAction(getTabPane(), models, recorder).execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void resumeAll(ActionEvent evt) {
|
||||
boolean yes = Dialogs.showConfirmDialog("Resume all models", "", "Resume the recording of all models?", getTabPane().getScene());
|
||||
if (yes) {
|
||||
new ResumeAction(getTabPane(), recorder.getModels(), recorder).execute();
|
||||
List<Model> models = recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).collect(Collectors.toList());
|
||||
new ResumeAction(getTabPane(), models, recorder).execute();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import ctbrec.ui.SiteUiFactory;
|
|||
import ctbrec.ui.StreamSourceSelectionDialog;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.PausedIndicator;
|
||||
import ctbrec.ui.controls.RecordingIndicator;
|
||||
import ctbrec.ui.controls.StreamPreview;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.FillTransition;
|
||||
|
@ -51,6 +51,7 @@ import javafx.scene.control.ContextMenu;
|
|||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
@ -72,6 +73,11 @@ public class ThumbCell extends StackPane {
|
|||
private static final Logger LOG = LoggerFactory.getLogger(ThumbCell.class);
|
||||
private static final Duration ANIMATION_DURATION = new Duration(250);
|
||||
|
||||
private static Image imgRecordIndicator = new Image(ThumbCell.class.getResource("/media-record-16.png").toExternalForm());
|
||||
private static Image imgPauseIndicator = new Image(ThumbCell.class.getResource("/media-playback-pause-16.png").toExternalForm());
|
||||
private static Image imgBookmarkIndicator = new Image(ThumbCell.class.getResource("/bookmark-new-16.png").toExternalForm());
|
||||
|
||||
private ModelRecordingState modelRecordingState = ModelRecordingState.NOT;
|
||||
private Model model;
|
||||
private StreamPreview streamPreview;
|
||||
private ImageView iv;
|
||||
|
@ -85,8 +91,8 @@ public class ThumbCell extends StackPane {
|
|||
private Text topic;
|
||||
private Text resolutionTag;
|
||||
private Recorder recorder;
|
||||
private Circle recordingIndicator;
|
||||
private PausedIndicator pausedIndicator;
|
||||
private RecordingIndicator recordingIndicator;
|
||||
private Tooltip recordingIndicatorTooltip;
|
||||
private StackPane previewTrigger;
|
||||
private int index = 0;
|
||||
ContextMenu popup;
|
||||
|
@ -178,22 +184,15 @@ public class ThumbCell extends StackPane {
|
|||
StackPane.setMargin(resolutionTag, new Insets(2, 4, 2, 2));
|
||||
getChildren().add(resolutionTag);
|
||||
|
||||
recordingIndicator = new Circle(8);
|
||||
recordingIndicator.setFill(colorRecording);
|
||||
recordingIndicator = new RecordingIndicator(16);
|
||||
recordingIndicator.setCursor(Cursor.HAND);
|
||||
recordingIndicator.setOnMouseClicked(e -> pauseResumeAction(true));
|
||||
Tooltip tooltip = new Tooltip("Pause Recording");
|
||||
Tooltip.install(recordingIndicator, tooltip);
|
||||
recordingIndicator.setOnMouseClicked(this::recordingInidicatorClicked);
|
||||
recordingIndicatorTooltip = new Tooltip("Pause Recording");
|
||||
Tooltip.install(recordingIndicator, recordingIndicatorTooltip);
|
||||
StackPane.setMargin(recordingIndicator, new Insets(3));
|
||||
StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT);
|
||||
getChildren().add(recordingIndicator);
|
||||
|
||||
pausedIndicator = new PausedIndicator(16, colorRecording);
|
||||
pausedIndicator.setOnMouseClicked(e -> pauseResumeAction(false));
|
||||
StackPane.setMargin(pausedIndicator, new Insets(3));
|
||||
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
|
||||
getChildren().add(pausedIndicator);
|
||||
|
||||
if (Config.getInstance().getSettings().livePreviews) {
|
||||
getChildren().add(createPreviewTrigger());
|
||||
}
|
||||
|
@ -231,6 +230,21 @@ public class ThumbCell extends StackPane {
|
|||
update();
|
||||
}
|
||||
|
||||
private void recordingInidicatorClicked(MouseEvent evt) {
|
||||
switch(modelRecordingState) {
|
||||
case RECORDING:
|
||||
pauseResumeAction(true);
|
||||
break;
|
||||
case PAUSED:
|
||||
pauseResumeAction(false);
|
||||
break;
|
||||
case BOOKMARKED:
|
||||
recordLater(false);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
private Node createPreviewTrigger() {
|
||||
int s = 32;
|
||||
previewTrigger = new StackPane();
|
||||
|
@ -293,7 +307,6 @@ public class ThumbCell extends StackPane {
|
|||
streamPreview.stop();
|
||||
}
|
||||
recordingIndicator.setVisible(!visible);
|
||||
pausedIndicator.setVisible(!visible);
|
||||
if (!visible) {
|
||||
updateRecordingIndicator();
|
||||
}
|
||||
|
@ -445,17 +458,32 @@ public class ThumbCell extends StackPane {
|
|||
c = mouseHovering ? colorHighlight : colorNormal;
|
||||
}
|
||||
nameBackground.setFill(c);
|
||||
|
||||
updateRecordingIndicator();
|
||||
}
|
||||
|
||||
private void updateRecordingIndicator() {
|
||||
if (recording) {
|
||||
recordingIndicator.setVisible(!model.isSuspended());
|
||||
pausedIndicator.setVisible(model.isSuspended());
|
||||
recordingIndicator.setVisible(true);
|
||||
if (model.isSuspended()) {
|
||||
modelRecordingState = ModelRecordingState.PAUSED;
|
||||
recordingIndicator.setImage(imgPauseIndicator);
|
||||
recordingIndicatorTooltip.setText("Resume Recording");
|
||||
} else {
|
||||
modelRecordingState = ModelRecordingState.RECORDING;
|
||||
recordingIndicator.setImage(imgRecordIndicator);
|
||||
recordingIndicatorTooltip.setText("Pause Recording");
|
||||
}
|
||||
} else {
|
||||
recordingIndicator.setVisible(false);
|
||||
pausedIndicator.setVisible(false);
|
||||
if (model.isMarkedForLaterRecording()) {
|
||||
recordingIndicator.setVisible(true);
|
||||
modelRecordingState = ModelRecordingState.BOOKMARKED;
|
||||
recordingIndicator.setImage(imgBookmarkIndicator);
|
||||
recordingIndicatorTooltip.setText("Forget Model");
|
||||
} else {
|
||||
recordingIndicator.setVisible(false);
|
||||
modelRecordingState = ModelRecordingState.NOT;
|
||||
recordingIndicator.setImage(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -562,9 +590,9 @@ public class ThumbCell extends StackPane {
|
|||
});
|
||||
}
|
||||
|
||||
void recordLater() {
|
||||
model.setMarkedForLaterRecording(true);
|
||||
startStopAction(true);
|
||||
void recordLater(boolean recordLater) {
|
||||
model.setMarkedForLaterRecording(recordLater);
|
||||
startStopAction(recordLater);
|
||||
}
|
||||
|
||||
public Model getModel() {
|
||||
|
@ -577,7 +605,6 @@ public class ThumbCell extends StackPane {
|
|||
this.model.setPreview(model.getPreview());
|
||||
this.model.setTags(model.getTags());
|
||||
this.model.setUrl(model.getUrl());
|
||||
this.model.setSuspended(recorder.isSuspended(model));
|
||||
update();
|
||||
}
|
||||
|
||||
|
@ -591,9 +618,11 @@ public class ThumbCell extends StackPane {
|
|||
|
||||
private void update() {
|
||||
model.setSuspended(recorder.isSuspended(model));
|
||||
model.setMarkedForLaterRecording(recorder.isMarkedForLaterRecording(model));
|
||||
setRecording(recorder.isTracked(model));
|
||||
updateRecordingIndicator();
|
||||
setImage(model.getPreview());
|
||||
String txt = recording ? " " : "";
|
||||
String txt = (modelRecordingState != ModelRecordingState.NOT) ? " " : "";
|
||||
txt += model.getDescription() != null ? model.getDescription() : "";
|
||||
topic.setText(txt);
|
||||
|
||||
|
@ -693,4 +722,11 @@ public class ThumbCell extends StackPane {
|
|||
model.setMarkedForLaterRecording(false);
|
||||
startStopAction(true);
|
||||
}
|
||||
|
||||
private enum ModelRecordingState {
|
||||
RECORDING,
|
||||
PAUSED,
|
||||
BOOKMARKED,
|
||||
NOT
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,31 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
|
@ -7,12 +33,24 @@ import ctbrec.recorder.Recorder;
|
|||
import ctbrec.sites.Site;
|
||||
import ctbrec.sites.mfc.MyFreeCamsClient;
|
||||
import ctbrec.sites.mfc.MyFreeCamsModel;
|
||||
import ctbrec.ui.*;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
import ctbrec.ui.TipDialog;
|
||||
import ctbrec.ui.TokenLabel;
|
||||
import ctbrec.ui.action.IgnoreModelsAction;
|
||||
import ctbrec.ui.action.OpenRecordingsDir;
|
||||
import ctbrec.ui.action.SetStopDateAction;
|
||||
import ctbrec.ui.controls.*;
|
||||
import javafx.animation.*;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.SearchPopover;
|
||||
import ctbrec.ui.controls.SearchPopoverTreeList;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.ParallelTransition;
|
||||
import javafx.animation.ScaleTransition;
|
||||
import javafx.animation.TranslateTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -28,24 +66,34 @@ import javafx.geometry.Insets;
|
|||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.transform.Transform;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.showError;
|
||||
|
||||
public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
|
||||
|
@ -441,7 +489,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
MenuItem addPaused = new MenuItem("Add in paused state");
|
||||
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
|
||||
MenuItem recordLater = new MenuItem("Record Later");
|
||||
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell)));
|
||||
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), true));
|
||||
MenuItem removeRecordLater = new MenuItem("Forget Model");
|
||||
removeRecordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), false));
|
||||
MenuItem addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
|
||||
|
||||
MenuItem pause = new MenuItem("Pause Recording");
|
||||
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
|
||||
|
@ -476,10 +527,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
if (modelIsTrackedByRecorder) {
|
||||
contextMenu.getItems().addAll(pauseResume, recordLater);
|
||||
} else {
|
||||
contextMenu.getItems().addAll(recordUntil, addPaused);
|
||||
if (!recorder.isMarkedForLaterRecording(model)) {
|
||||
contextMenu.getItems().add(recordLater);
|
||||
}
|
||||
contextMenu.getItems().addAll(recordUntil, addPaused, addRemoveBookmark);
|
||||
}
|
||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||
if (site.supportsFollow()) {
|
||||
|
@ -500,9 +548,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
return contextMenu;
|
||||
}
|
||||
|
||||
private void recordLater(List<ThumbCell> list) {
|
||||
private void recordLater(List<ThumbCell> list, boolean recordLater) {
|
||||
for (ThumbCell cell : list) {
|
||||
cell.recordLater();
|
||||
cell.recordLater(recordLater);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 785 B |
|
@ -28,6 +28,8 @@ until a recording is finished. 0 means unlimited.
|
|||
|
||||
- **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
|
||||
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.
|
||||
|
||||
- **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.
|
||||
|
||||
|
@ -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,
|
||||
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
|
||||
a machine, which can be accessed from the internet, because this is totally unprotected at the moment.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 718 B |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -17,6 +17,7 @@ import ctbrec.recorder.download.Download;
|
|||
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||
import ctbrec.recorder.download.hls.HlsDownload;
|
||||
import ctbrec.recorder.download.hls.HlsdlDownload;
|
||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||
import ctbrec.sites.Site;
|
||||
import okhttp3.Request;
|
||||
|
@ -275,10 +276,14 @@ public abstract class AbstractModel implements Model {
|
|||
|
||||
@Override
|
||||
public Download createDownload() {
|
||||
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
||||
return new HlsDownload(getSite().getHttpClient());
|
||||
if (Config.getInstance().getSettings().useHlsdl) {
|
||||
return new HlsdlDownload();
|
||||
} 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;
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
public synchronized void save() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter())
|
||||
.add(PostProcessor.class, new PostProcessorJsonAdapter())
|
||||
|
|
|
@ -69,6 +69,7 @@ public class Settings {
|
|||
public String flirt4freePassword;
|
||||
public String flirt4freeUsername;
|
||||
public boolean generatePlaylist = true;
|
||||
public String hlsdlExecutable = "hlsdl";
|
||||
public int httpPort = 8080;
|
||||
public int httpSecurePort = 8443;
|
||||
public String httpServer = "localhost";
|
||||
|
@ -84,6 +85,7 @@ public class Settings {
|
|||
public boolean livePreviews = false;
|
||||
public boolean localRecording = true;
|
||||
public boolean logFFmpegOutput = false;
|
||||
public boolean loghlsdlOutput = false;
|
||||
public int minimumResolution = 0;
|
||||
public int maximumResolution = 8640;
|
||||
public int maximumResolutionPlayer = 0;
|
||||
|
@ -156,6 +158,7 @@ public class Settings {
|
|||
public boolean transportLayerSecurity = true;
|
||||
public int thumbWidth = 180;
|
||||
public boolean updateThumbnails = true;
|
||||
public boolean useHlsdl = false;
|
||||
@Deprecated
|
||||
public String username = "";
|
||||
public int windowHeight = 800;
|
||||
|
|
|
@ -9,15 +9,15 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FfmpegStreamRedirector implements Runnable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FfmpegStreamRedirector.class);
|
||||
public class ProcessStreamRedirector implements Runnable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ProcessStreamRedirector.class);
|
||||
|
||||
private InputStream in;
|
||||
private OutputStream out;
|
||||
private boolean keepGoing = true;
|
||||
private ScheduledExecutorService executor;
|
||||
|
||||
public FfmpegStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
|
||||
public ProcessStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
|
||||
super();
|
||||
this.executor = executor;
|
||||
this.in = in;
|
||||
|
@ -37,7 +37,7 @@ public class FfmpegStreamRedirector implements Runnable {
|
|||
executor.schedule(this, 100, MILLISECONDS);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.io.DevNull;
|
||||
import ctbrec.io.FfmpegStreamRedirector;
|
||||
import ctbrec.io.ProcessStreamRedirector;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
public class FFmpeg {
|
||||
|
@ -30,8 +30,8 @@ public class FFmpeg {
|
|||
private Consumer<Integer> exitCallback;
|
||||
private File ffmpegLog = null;
|
||||
private OutputStream ffmpegLogStream;
|
||||
private FfmpegStreamRedirector stdoutRedirector;
|
||||
private FfmpegStreamRedirector stderrRedirector;
|
||||
private ProcessStreamRedirector stdoutRedirector;
|
||||
private ProcessStreamRedirector stderrRedirector;
|
||||
|
||||
private FFmpeg() {}
|
||||
|
||||
|
@ -45,7 +45,7 @@ public class FFmpeg {
|
|||
};
|
||||
}
|
||||
|
||||
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException, InterruptedException {
|
||||
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException {
|
||||
LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline));
|
||||
process = Runtime.getRuntime().exec(cmdline, env, executionDir);
|
||||
afterStart();
|
||||
|
@ -83,8 +83,8 @@ public class FFmpeg {
|
|||
} else {
|
||||
ffmpegLogStream = new DevNull();
|
||||
}
|
||||
stdoutRedirector = new FfmpegStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
|
||||
stderrRedirector = new FfmpegStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
|
||||
stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
|
||||
stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
|
||||
processOutputReader.submit(stdoutRedirector);
|
||||
processOutputReader.submit(stderrRedirector);
|
||||
}
|
||||
|
|
|
@ -143,9 +143,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
segmentDownloadFinished(result.get());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
e.printStackTrace();
|
||||
LOG.error("Error in segmentDownloadFinished", e);
|
||||
} catch (ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
LOG.error("Error in segmentDownloadFinished", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
running = false;
|
||||
}
|
||||
} catch (ExecutionException e1) {
|
||||
modelState = ctbrec.Model.State.UNKNOWN;
|
||||
modelState = State.UNKNOWN;
|
||||
}
|
||||
LOG.info(errorMsg, model, modelState);
|
||||
waitSomeTime(TEN_SECONDS);
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
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 {
|
||||
LOG.debug("hlsdl command line: {}", Arrays.toString(cmdline));
|
||||
process = Runtime.getRuntime().exec(cmdline, env, executionDir);
|
||||
afterStart();
|
||||
}
|
||||
|
||||
private void afterStart() throws IOException {
|
||||
notifyStartCallback(process);
|
||||
setupLogging();
|
||||
}
|
||||
|
||||
public void shutdown(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 {
|
||||
int exitCode = process.waitFor();
|
||||
try {
|
||||
shutdown(exitCode);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error while shutting down hlsdl process", e);
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
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,205 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
import static ctbrec.recorder.download.StreamSource.*;
|
||||
|
||||
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.concurrent.ExecutorService;
|
||||
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.Download;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
|
||||
public class HlsdlDownload extends AbstractDownload {
|
||||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(HlsdlDownload.class);
|
||||
|
||||
protected File targetFile;
|
||||
|
||||
private transient Hlsdl hlsdl;
|
||||
protected transient Process hlsdlProcess;
|
||||
protected transient boolean running = true;
|
||||
|
||||
@Override
|
||||
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
|
||||
super.init(config, model, startTime, executorService);
|
||||
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
|
||||
|
||||
createTargetDirectory();
|
||||
startHlsdlProcess();
|
||||
if (hlsdlProcess == null) {
|
||||
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
||||
}
|
||||
}
|
||||
|
||||
protected void createTargetDirectory() throws IOException {
|
||||
Files.createDirectories(targetFile.getParentFile().toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Download call() throws Exception {
|
||||
try {
|
||||
if (running && !hlsdlProcess.isAlive()) {
|
||||
running = false;
|
||||
int exitValue = hlsdlProcess.exitValue();
|
||||
hlsdl.shutdown(exitValue);
|
||||
}
|
||||
|
||||
rescheduleTime = Instant.now().plusSeconds(1);
|
||||
|
||||
if (splittingStrategy.splitNecessary(this)) {
|
||||
stop();
|
||||
}
|
||||
} catch (ProcessExitedUncleanException e) {
|
||||
LOG.error("hlsdl exited unclean", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void startHlsdlProcess() {
|
||||
try {
|
||||
String[] cmdline = prepareCommandLine();
|
||||
hlsdl = new Hlsdl.Builder()
|
||||
.logOutput(config.getSettings().logFFmpegOutput)
|
||||
.onStarted(p -> hlsdlProcess = p)
|
||||
.build();
|
||||
hlsdl.exec(cmdline, OS.getEnvironment(), targetFile.getParentFile());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error starting hlsdl", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] prepareCommandLine() 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalizeDownload() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
}
|
|
@ -127,11 +127,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
ffmpeg.exec(cmdline, new String[0], target.getParentFile());
|
||||
} catch (IOException | ProcessExitedUncleanException e) {
|
||||
LOG.error("Error in FFmpeg thread", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (running) {
|
||||
LOG.info("Interrupted while waiting for ffmpeg", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package ctbrec.sites.fc2live;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.download.hls.HlsdlDownload;
|
||||
|
||||
public class Fc2HlsdlDownload extends HlsdlDownload {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Fc2HlsdlDownload.class);
|
||||
|
||||
|
||||
@Override
|
||||
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
|
||||
super.init(config, model, startTime, executorService);
|
||||
Fc2Model fc2Model = (Fc2Model) model;
|
||||
try {
|
||||
fc2Model.openWebsocket();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Couldn't start download for {}", model, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalizeDownload() {
|
||||
Fc2Model fc2Model = (Fc2Model) model;
|
||||
fc2Model.closeWebsocket();
|
||||
super.finalizeDownload();
|
||||
}
|
||||
}
|
|
@ -381,10 +381,14 @@ public class Fc2Model extends AbstractModel {
|
|||
|
||||
@Override
|
||||
public Download createDownload() {
|
||||
if(Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
||||
return new Fc2HlsDownload(getSite().getHttpClient());
|
||||
if (Config.getInstance().getSettings().useHlsdl) {
|
||||
return new Fc2HlsdlDownload();
|
||||
} 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