forked from j62/ctbrec
Add providers for MFC streams sources
Since MFC uses different streaming technologies, the stream sources have to be determined differently. This is now done in dedicated StreamSourceProvider classes.
This commit is contained in:
parent
98c1731c8e
commit
1c64b82deb
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
|
@ -121,7 +123,7 @@ public class JavaFxModel implements Model {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
|
||||
return delegate.getStreamSources();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,10 +20,12 @@ import ctbrec.ui.controls.Dialogs;
|
|||
import javafx.scene.Scene;
|
||||
|
||||
public class Player {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(Player.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Player.class);
|
||||
private static PlayerThread playerThread;
|
||||
public static Scene scene;
|
||||
|
||||
private Player() {}
|
||||
|
||||
public static boolean play(String url) {
|
||||
return play(url, true);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
@ -30,9 +29,6 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.State;
|
||||
|
@ -75,11 +71,12 @@ import javafx.scene.layout.HBox;
|
|||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);
|
||||
private static final String ERROR_WHILE_DOWNLOADING_RECORDING = "Error while downloading recording";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);
|
||||
|
||||
private ScheduledService<List<JavaFxRecording>> updateService;
|
||||
private Config config;
|
||||
|
@ -91,7 +88,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
FlowPane grid = new FlowPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
|
||||
TableView<JavaFxRecording> table = new TableView<>();
|
||||
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
|
||||
ContextMenu popup;
|
||||
ProgressBar spaceLeft;
|
||||
|
@ -125,104 +122,30 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
name.setPrefWidth(200);
|
||||
name.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getName()));
|
||||
TableColumn<JavaFxRecording, Instant> date = new TableColumn<>("Date");
|
||||
date.setCellValueFactory((cdf) -> {
|
||||
date.setCellValueFactory(cdf -> {
|
||||
Instant instant = cdf.getValue().getStartDate();
|
||||
return new SimpleObjectProperty<Instant>(instant);
|
||||
});
|
||||
date.setCellFactory(new Callback<TableColumn<JavaFxRecording, Instant>, TableCell<JavaFxRecording, Instant>>() {
|
||||
@Override
|
||||
public TableCell<JavaFxRecording, Instant> call(TableColumn<JavaFxRecording, Instant> param) {
|
||||
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
|
||||
@Override
|
||||
protected void updateItem(Instant instant, boolean empty) {
|
||||
if(empty || instant == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
||||
setText(dtf.format(time));
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
});
|
||||
date.setCellFactory(param -> createDateCell());
|
||||
date.setPrefWidth(200);
|
||||
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
|
||||
status.setCellValueFactory((cdf) -> cdf.getValue().getStatusProperty());
|
||||
status.setCellValueFactory(cdf -> cdf.getValue().getStatusProperty());
|
||||
status.setPrefWidth(300);
|
||||
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
|
||||
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
|
||||
progress.setCellValueFactory(cdf -> cdf.getValue().getProgressProperty());
|
||||
progress.setPrefWidth(100);
|
||||
TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
|
||||
size.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
size.setPrefWidth(100);
|
||||
size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty());
|
||||
size.setCellFactory(new Callback<TableColumn<JavaFxRecording, Number>, TableCell<JavaFxRecording, Number>>() {
|
||||
@Override
|
||||
public TableCell<JavaFxRecording, Number> call(TableColumn<JavaFxRecording, Number> param) {
|
||||
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
|
||||
@Override
|
||||
protected void updateItem(Number sizeInByte, boolean empty) {
|
||||
if(empty || sizeInByte == null) {
|
||||
setText(null);
|
||||
setStyle(null);
|
||||
} else {
|
||||
setText(StringUtil.formatSize(sizeInByte));
|
||||
setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
int row = this.getTableRow().getIndex();
|
||||
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
|
||||
if(!rec.valueChanged() && rec.getStatus() == State.RECORDING) {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
});
|
||||
size.setCellFactory(param -> createSizeCell());
|
||||
|
||||
table.getColumns().addAll(name, date, status, progress, size);
|
||||
table.setItems(observableRecordings);
|
||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if(recordings != null && !recordings.isEmpty()) {
|
||||
popup = createContextMenu(recordings);
|
||||
if(!popup.getItems().isEmpty()) {
|
||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if(popup != null) {
|
||||
popup.hide();
|
||||
}
|
||||
});
|
||||
table.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
Recording recording = table.getSelectionModel().getSelectedItem();
|
||||
if(recording != null) {
|
||||
play(recording);
|
||||
}
|
||||
}
|
||||
});
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if (recordings != null && !recordings.isEmpty()) {
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) {
|
||||
delete(recordings);
|
||||
}
|
||||
} else if (event.getCode() == KeyCode.ENTER) {
|
||||
if(recordings.get(0).getStatus() == State.FINISHED) {
|
||||
play(recordings.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::onContextMenuRequested);
|
||||
table.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
|
||||
table.addEventFilter(MouseEvent.MOUSE_CLICKED, this::onMouseClicked);
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, this::onKeyPressed);
|
||||
scrollPane.setContent(table);
|
||||
|
||||
HBox spaceBox = new HBox(5);
|
||||
|
@ -246,14 +169,94 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
restoreState();
|
||||
}
|
||||
|
||||
private TableCell<JavaFxRecording, Number> createSizeCell() {
|
||||
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
|
||||
@Override
|
||||
protected void updateItem(Number sizeInByte, boolean empty) {
|
||||
if(empty || sizeInByte == null) {
|
||||
setText(null);
|
||||
setStyle(null);
|
||||
} else {
|
||||
setText(StringUtil.formatSize(sizeInByte));
|
||||
setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
int row = this.getTableRow().getIndex();
|
||||
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
|
||||
if(!rec.valueChanged() && rec.getStatus() == State.RECORDING) {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
|
||||
}
|
||||
|
||||
private TableCell<JavaFxRecording, Instant> createDateCell() {
|
||||
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
|
||||
@Override
|
||||
protected void updateItem(Instant instant, boolean empty) {
|
||||
if(empty || instant == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
||||
setText(dtf.format(time));
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
private void onContextMenuRequested(ContextMenuEvent event) {
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if (recordings != null && !recordings.isEmpty()) {
|
||||
popup = createContextMenu(recordings);
|
||||
if (!popup.getItems().isEmpty()) {
|
||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
|
||||
private void onMousePressed(MouseEvent event) {
|
||||
if(popup != null) {
|
||||
popup.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMouseClicked(MouseEvent event) {
|
||||
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
Recording recording = table.getSelectionModel().getSelectedItem();
|
||||
if(recording != null) {
|
||||
play(recording);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onKeyPressed( KeyEvent event ) {
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if (recordings != null && !recordings.isEmpty()) {
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) {
|
||||
delete(recordings);
|
||||
}
|
||||
} else if (event.getCode() == KeyCode.ENTER && recordings.get(0).getStatus() == State.FINISHED) {
|
||||
play(recordings.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initializeUpdateService() {
|
||||
updateService = createUpdateService();
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||
updateService.setOnSucceeded((event) -> {
|
||||
updateService.setOnSucceeded(event -> {
|
||||
updateRecordingsTable();
|
||||
updateFreeSpaceDisplay();
|
||||
});
|
||||
updateService.setOnFailed((event) -> {
|
||||
updateService.setOnFailed(event -> {
|
||||
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
|
||||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
|
||||
autosizeAlert.setTitle("Whoopsie!");
|
||||
|
@ -309,12 +312,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
|
||||
ScheduledService<List<JavaFxRecording>> updateService = new ScheduledService<List<JavaFxRecording>>() {
|
||||
ScheduledService<List<JavaFxRecording>> service = new ScheduledService<List<JavaFxRecording>>() {
|
||||
@Override
|
||||
protected Task<List<JavaFxRecording>> createTask() {
|
||||
return new Task<List<JavaFxRecording>>() {
|
||||
@Override
|
||||
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
updateSpace();
|
||||
|
||||
List<JavaFxRecording> recordings = new ArrayList<>();
|
||||
|
@ -339,17 +342,14 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
};
|
||||
}
|
||||
};
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("RecordingsTab UpdateService");
|
||||
return t;
|
||||
}
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("RecordingsTab UpdateService");
|
||||
return t;
|
||||
});
|
||||
updateService.setExecutor(executor);
|
||||
return updateService;
|
||||
service.setExecutor(executor);
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -375,9 +375,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
JavaFxRecording first = recordings.get(0);
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction((e) -> {
|
||||
play(recordings.get(0));
|
||||
});
|
||||
openInPlayer.setOnAction(e -> play(recordings.get(0)));
|
||||
if(first.getStatus() == State.FINISHED || Config.getInstance().getSettings().localRecording) {
|
||||
contextMenu.getItems().add(openInPlayer);
|
||||
}
|
||||
|
@ -397,33 +395,24 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
// }
|
||||
|
||||
MenuItem deleteRecording = new MenuItem("Delete");
|
||||
deleteRecording.setOnAction((e) -> {
|
||||
delete(recordings);
|
||||
});
|
||||
deleteRecording.setOnAction(e -> delete(recordings));
|
||||
if(first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING || first.getStatus() == State.FAILED || recordings.size() > 1) {
|
||||
contextMenu.getItems().add(deleteRecording);
|
||||
}
|
||||
|
||||
MenuItem openDir = new MenuItem("Open directory");
|
||||
openDir.setOnAction((e) -> {
|
||||
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||
String path = first.getPath();
|
||||
File tsFile = new File(recordingsDir, path);
|
||||
new Thread(() -> {
|
||||
DesktopIntegration.open(tsFile.getParent());
|
||||
}).start();
|
||||
});
|
||||
openDir.setOnAction(e -> onOpenDirectory(first));
|
||||
if(Config.getInstance().getSettings().localRecording) {
|
||||
contextMenu.getItems().add(openDir);
|
||||
}
|
||||
|
||||
MenuItem downloadRecording = new MenuItem("Download");
|
||||
downloadRecording.setOnAction((e) -> {
|
||||
downloadRecording.setOnAction(e -> {
|
||||
try {
|
||||
download(first);
|
||||
} catch (IOException | ParseException | PlaylistException e1) {
|
||||
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
|
||||
LOG.error("Error while downloading recording", e1);
|
||||
} catch (IOException e1) {
|
||||
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e1);
|
||||
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e1);
|
||||
}
|
||||
});
|
||||
if (!Config.getInstance().getSettings().localRecording && first.getStatus() == State.FINISHED) {
|
||||
|
@ -431,9 +420,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
|
||||
rerunPostProcessing.setOnAction((e) -> {
|
||||
triggerPostProcessing(first);
|
||||
});
|
||||
rerunPostProcessing.setOnAction(e -> triggerPostProcessing(first));
|
||||
if (first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING) {
|
||||
contextMenu.getItems().add(rerunPostProcessing);
|
||||
}
|
||||
|
@ -447,6 +434,13 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
return contextMenu;
|
||||
}
|
||||
|
||||
private void onOpenDirectory(JavaFxRecording first) {
|
||||
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||
String path = first.getPath();
|
||||
File tsFile = new File(recordingsDir, path);
|
||||
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
|
||||
}
|
||||
|
||||
private void triggerPostProcessing(JavaFxRecording first) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
@ -458,8 +452,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}).start();
|
||||
}
|
||||
|
||||
private void download(Recording recording) throws IOException, ParseException, PlaylistException {
|
||||
String filename = recording.getPath().substring(1).replaceAll("/", "-") + ".ts";
|
||||
private void download(Recording recording) throws IOException {
|
||||
String filename = recording.getPath().substring(1).replace("/", "-") + ".ts";
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setInitialFileName(filename);
|
||||
if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
|
||||
|
@ -475,58 +469,52 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
String hlsBase = config.getServerUrl() + "/hls";
|
||||
URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8");
|
||||
LOG.info("Downloading {}", recording.getPath());
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
MergedHlsDownload download = new MergedHlsDownload(CamrecApplication.httpClient);
|
||||
download.start(url.toString(), target, (progress) -> {
|
||||
Platform.runLater(() -> {
|
||||
if (progress == 100) {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
LOG.debug("Download finished for recording {}", recording.getPath());
|
||||
} else {
|
||||
recording.setStatus(DOWNLOADING);
|
||||
recording.setProgress(progress);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (FileNotFoundException e) {
|
||||
showErrorDialog("Error while downloading recording", "The target file couldn't be created", e);
|
||||
LOG.error("Error while downloading recording", e);
|
||||
} catch (IOException e) {
|
||||
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e);
|
||||
LOG.error("Error while downloading recording", e);
|
||||
} finally {
|
||||
Platform.runLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.setName("Download Thread " + recording.getPath());
|
||||
t.start();
|
||||
|
||||
startDownloadThread(url, target, recording);
|
||||
recording.setStatus(State.DOWNLOADING);
|
||||
recording.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorDialog(final String title, final String msg, final Exception e) {
|
||||
Platform.runLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
|
||||
autosizeAlert.setTitle(title);
|
||||
autosizeAlert.setHeaderText(msg);
|
||||
autosizeAlert.setContentText("An error occured: " + e.getLocalizedMessage());
|
||||
autosizeAlert.showAndWait();
|
||||
private void startDownloadThread(URL url, File target, Recording recording) {
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
MergedHlsDownload download = new MergedHlsDownload(CamrecApplication.httpClient);
|
||||
download.start(url.toString(), target, progress -> Platform.runLater(() -> {
|
||||
if (progress == 100) {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
LOG.debug("Download finished for recording {}", recording.getPath());
|
||||
} else {
|
||||
recording.setStatus(DOWNLOADING);
|
||||
recording.setProgress(progress);
|
||||
}
|
||||
}));
|
||||
} catch (FileNotFoundException e) {
|
||||
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The target file couldn't be created", e);
|
||||
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
|
||||
} catch (IOException e) {
|
||||
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e);
|
||||
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
});
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.setName("Download Thread " + recording.getPath());
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void showErrorDialog(final String title, final String msg, final Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
|
||||
autosizeAlert.setTitle(title);
|
||||
autosizeAlert.setHeaderText(msg);
|
||||
autosizeAlert.setContentText("An error occured: " + e.getLocalizedMessage());
|
||||
autosizeAlert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private void play(Recording recording) {
|
||||
|
@ -543,7 +531,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}.start();
|
||||
} else {
|
||||
String hlsBase = Config.getInstance().getServerUrl() + "/hls";
|
||||
url = hlsBase + recording.getPath() + "/playlist.m3u8";
|
||||
url = hlsBase + recording.getPath() + (recording.getPath().endsWith(".mp4") ? "" : "/playlist.m3u8");
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -559,7 +547,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void delete(List<JavaFxRecording> recordings) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
|
||||
String msg;
|
||||
if(recordings.size() > 1) {
|
||||
msg = "Delete " + recordings.size() + " recordings for good?";
|
||||
|
@ -573,38 +560,39 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
confirm.setContentText("");
|
||||
confirm.showAndWait();
|
||||
if (confirm.getResult() == ButtonType.YES) {
|
||||
Thread deleteThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
List<Recording> deleted = new ArrayList<>();
|
||||
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording r = iterator.next();
|
||||
if(r.getStatus() != FINISHED) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
recorder.delete(r.getDelegate());
|
||||
deleted.add(r);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Error while deleting recording", e1);
|
||||
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
|
||||
}
|
||||
}
|
||||
observableRecordings.removeAll(deleted);
|
||||
} finally {
|
||||
recordingsLock.unlock();
|
||||
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
|
||||
}
|
||||
}
|
||||
};
|
||||
deleteThread.start();
|
||||
deleteAsync(recordings);
|
||||
} else {
|
||||
table.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteAsync(List<JavaFxRecording> recordings) {
|
||||
Thread deleteThread = new Thread(() -> {
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
List<Recording> deleted = new ArrayList<>();
|
||||
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording r = iterator.next();
|
||||
if(r.getStatus() != FINISHED && r.getStatus() != FAILED) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
recorder.delete(r.getDelegate());
|
||||
deleted.add(r);
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
LOG.error("Error while deleting recording", e1);
|
||||
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
|
||||
}
|
||||
}
|
||||
observableRecordings.removeAll(deleted);
|
||||
} finally {
|
||||
recordingsLock.unlock();
|
||||
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
|
||||
}
|
||||
});
|
||||
deleteThread.start();
|
||||
}
|
||||
|
||||
public void saveState() {
|
||||
if(!table.getSortOrder().isEmpty()) {
|
||||
TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
|
||||
|
@ -616,7 +604,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
columnWidths[i] = table.getColumns().get(i).getWidth();
|
||||
}
|
||||
Config.getInstance().getSettings().recordingsColumnWidths = columnWidths;
|
||||
};
|
||||
}
|
||||
|
||||
private void restoreState() {
|
||||
String sortCol = Config.getInstance().getSettings().recordingsSortColumn;
|
||||
|
|
|
@ -3,10 +3,13 @@ package ctbrec.ui.sites.myfreecams;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.sites.mfc.MyFreeCamsClient;
|
||||
import ctbrec.sites.mfc.SessionState;
|
||||
import ctbrec.sites.mfc.User;
|
||||
import ctbrec.ui.PaginatedScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
|
@ -19,10 +22,14 @@ public class HDCamsUpdateService extends PaginatedScheduledService {
|
|||
public List<Model> call() throws IOException {
|
||||
MyFreeCamsClient client = MyFreeCamsClient.getInstance();
|
||||
int modelsPerPage = 50;
|
||||
|
||||
return client.getModels().stream()
|
||||
.filter(m -> m.getPreview() != null)
|
||||
.filter(m -> m.getStreamUrl() != null)
|
||||
.filter(m -> m.getStreamUrl().contains("mfc_a_"))
|
||||
.filter(m -> Optional.ofNullable(client.getSessionState(m))
|
||||
.map(SessionState::getU)
|
||||
.map(User::getPhase)
|
||||
.orElse("").equalsIgnoreCase("a"))
|
||||
.filter(m -> {
|
||||
try {
|
||||
return m.isOnline();
|
||||
|
@ -31,7 +38,7 @@ public class HDCamsUpdateService extends PaginatedScheduledService {
|
|||
}
|
||||
})
|
||||
.sorted((m1,m2) -> (int)(m2.getCamScore() - m1.getCamScore()))
|
||||
.skip( (page-1) * modelsPerPage)
|
||||
.skip( (page-1l) * modelsPerPage)
|
||||
.limit(modelsPerPage)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
|
@ -73,7 +75,7 @@ public interface Model extends Comparable<Model> {
|
|||
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException;
|
||||
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException;
|
||||
|
||||
public void invalidateCacheEntries();
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ public class OS {
|
|||
configDir = new File(appData, CTBREC);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperatingSystemException("Unsupported operating system " + System.getProperty(OS_NAME));
|
||||
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
|
||||
}
|
||||
return configDir;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ public class OS {
|
|||
System.arraycopy(args, 0, cmd, 2, args.length);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperatingSystemException("Unsupported operating system " + System.getProperty(OS_NAME));
|
||||
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
|
||||
}
|
||||
LOG.debug("Browser command: {}", Arrays.toString(cmd));
|
||||
return cmd;
|
||||
|
@ -101,6 +101,41 @@ public class OS {
|
|||
}
|
||||
}
|
||||
|
||||
public static String[] getFFmpegCommand(String...args) {
|
||||
if(System.getenv("CTBREC_FFMPEG") != null) {
|
||||
String[] cmd = new String[args.length + 1];
|
||||
cmd[0] = System.getenv("CTBREC_FFMPEG");
|
||||
System.arraycopy(args, 0, cmd, 1, args.length);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = OS.class.getProtectionDomain().getCodeSource().getLocation().toURI();
|
||||
File jar = new File(uri.getPath());
|
||||
File browserDir = new File(jar.getParentFile(), "ffmpeg");
|
||||
String[] cmd;
|
||||
switch (getOsType()) {
|
||||
case LINUX:
|
||||
case MAC:
|
||||
cmd = new String[args.length + 1];
|
||||
cmd[0] = new File(browserDir, "ffmpeg").getAbsolutePath();
|
||||
System.arraycopy(args, 0, cmd, 1, args.length);
|
||||
break;
|
||||
case WINDOWS:
|
||||
cmd = new String[args.length + 1];
|
||||
cmd[0] = new File(browserDir, "ffmpeg.exe").getAbsolutePath();
|
||||
System.arraycopy(args, 0, cmd, 1, args.length);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
|
||||
}
|
||||
LOG.debug("FFmpeg command: {}", Arrays.toString(cmd));
|
||||
return cmd;
|
||||
} catch (URISyntaxException e) {
|
||||
throw new ForkProcessException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Settings getDefaultSettings() {
|
||||
Settings settings = new Settings();
|
||||
if(getOsType() == TYPE.WINDOWS) {
|
||||
|
|
|
@ -2,8 +2,8 @@ package ctbrec;
|
|||
|
||||
public class UnsupportedOperatingSystemException extends RuntimeException {
|
||||
|
||||
public UnsupportedOperatingSystemException(String msg) {
|
||||
super(msg);
|
||||
public UnsupportedOperatingSystemException(String os) {
|
||||
super("Unsupported operating system " + os);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.time.Instant;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -78,30 +79,6 @@ public class DashDownload implements Download {
|
|||
}
|
||||
}
|
||||
|
||||
private AdaptationSetType chooseBestVideo(List<AdaptationSetType> videoStreams) {
|
||||
AdaptationSetType best = null;
|
||||
long bestWidth = 0;
|
||||
for (AdaptationSetType stream : videoStreams) {
|
||||
if (stream.getWidth() > bestWidth) {
|
||||
bestWidth = stream.getWidth();
|
||||
best = stream;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private RepresentationType chooseBestRepresentation(List<RepresentationType> representations) {
|
||||
RepresentationType best = null;
|
||||
long bestBandwidth = 0;
|
||||
for (RepresentationType rep : representations) {
|
||||
if (rep.getBandwidth() > bestBandwidth) {
|
||||
bestBandwidth = rep.getBandwidth();
|
||||
best = rep;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private int downloadSegments(MPDtype mpd, AdaptationSetType adaptationSet, boolean isVideo) throws IOException {
|
||||
if (adaptationSet == null) {
|
||||
return 0;
|
||||
|
@ -139,6 +116,18 @@ public class DashDownload implements Download {
|
|||
return downloaded;
|
||||
}
|
||||
|
||||
private RepresentationType chooseBestRepresentation(List<RepresentationType> representations) {
|
||||
RepresentationType best = null;
|
||||
long bestBandwidth = 0;
|
||||
for (RepresentationType rep : representations) {
|
||||
if (rep.getBandwidth() > bestBandwidth) {
|
||||
bestBandwidth = rep.getBandwidth();
|
||||
best = rep;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private int downloadInitChunksForVideoAndAudio(boolean isVideo, MPDtype mpd, SegmentTemplateType segmentTemplate, RepresentationType representation)
|
||||
throws IOException {
|
||||
if (isVideo && !videoInitLoaded || !isVideo && !audioInitLoaded) {
|
||||
|
@ -176,11 +165,15 @@ public class DashDownload implements Download {
|
|||
String absFile = url.getFile();
|
||||
String prefix = isVideo ? "video" : "audio";
|
||||
int c = isVideo ? videoCounter++ : audioCounter++;
|
||||
File targetFile = new File(dir, prefix + '_' + df.format(c) + '_' + new File(absFile).getName());
|
||||
File segmentFile = new File(dir, prefix + '_' + df.format(c) + '_' + new File(absFile).getName());
|
||||
while(tries <= 10) {
|
||||
if (!targetFile.exists() || targetFile.length() == 0) {
|
||||
LOG.trace("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
|
||||
try (FileOutputStream out = new FileOutputStream(targetFile)) {
|
||||
if (!segmentFile.exists() || segmentFile.length() == 0) {
|
||||
if (tries > 1) {
|
||||
LOG.debug("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
|
||||
} else {
|
||||
LOG.trace("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
|
||||
}
|
||||
try (FileOutputStream out = new FileOutputStream(segmentFile)) {
|
||||
byte[] b = new byte[1024];
|
||||
int len = -1;
|
||||
while ((len = in.read(b)) >= 0) {
|
||||
|
@ -225,7 +218,7 @@ public class DashDownload implements Download {
|
|||
}
|
||||
}
|
||||
|
||||
private void downloadManifestAndItsSegments(Unmarshaller u) throws IOException, JAXBException {
|
||||
private void downloadManifestAndItsSegments(Unmarshaller u) throws IOException, JAXBException, ExecutionException {
|
||||
String manifest = getManifest(manifestUrl);
|
||||
LOG.trace("Manifest: {}", manifest);
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@ -249,18 +242,35 @@ public class DashDownload implements Download {
|
|||
downloadDir.toFile().mkdirs();
|
||||
|
||||
AdaptationSetType video = chooseBestVideo(videoStreams);
|
||||
int downloaded = downloadSegments(mpd, video, true);
|
||||
if (video == null) {
|
||||
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
|
||||
} else {
|
||||
int downloaded = downloadSegments(mpd, video, true);
|
||||
|
||||
AdaptationSetType audio = audioStreams.isEmpty() ? null : audioStreams.get(0);
|
||||
downloaded += downloadSegments(mpd, audio, false);
|
||||
AdaptationSetType audio = audioStreams.isEmpty() ? null : audioStreams.get(0);
|
||||
downloaded += downloadSegments(mpd, audio, false);
|
||||
|
||||
if (downloaded == 0) {
|
||||
LOG.trace("No new segments - Sleeping a bit");
|
||||
waitSomeTime();
|
||||
if (downloaded == 0) {
|
||||
LOG.trace("No new segments - Sleeping a bit");
|
||||
waitSomeTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AdaptationSetType chooseBestVideo(List<AdaptationSetType> videoStreams) {
|
||||
AdaptationSetType best = null;
|
||||
int maxHeight = config.getSettings().maximumResolution;
|
||||
long bestHeight = 0;
|
||||
for (AdaptationSetType stream : videoStreams) {
|
||||
if (stream.getHeight() > bestHeight && (maxHeight == 0 || stream.getHeight() <= maxHeight)) {
|
||||
bestHeight = stream.getHeight();
|
||||
best = stream;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (running) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import java.util.Arrays;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.OS;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
|
||||
public class FfmpegMuxer {
|
||||
|
@ -72,14 +73,13 @@ public class FfmpegMuxer {
|
|||
try {
|
||||
LOG.debug("Merging:\n{}\n{}\n{}", mp4VideoTrack, mp4AudioTrack, output);
|
||||
// @formatter:off
|
||||
String[] cmdline = new String[] {
|
||||
"ffmpeg",
|
||||
String[] cmdline = OS.getFFmpegCommand(
|
||||
"-i", mp4VideoTrack.getCanonicalPath(),
|
||||
"-i", mp4AudioTrack.getCanonicalPath(),
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
output.getCanonicalPath()
|
||||
};
|
||||
);
|
||||
// @formatter:on
|
||||
LOG.debug("Command line: {}", Arrays.toString(cmdline));
|
||||
Process ffmpeg = Runtime.getRuntime().exec(cmdline);
|
||||
|
|
|
@ -19,6 +19,8 @@ import java.util.concurrent.ThreadFactory;
|
|||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -129,7 +131,7 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
}
|
||||
|
||||
|
||||
protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
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);
|
||||
|
|
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -41,7 +43,7 @@ public class LiveJasminHlsDownload extends HlsDownload {
|
|||
try {
|
||||
LOG.debug("Updating segment playlist URL for {}", getModel());
|
||||
segmentUrl = getSegmentPlaylistUrl(getModel());
|
||||
} catch (IOException | ExecutionException | ParseException | PlaylistException e) {
|
||||
} catch (IOException | JAXBException | ExecutionException | ParseException | PlaylistException e) {
|
||||
LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -41,7 +43,7 @@ public class LiveJasminMergedHlsDownload extends MergedHlsDownload {
|
|||
try {
|
||||
LOG.debug("Updating segment playlist URL for {}", getModel());
|
||||
segmentUrl = getSegmentPlaylistUrl(getModel());
|
||||
} catch (IOException | ExecutionException | ParseException | PlaylistException e) {
|
||||
} catch (IOException | JAXBException | ExecutionException | ParseException | PlaylistException e) {
|
||||
LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package ctbrec.sites.mfc;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBElement;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import ctbrec.recorder.download.dash.AdaptationSetType;
|
||||
import ctbrec.recorder.download.dash.MPDtype;
|
||||
import ctbrec.recorder.download.dash.PeriodType;
|
||||
import ctbrec.recorder.download.dash.RepresentationType;
|
||||
import ctbrec.sites.Site;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class DashStreamSourceProvider implements StreamSourceProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashStreamSourceProvider.class);
|
||||
|
||||
private Config config;
|
||||
|
||||
private Site site;
|
||||
|
||||
public DashStreamSourceProvider(Config config, Site site) {
|
||||
this.config = config;
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources(String streamUrl) throws JAXBException, IOException {
|
||||
String manifest = getManifest(streamUrl);
|
||||
JAXBContext jc = JAXBContext.newInstance(MPDtype.class.getPackage().getName());
|
||||
Unmarshaller u = jc.createUnmarshaller();
|
||||
@SuppressWarnings("unchecked")
|
||||
JAXBElement<MPDtype> root = (JAXBElement<MPDtype>) u.unmarshal(new ByteArrayInputStream(manifest.getBytes()));
|
||||
MPDtype mpd = root.getValue();
|
||||
List<PeriodType> periods = mpd.getPeriod();
|
||||
if (periods.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
PeriodType period = periods.get(0);
|
||||
List<AdaptationSetType> videoStreams = new ArrayList<>();
|
||||
List<AdaptationSetType> adaptationSets = period.getAdaptationSet();
|
||||
for (AdaptationSetType adaptationSet : adaptationSets) {
|
||||
String mimeType = adaptationSet.getMimeType();
|
||||
if (mimeType.equalsIgnoreCase("video/mp4")) {
|
||||
videoStreams.add(adaptationSet);
|
||||
}
|
||||
}
|
||||
|
||||
return videoStreams.stream().map(ast -> {
|
||||
RepresentationType representation = ast.getRepresentation().get(0);
|
||||
StreamSource src = new StreamSource();
|
||||
src.width = ast.getWidth().intValue();
|
||||
src.height = ast.getHeight().intValue();
|
||||
src.bandwidth = (int)representation.getBandwidth();
|
||||
src.mediaPlaylistUrl = streamUrl;
|
||||
return src;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private String getManifest(String url) throws IOException {
|
||||
// @formatter:off
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("User-Agent", config.getSettings().httpUserAgent)
|
||||
.header("Origin", site.getBaseUrl())
|
||||
.header("Referer", site.getBaseUrl())
|
||||
.header("Connection", "keep-alive")
|
||||
.build(); // @formatter:on
|
||||
LOG.trace("Loading manifest {}", url);
|
||||
try (Response response = site.getHttpClient().execute(request)) {
|
||||
return response.body().string();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package ctbrec.sites.mfc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class HlsStreamSourceProvider implements StreamSourceProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HlsStreamSourceProvider.class);
|
||||
private HttpClient httpClient;
|
||||
|
||||
public HlsStreamSourceProvider(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources(String streamUrl) throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
MasterPlaylist masterPlaylist = getMasterPlaylist(streamUrl);
|
||||
List<StreamSource> sources = new ArrayList<>();
|
||||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
if(playlist.getStreamInfo().getResolution() != null) {
|
||||
src.width = playlist.getStreamInfo().getResolution().width;
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
} else {
|
||||
src.width = Integer.MAX_VALUE;
|
||||
src.height = Integer.MAX_VALUE;
|
||||
}
|
||||
String masterUrl = streamUrl;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
if(Config.getInstance().getSettings().mfcIgnoreUpscaled) {
|
||||
return sources.stream()
|
||||
.filter(src -> src.height != 960)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return sources;
|
||||
}
|
||||
}
|
||||
|
||||
private MasterPlaylist getMasterPlaylist(String streamUrl) throws IOException, ParseException, PlaylistException {
|
||||
if(streamUrl == null) {
|
||||
throw new IllegalStateException("Stream url unknown");
|
||||
}
|
||||
LOG.trace("Loading master playlist {}", streamUrl);
|
||||
Request req = new Request.Builder().url(streamUrl).build();
|
||||
try(Response response = httpClient.execute(req)) {
|
||||
if(response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -625,20 +625,13 @@ public class MyFreeCamsClient {
|
|||
return models.getIfPresent(uid);
|
||||
}
|
||||
|
||||
public void getSessionState(ctbrec.Model model) {
|
||||
public SessionState getSessionState(ctbrec.Model model) {
|
||||
for (SessionState state : sessionStates.asMap().values()) {
|
||||
if (Objects.equals(state.getNm(), model.getName())) {
|
||||
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class).indent(" ");
|
||||
System.out.println(adapter.toJson(state));
|
||||
System.out.println(model.getPreview());
|
||||
System.out.println("H5 " + serverConfig.isOnHtml5VideoServer(state));
|
||||
System.out.println("NG " + serverConfig.isOnNgServer(state));
|
||||
System.out.println("WZ " + serverConfig.isOnWzObsVideoServer(state));
|
||||
System.out.println("OBS " + serverConfig.isOnObsServer(state));
|
||||
System.out.println("URL: " + getStreamUrl(state));
|
||||
System.out.println("#####################");
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ServerConfig getServerConfig() {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ctbrec.sites.mfc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
|
@ -10,21 +9,15 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
|
@ -98,57 +91,23 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
MasterPlaylist masterPlaylist = getMasterPlaylist();
|
||||
List<StreamSource> sources = new ArrayList<>();
|
||||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
if(playlist.getStreamInfo().getResolution() != null) {
|
||||
src.width = playlist.getStreamInfo().getResolution().width;
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
} else {
|
||||
src.width = Integer.MAX_VALUE;
|
||||
src.height = Integer.MAX_VALUE;
|
||||
}
|
||||
String masterUrl = streamUrl;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
if(Config.getInstance().getSettings().mfcIgnoreUpscaled) {
|
||||
return sources.stream()
|
||||
.filter(src -> src.height != 960)
|
||||
.collect(Collectors.toList());
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
|
||||
return getStreamSourceProvider().getStreamSources(updateStreamUrl());
|
||||
}
|
||||
|
||||
private StreamSourceProvider getStreamSourceProvider() {
|
||||
if(isHlsStream()) {
|
||||
return new HlsStreamSourceProvider(getSite().getHttpClient());
|
||||
} else {
|
||||
return sources;
|
||||
return new DashStreamSourceProvider(Config.getInstance(), site);
|
||||
}
|
||||
}
|
||||
|
||||
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||
if(getHlsUrl() == null) {
|
||||
throw new IllegalStateException("Stream url unknown");
|
||||
}
|
||||
LOG.trace("Loading master playlist {}", streamUrl);
|
||||
Request req = new Request.Builder().url(streamUrl).build();
|
||||
try(Response response = site.getHttpClient().execute(req)) {
|
||||
if(response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
private boolean isHlsStream() {
|
||||
return Optional.ofNullable(streamUrl).orElse("").endsWith("m3u8");
|
||||
}
|
||||
|
||||
private String getHlsUrl() {
|
||||
private String updateStreamUrl() {
|
||||
if(streamUrl == null) {
|
||||
MyFreeCams mfc = (MyFreeCams) getSite();
|
||||
mfc.getClient().update(this);
|
||||
|
@ -211,7 +170,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
Collections.sort(streamSources);
|
||||
StreamSource best = streamSources.get(streamSources.size() - 1);
|
||||
resolution = new int[] { best.width, best.height };
|
||||
} catch (ParseException | PlaylistException e) {
|
||||
} catch (JAXBException | ParseException | PlaylistException e) {
|
||||
LOG.warn("Couldn't determine stream resolution - {}", e.getMessage());
|
||||
} catch (ExecutionException | IOException e) {
|
||||
LOG.error("Couldn't determine stream resolution", e);
|
||||
|
@ -294,25 +253,38 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
int camserv = state.getU().getCamserv();
|
||||
String server;
|
||||
ServerConfig sc = ((MyFreeCams)site).getClient().getServerConfig();
|
||||
String phase = state.getU().getPhase();
|
||||
if(sc.isOnNgServer(state)) {
|
||||
server = sc.ngVideoServers.get(Integer.toString(camserv));
|
||||
camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
|
||||
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + state.getU().getPhase()+ '_' + userChannel;
|
||||
camserv = toCamServ(server);
|
||||
previewUrl = toPreviewUrl(camserv, phase, userChannel);
|
||||
} else if(sc.isOnWzObsVideoServer(state)) {
|
||||
server = sc.wzobsServers.get(Integer.toString(camserv));
|
||||
camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
|
||||
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + state.getU().getPhase()+ '_' + userChannel;
|
||||
camserv = toCamServ(server);
|
||||
previewUrl = toPreviewUrl(camserv, phase, userChannel);
|
||||
} else if(sc.isOnHtml5VideoServer(state)) {
|
||||
server = sc.h5Servers.get(Integer.toString(camserv));
|
||||
camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
|
||||
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
|
||||
camserv = toCamServ(server);
|
||||
previewUrl = toPreviewUrl(camserv, userChannel);
|
||||
} else {
|
||||
if(camserv > 500) camserv -= 500;
|
||||
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
|
||||
previewUrl = toPreviewUrl(camserv, userChannel);
|
||||
}
|
||||
return previewUrl;
|
||||
}
|
||||
|
||||
private String toPreviewUrl(int camserv, String phase, int userChannel) {
|
||||
return "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + phase + '_' + userChannel;
|
||||
}
|
||||
|
||||
private String toPreviewUrl(int camserv, int userChannel) {
|
||||
return "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
|
||||
}
|
||||
|
||||
private int toCamServ(String server) {
|
||||
return Integer.parseInt(server.replaceAll("[^0-9]+", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean follow() {
|
||||
return ((MyFreeCams)site).getClient().follow(getUid());
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package ctbrec.sites.mfc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
|
||||
public interface StreamSourceProvider {
|
||||
|
||||
public List<StreamSource> getStreamSources(String streamUrl) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException;
|
||||
|
||||
}
|
Loading…
Reference in New Issue