diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index dc7ed021..af619c8a 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -31,6 +31,8 @@ import ctbrec.event.Event; import ctbrec.event.EventBusHolder; import ctbrec.event.EventHandler; import ctbrec.event.EventHandlerConfiguration; +import ctbrec.io.BandwidthMeter; +import ctbrec.io.ByteUnitFormatter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.NextGenLocalRecorder; @@ -63,11 +65,15 @@ import javafx.application.Application; import javafx.application.HostServices; import javafx.application.Platform; import javafx.beans.value.ChangeListener; +import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Alert; +import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.stage.WindowEvent; @@ -83,7 +89,10 @@ public class CamrecApplication extends Application { private OnlineMonitor onlineMonitor; static HostServices hostServices; private SettingsTab settingsTab; - private TabPane rootPane = new TabPane(); + private BorderPane rootPane = new BorderPane(); + private HBox statusBar = new HBox(); + private Label statusLabel = new Label(); + private TabPane tabPane = new TabPane(); private List sites = new ArrayList<>(); public static HttpClient httpClient; public static String title; @@ -91,6 +100,9 @@ public class CamrecApplication extends Application { private RecordedModelsTab modelsTab; private RecordingsTab recordingsTab; + private int activeRecordings = 0; + private double bytesPerSecond = 0; + @Override public void start(Stage primaryStage) throws Exception { this.primaryStage = primaryStage; @@ -109,6 +121,7 @@ public class CamrecApplication extends Application { loadConfig(); registerAlertSystem(); registerActiveRecordingsCounter(); + registerBandwidthMeterListener(); createHttpClient(); hostServices = getHostServices(); createRecorder(); @@ -158,26 +171,28 @@ public class CamrecApplication extends Application { int windowWidth = Config.getInstance().getSettings().windowWidth; int windowHeight = Config.getInstance().getSettings().windowHeight; - rootPane = new TabPane(); + Scene scene = new Scene(rootPane, windowWidth, windowHeight); primaryStage.setScene(scene); + rootPane.setCenter(tabPane); + rootPane.setBottom(statusBar); for (Iterator iterator = sites.iterator(); iterator.hasNext();) { Site site = iterator.next(); if(site.isEnabled()) { SiteTab siteTab = new SiteTab(site, scene); - rootPane.getTabs().add(siteTab); + tabPane.getTabs().add(siteTab); } } modelsTab = new RecordedModelsTab("Recording", recorder, sites); - rootPane.getTabs().add(modelsTab); + tabPane.getTabs().add(modelsTab); recordingsTab = new RecordingsTab("Recordings", recorder, config, sites); - rootPane.getTabs().add(recordingsTab); + tabPane.getTabs().add(recordingsTab); settingsTab = new SettingsTab(sites, recorder); - rootPane.getTabs().add(settingsTab); - rootPane.getTabs().add(new NewsTab()); - rootPane.getTabs().add(new DonateTabFx()); - rootPane.getTabs().add(new HelpTab()); + tabPane.getTabs().add(settingsTab); + tabPane.getTabs().add(new NewsTab()); + tabPane.getTabs().add(new DonateTabFx()); + tabPane.getTabs().add(new HelpTab()); switchToStartTab(); writeColorSchemeStyleSheet(); @@ -205,7 +220,7 @@ public class CamrecApplication extends Application { primaryStage.setOnCloseRequest(createShutdownHandler()); // register changelistener to activate / deactivate tabs, when the user switches between them - rootPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener) (ov, from, to) -> { + tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener) (ov, from, to) -> { if (from instanceof TabSelectionListener) { ((TabSelectionListener) from).deselected(); } @@ -213,6 +228,9 @@ public class CamrecApplication extends Application { ((TabSelectionListener) to).selected(); } }); + + statusBar.getChildren().add(statusLabel); + HBox.setMargin(statusLabel, new Insets(10, 10, 10, 10)); } private javafx.event.EventHandler createShutdownHandler() { @@ -303,8 +321,8 @@ public class CamrecApplication extends Application { if(evt.getType() == Event.Type.MODEL_ONLINE || evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) { try { List models = recorder.getCurrentlyRecording(); - long count = models.size(); - String windowTitle = count > 0 ? "(" + count + ") " + title : title; + activeRecordings = models.size(); + String windowTitle = activeRecordings > 0 ? "(" + activeRecordings + ") " + title : title; Platform.runLater(() -> primaryStage.setTitle(windowTitle)); } catch (Exception e) { LOG.warn("Couldn't update window title", e); @@ -314,6 +332,16 @@ public class CamrecApplication extends Application { }); } + private void registerBandwidthMeterListener() { + BandwidthMeter.addListener((bytes, dur) -> { + long seconds = dur.getSeconds(); + bytesPerSecond = bytes / (double)seconds; + String humanreadable = ByteUnitFormatter.format(bytesPerSecond); + String status = String.format("Recording %s / %s models @ %s/s", activeRecordings, recorder.getModels().size(), humanreadable); + Platform.runLater(() -> statusLabel.setText(status)); + }); + } + private void writeColorSchemeStyleSheet() { File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); try(FileOutputStream fos = new FileOutputStream(colorCss)) { @@ -340,15 +368,15 @@ public class CamrecApplication extends Application { private void switchToStartTab() { String startTab = Config.getInstance().getSettings().startTab; if(StringUtil.isNotBlank(startTab)) { - for (Tab tab : rootPane.getTabs()) { + for (Tab tab : tabPane.getTabs()) { if(Objects.equals(startTab, tab.getText())) { - rootPane.getSelectionModel().select(tab); + tabPane.getSelectionModel().select(tab); break; } } } - if(rootPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) { - ((TabSelectionListener)rootPane.getSelectionModel().getSelectedItem()).selected(); + if(tabPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) { + ((TabSelectionListener)tabPane.getSelectionModel().getSelectedItem()).selected(); } } @@ -412,7 +440,7 @@ public class CamrecApplication extends Application { Version ctbrecVersion = getVersion(); if (latestVersion.compareTo(ctbrecVersion) > 0) { LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion); - Platform.runLater(() -> rootPane.getTabs().add(new UpdateTab(latest))); + Platform.runLater(() -> tabPane.getTabs().add(new UpdateTab(latest))); } else { LOG.debug("ctbrec is up-to-date {}", ctbrecVersion); } diff --git a/common/src/main/java/ctbrec/io/BandwidthMeter.java b/common/src/main/java/ctbrec/io/BandwidthMeter.java new file mode 100644 index 00000000..1d7aabd8 --- /dev/null +++ b/common/src/main/java/ctbrec/io/BandwidthMeter.java @@ -0,0 +1,96 @@ +package ctbrec.io; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class BandwidthMeter { + + private static final Duration MEASURE_TIMEFRAME = Duration.ofSeconds(10); + private static List records = new ArrayList<>(100); + private static Lock lock = new ReentrantLock(true); + private static List listeners = new ArrayList<>(); + private static Instant lastUpdate = Instant.EPOCH; + + private BandwidthMeter() { + } + + public static void add(long bytes) { + Record r = new Record(bytes); + lock.lock(); + try { + records.add(r); + } finally { + lock.unlock(); + } + + Instant oneSecondAgo = Instant.now().minus(Duration.ofSeconds(1)); + if (lastUpdate.isBefore(oneSecondAgo)) { + long throughput = getThroughput(); + for (Listener listener : listeners) { + listener.bandwidthCalculated(throughput, MEASURE_TIMEFRAME); + } + lastUpdate = Instant.now(); + } + } + + /** + * Get the throughput over the last 10 seconds + * @return throughput in bytes + */ + public static long getThroughput() { + return getThroughput(MEASURE_TIMEFRAME); + } + + /** + * Get the throughput over the given duration + * @return throughput in bytes + */ + public static long getThroughput(Duration d) { + Instant now = Instant.now(); + Instant measureStart = now.minus(d); + long throughput = 0; + lock.lock(); + try { + for (Iterator iterator = records.iterator(); iterator.hasNext();) { + Record record = iterator.next(); + if (record.timestamp.isBefore(measureStart)) { + iterator.remove(); + } else { + throughput += record.bytes; + } + } + + } finally { + lock.unlock(); + } + return throughput; + } + + public static void addListener(Listener l) { + listeners.add(l); + } + + public static void removeListener(Listener l) { + listeners.remove(l); + } + + private static class Record { + public Instant timestamp; + public long bytes; + + public Record(long bytes) { + timestamp = Instant.now(); + this.bytes = bytes; + } + } + + @FunctionalInterface + public static interface Listener { + void bandwidthCalculated(long bytes, Duration timeframe); + } +} diff --git a/common/src/main/java/ctbrec/io/ByteUnitFormatter.java b/common/src/main/java/ctbrec/io/ByteUnitFormatter.java new file mode 100644 index 00000000..dc748de4 --- /dev/null +++ b/common/src/main/java/ctbrec/io/ByteUnitFormatter.java @@ -0,0 +1,42 @@ +package ctbrec.io; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +public class ByteUnitFormatter { + + private ByteUnitFormatter() { + } + + public static final long KiB = 1024; + public static final long MiB = KiB * 1024; + public static final long GiB = MiB * 1024; + public static final long TiB = GiB * 1024; + + private static NumberFormat nf = new DecimalFormat(".00"); + + public static String format(long bytes) { + double decimal = bytes; + return format(decimal); + } + + public static String format(double bytes) { + double decimal = bytes; + String unit = "Bytes"; + if (bytes >= TiB) { + decimal = bytes / (1024 * 1024 * 1024 * 1024); + unit = "TiB"; + } else if (bytes >= GiB) { + decimal = bytes / (1024 * 1024 * 1024); + unit = "GiB"; + } else if (bytes >= MiB) { + decimal = bytes / (1024 * 1024); + unit = "MiB"; + } else if (bytes >= KiB) { + decimal = bytes / 1024; + unit = "KiB"; + } + + return nf.format(decimal) + " " + unit; + } +} 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 35a0111e..63a2c509 100644 --- a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java @@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording; +import ctbrec.io.BandwidthMeter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.download.AbstractDownload; @@ -217,6 +218,7 @@ public class DashDownload extends AbstractDownload { int len = -1; while ((len = in.read(b)) >= 0) { out.write(b, 0, len); + BandwidthMeter.add(len); } out.flush(); } 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 f9397b4a..ce885e78 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java @@ -40,6 +40,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording.State; import ctbrec.UnknownModel; +import ctbrec.io.BandwidthMeter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; @@ -95,7 +96,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload { return new SegmentPlaylist(segmentsURL); } - InputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + BandwidthMeter.add(bytes.length); + InputStream inputStream = new ByteArrayInputStream(bytes); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); if (playlist.hasMediaPlaylist()) { diff --git a/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java index 3c4fd217..4360e878 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java @@ -45,6 +45,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording; import ctbrec.Recording.State; +import ctbrec.io.BandwidthMeter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.PlaylistGenerator; @@ -274,7 +275,7 @@ public class HlsDownload extends AbstractHlsDownload { downloadThreadPool.shutdownNow(); } - private static class SegmentDownload implements Callable { + private class SegmentDownload implements Callable { private URL url; private Path file; private HttpClient client; @@ -308,6 +309,7 @@ public class HlsDownload extends AbstractHlsDownload { int length = -1; while( (length = in.read(b)) >= 0 && !Thread.currentThread().isInterrupted()) { fos.write(b, 0, length); + BandwidthMeter.add(length); } return true; } diff --git a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java index 5bff886a..fc71c099 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java @@ -34,6 +34,7 @@ import ctbrec.Hmac; import ctbrec.Model; import ctbrec.OS; import ctbrec.Recording; +import ctbrec.io.BandwidthMeter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.io.StreamRedirectThread; @@ -419,6 +420,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { try (Response response = client.execute(request)) { if (response.isSuccessful()) { byte[] segment = response.body().bytes(); + BandwidthMeter.add(segment.length); if (lsp.encrypted) { segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment); } diff --git a/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java b/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java index 1fad793b..b9516ad0 100644 --- a/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java +++ b/common/src/main/java/ctbrec/sites/showup/ShowupMergedDownload.java @@ -12,6 +12,7 @@ import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; +import ctbrec.io.BandwidthMeter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload; @@ -44,6 +45,7 @@ public class ShowupMergedDownload extends MergedFfmpegHlsDownload { int length = -1; boolean keepGoing = true; while ((length = in.read(buffer)) >= 0 && keepGoing) { + BandwidthMeter.add(length); writeSegment(buffer, 0, length); keepGoing = running && !Thread.interrupted() && model.isOnline(true); if (livestreamDownload && splitRecording()) {