diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index 432bcf53..c492164a 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -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 getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { return delegate.getStreamSources(); } diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index b31878af..14b0f53b 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -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); } diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index c8f0130f..4b55599a 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -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> updateService; private Config config; @@ -91,7 +88,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { FlowPane grid = new FlowPane(); ScrollPane scrollPane = new ScrollPane(); - TableView table = new TableView(); + TableView table = new TableView<>(); ObservableList 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 date = new TableColumn<>("Date"); - date.setCellValueFactory((cdf) -> { + date.setCellValueFactory(cdf -> { Instant instant = cdf.getValue().getStartDate(); return new SimpleObjectProperty(instant); }); - date.setCellFactory(new Callback, TableCell>() { - @Override - public TableCell call(TableColumn param) { - TableCell cell = new TableCell() { - @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 status = new TableColumn<>("Status"); - status.setCellValueFactory((cdf) -> cdf.getValue().getStatusProperty()); + status.setCellValueFactory(cdf -> cdf.getValue().getStatusProperty()); status.setPrefWidth(300); TableColumn progress = new TableColumn<>("Progress"); - progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty()); + progress.setCellValueFactory(cdf -> cdf.getValue().getProgressProperty()); progress.setPrefWidth(100); TableColumn size = new TableColumn<>("Size"); size.setStyle("-fx-alignment: CENTER-RIGHT;"); size.setPrefWidth(100); size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty()); - size.setCellFactory(new Callback, TableCell>() { - @Override - public TableCell call(TableColumn param) { - TableCell cell = new TableCell() { - @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 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 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 createSizeCell() { + TableCell cell = new TableCell() { + @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 createDateCell() { + TableCell cell = new TableCell() { + @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 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 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> createUpdateService() { - ScheduledService> updateService = new ScheduledService>() { + ScheduledService> service = new ScheduledService>() { @Override protected Task> createTask() { return new Task>() { @Override - public List call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + public List call() throws IOException, InvalidKeyException, NoSuchAlgorithmException { updateSpace(); List 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 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 deleted = new ArrayList<>(); - for (Iterator 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 recordings) { + Thread deleteThread = new Thread(() -> { + recordingsLock.lock(); + try { + List deleted = new ArrayList<>(); + for (Iterator 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 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; diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/HDCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/HDCamsUpdateService.java index 9fd990ac..2cdeccc1 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/HDCamsUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/HDCamsUpdateService.java @@ -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 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()); } diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index fe9a4448..01a58b78 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -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 { public State getOnlineState(boolean failFast) throws IOException, ExecutionException; - public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException; + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException; public void invalidateCacheEntries(); diff --git a/common/src/main/java/ctbrec/OS.java b/common/src/main/java/ctbrec/OS.java index 4c26d3bd..76f759cc 100644 --- a/common/src/main/java/ctbrec/OS.java +++ b/common/src/main/java/ctbrec/OS.java @@ -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) { diff --git a/common/src/main/java/ctbrec/UnsupportedOperatingSystemException.java b/common/src/main/java/ctbrec/UnsupportedOperatingSystemException.java index 69f9d456..c1340794 100644 --- a/common/src/main/java/ctbrec/UnsupportedOperatingSystemException.java +++ b/common/src/main/java/ctbrec/UnsupportedOperatingSystemException.java @@ -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); } } diff --git a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java index 351d5010..15fa8267 100644 --- a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java @@ -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 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 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 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 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) { diff --git a/common/src/main/java/ctbrec/recorder/download/dash/FfmpegMuxer.java b/common/src/main/java/ctbrec/recorder/download/dash/FfmpegMuxer.java index 13763b2c..0f3c7d8a 100644 --- a/common/src/main/java/ctbrec/recorder/download/dash/FfmpegMuxer.java +++ b/common/src/main/java/ctbrec/recorder/download/dash/FfmpegMuxer.java @@ -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); diff --git a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java index 91d37571..7754ebad 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java @@ -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 streamSources = model.getStreamSources(); Collections.sort(streamSources); diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java index 75537d60..23ce8d6a 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java @@ -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); } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java index 424b6ca0..8436ff37 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java @@ -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); } } diff --git a/common/src/main/java/ctbrec/sites/mfc/DashStreamSourceProvider.java b/common/src/main/java/ctbrec/sites/mfc/DashStreamSourceProvider.java new file mode 100644 index 00000000..91941cfb --- /dev/null +++ b/common/src/main/java/ctbrec/sites/mfc/DashStreamSourceProvider.java @@ -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 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 root = (JAXBElement) u.unmarshal(new ByteArrayInputStream(manifest.getBytes())); + MPDtype mpd = root.getValue(); + List periods = mpd.getPeriod(); + if (periods.isEmpty()) { + return Collections.emptyList(); + } else { + PeriodType period = periods.get(0); + List videoStreams = new ArrayList<>(); + List 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(); + } + } +} diff --git a/common/src/main/java/ctbrec/sites/mfc/HlsStreamSourceProvider.java b/common/src/main/java/ctbrec/sites/mfc/HlsStreamSourceProvider.java new file mode 100644 index 00000000..4cba4bc1 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/mfc/HlsStreamSourceProvider.java @@ -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 getStreamSources(String streamUrl) throws IOException, ExecutionException, ParseException, PlaylistException { + MasterPlaylist masterPlaylist = getMasterPlaylist(streamUrl); + List 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()); + } + } + } +} diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 6dbcded3..2741c6d2 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -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 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() { diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index c3634f06..b7e56a4c 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -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 getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - MasterPlaylist masterPlaylist = getMasterPlaylist(); - List 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 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()); diff --git a/common/src/main/java/ctbrec/sites/mfc/StreamSourceProvider.java b/common/src/main/java/ctbrec/sites/mfc/StreamSourceProvider.java new file mode 100644 index 00000000..adf277eb --- /dev/null +++ b/common/src/main/java/ctbrec/sites/mfc/StreamSourceProvider.java @@ -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 getStreamSources(String streamUrl) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException; + +}