From 98cefacae35bc84c4bfdf54149275043343ed1ea Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 11 Jul 2018 20:41:22 +0200 Subject: [PATCH] Added possibility to select the stream quality. The Settings tab now contains a checkbox to enable manual stream selection. If not checked, the stream with the highest quality is selected. --- src/main/java/ctbrec/HttpClient.java | 4 +- src/main/java/ctbrec/Model.java | 9 +++ src/main/java/ctbrec/Settings.java | 1 + src/main/java/ctbrec/recorder/Chaturbate.java | 23 ++++-- .../ctbrec/recorder/download/HlsDownload.java | 11 ++- .../recorder/download/StreamSource.java | 40 +++++++++++ src/main/java/ctbrec/ui/SettingsTab.java | 9 +++ src/main/java/ctbrec/ui/ThumbCell.java | 71 ++++++++++++++++++- src/main/resources/logback.xml | 4 +- 9 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 src/main/java/ctbrec/recorder/download/StreamSource.java diff --git a/src/main/java/ctbrec/HttpClient.java b/src/main/java/ctbrec/HttpClient.java index 91f379a6..f032e83f 100644 --- a/src/main/java/ctbrec/HttpClient.java +++ b/src/main/java/ctbrec/HttpClient.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import ctbrec.ui.CookieJarImpl; import ctbrec.ui.HtmlParser; import ctbrec.ui.Launcher; +import okhttp3.ConnectionPool; import okhttp3.Cookie; import okhttp3.FormBody; import okhttp3.OkHttpClient; @@ -32,7 +33,8 @@ public class HttpClient { .cookieJar(cookieJar) .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.SECONDS) .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.SECONDS) - .addInterceptor(new LoggingInterceptor()) + .connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES)) + //.addInterceptor(new LoggingInterceptor()) .build(); } diff --git a/src/main/java/ctbrec/Model.java b/src/main/java/ctbrec/Model.java index 7e4db43b..dd6094a2 100644 --- a/src/main/java/ctbrec/Model.java +++ b/src/main/java/ctbrec/Model.java @@ -10,6 +10,7 @@ public class Model { private String description; private List tags = new ArrayList<>(); private boolean online = false; + private int streamUrlIndex = -1; public String getUrl() { return url; @@ -59,6 +60,14 @@ public class Model { this.description = description; } + public int getStreamUrlIndex() { + return streamUrlIndex; + } + + public void setStreamUrlIndex(int streamUrlIndex) { + this.streamUrlIndex = streamUrlIndex; + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index f0f580c2..bff554ef 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -19,5 +19,6 @@ public class Settings { public boolean automergeKeepSegments = false; public boolean determineResolution = false; public boolean requireAuthentication = false; + public boolean chooseStreamQuality = false; public byte[] key = null; } diff --git a/src/main/java/ctbrec/recorder/Chaturbate.java b/src/main/java/ctbrec/recorder/Chaturbate.java index ec44e92c..b030006e 100644 --- a/src/main/java/ctbrec/recorder/Chaturbate.java +++ b/src/main/java/ctbrec/recorder/Chaturbate.java @@ -2,7 +2,6 @@ package ctbrec.recorder; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,11 +60,7 @@ public class Chaturbate { return res; } - URL masterUrl = new URL(streamInfo.url); - InputStream inputStream = masterUrl.openStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); - Playlist playlist = parser.parse(); - MasterPlaylist master = playlist.getMasterPlaylist(); + MasterPlaylist master = getMasterPlaylist(model, client); for (PlaylistData playlistData : master.getPlaylists()) { if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) { int h = playlistData.getStreamInfo().getResolution().height; @@ -78,4 +73,20 @@ public class Chaturbate { } return res; } + + public static MasterPlaylist getMasterPlaylist(Model model, HttpClient client) throws IOException, ParseException, PlaylistException { + StreamInfo streamInfo = getStreamInfo(model, client); + return getMasterPlaylist(streamInfo, client); + } + + public static MasterPlaylist getMasterPlaylist(StreamInfo streamInfo, HttpClient client) throws IOException, ParseException, PlaylistException { + LOG.trace("Loading master playlist {}", streamInfo.url); + Request req = new Request.Builder().url(streamInfo.url).build(); + Response response = client.execute(req); + InputStream inputStream = response.body().byteStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + return master; + } } diff --git a/src/main/java/ctbrec/recorder/download/HlsDownload.java b/src/main/java/ctbrec/recorder/download/HlsDownload.java index dc714140..23b7366c 100644 --- a/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -69,7 +69,7 @@ public class HlsDownload implements Download { Files.createDirectories(downloadDir); } - String segments = parseMaster(streamInfo.url); + String segments = parseMaster(streamInfo.url, model.getStreamUrlIndex()); if(segments != null) { int lastSegment = 0; int nextSegment = 0; @@ -167,14 +167,19 @@ public class HlsDownload implements Download { return null; } - private String parseMaster(String url) throws IOException, ParseException, PlaylistException { + private String parseMaster(String url, int streamUrlIndex) throws IOException, ParseException, PlaylistException { URL masterUrl = new URL(url); InputStream inputStream = masterUrl.openStream(); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); Playlist playlist = parser.parse(); if(playlist.hasMasterPlaylist()) { MasterPlaylist master = playlist.getMasterPlaylist(); - PlaylistData bestQuality = master.getPlaylists().get(master.getPlaylists().size()-1); + PlaylistData bestQuality = null; + if(streamUrlIndex >= 0 && streamUrlIndex < master.getPlaylists().size()) { + bestQuality = master.getPlaylists().get(streamUrlIndex); + } else { + bestQuality = master.getPlaylists().get(master.getPlaylists().size()-1); + } String uri = bestQuality.getUri(); if(!uri.startsWith("http")) { String _masterUrl = masterUrl.toString(); diff --git a/src/main/java/ctbrec/recorder/download/StreamSource.java b/src/main/java/ctbrec/recorder/download/StreamSource.java new file mode 100644 index 00000000..ab9da42c --- /dev/null +++ b/src/main/java/ctbrec/recorder/download/StreamSource.java @@ -0,0 +1,40 @@ +package ctbrec.recorder.download; + +import java.text.DecimalFormat; + +public class StreamSource { + public int bandwidth; + public int height; + public String mediaPlaylistUrl; + + public int getBandwidth() { + return bandwidth; + } + + public void setBandwidth(int bandwidth) { + this.bandwidth = bandwidth; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public String getMediaPlaylistUrl() { + return mediaPlaylistUrl; + } + + public void setMediaPlaylistUrl(String mediaPlaylistUrl) { + this.mediaPlaylistUrl = mediaPlaylistUrl; + } + + @Override + public String toString() { + DecimalFormat df = new DecimalFormat("0.00"); + float mbit = bandwidth / 1024.0f / 1024.0f; + return height + "p (" + df.format(mbit) + " Mbit/s)"; + } +} diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index e2500d19..1804b69d 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -50,6 +50,7 @@ public class SettingsTab extends Tab { private CheckBox secureCommunication = new CheckBox(); private CheckBox automerge = new CheckBox(); private CheckBox automergeKeepSegments = new CheckBox(); + private CheckBox chooseStreamQuality = new CheckBox(); private PasswordField password; private RadioButton recordLocal; private RadioButton recordRemote; @@ -124,6 +125,14 @@ public class SettingsTab extends Tab { GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); layout.add(loadResolution, 1, row); + l = new Label("Manually select stream quality"); + layout.add(l, 0, ++row); + chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); + chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected()); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + layout.add(chooseStreamQuality, 1, row); + l = new Label("Auto-merge recordings"); layout.add(l, 0, ++row); automerge.setSelected(Config.getInstance().getSettings().automerge); diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index 05d436a0..cc16b33f 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -1,15 +1,21 @@ package ctbrec.ui; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.Config; import ctbrec.HttpClient; @@ -17,6 +23,7 @@ import ctbrec.Model; import ctbrec.recorder.Chaturbate; import ctbrec.recorder.Recorder; import ctbrec.recorder.StreamInfo; +import ctbrec.recorder.download.StreamSource; import javafx.animation.FadeTransition; import javafx.animation.FillTransition; import javafx.animation.Interpolator; @@ -26,12 +33,14 @@ import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; +import javafx.concurrent.Task; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Alert; +import javafx.scene.control.ChoiceDialog; import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.image.Image; @@ -334,16 +343,76 @@ public class ThumbCell extends StackPane { private void startStopAction(boolean start) { setCursor(Cursor.WAIT); + + boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality; + if(selectSource && start) { + Task> selectStreamSource = new Task>() { + @Override + protected List call() throws Exception { + StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client); + MasterPlaylist masterPlaylist = Chaturbate.getMasterPlaylist(streamInfo, client); + List sources = new ArrayList<>(); + for (PlaylistData playlist : masterPlaylist.getPlaylists()) { + if (playlist.hasStreamInfo()) { + StreamSource src = new StreamSource(); + src.bandwidth = playlist.getStreamInfo().getBandwidth(); + src.height = playlist.getStreamInfo().getResolution().height; + String masterUrl = streamInfo.url; + 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); + } + } + return sources; + } + }; + selectStreamSource.setOnSucceeded((e) -> { + List sources; + try { + sources = selectStreamSource.get(); + ChoiceDialog choiceDialog = new ChoiceDialog(sources.get(sources.size()-1), sources); + choiceDialog.setTitle("Stream Quality"); + choiceDialog.setHeaderText("Select your preferred stream quality"); + Optional selectedSource = choiceDialog.showAndWait(); + if(selectedSource.isPresent()) { + int index = sources.indexOf(selectedSource.get()); + model.setStreamUrlIndex(index); + _startStopAction(model, start); + } + } catch (InterruptedException | ExecutionException e1) { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't start/stop recording"); + alert.setContentText("I/O error while starting/stopping the recording: " + e1.getLocalizedMessage()); + alert.showAndWait(); + } + }); + selectStreamSource.setOnFailed((e) -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't start/stop recording"); + alert.setContentText("I/O error while starting/stopping the recording: " + selectStreamSource.getException().getLocalizedMessage()); + alert.showAndWait(); + }); + new Thread(selectStreamSource).start(); + } else { + _startStopAction(model, start); + } + } + + private void _startStopAction(Model model, boolean start) { new Thread() { @Override public void run() { try { if(start) { + // start the recording recorder.startRecording(model); } else { recorder.stopRecording(model); } - setRecording(start); } catch (Exception e1) { LOG.error("Couldn't start/stop recording", e1); Platform.runLater(() -> { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index be906a27..3ecd8ee2 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -29,7 +29,7 @@ --> - +