forked from j62/ctbrec
1
0
Fork 0

Add BandwidthMeter, which tracks the current bandwidth usage

This commit is contained in:
0xboobface 2020-06-12 18:21:32 +02:00
parent bd48d6bf9c
commit cd6175a7eb
8 changed files with 196 additions and 19 deletions

View File

@ -31,6 +31,8 @@ import ctbrec.event.Event;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler; import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration; import ctbrec.event.EventHandlerConfiguration;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.ByteUnitFormatter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.NextGenLocalRecorder; import ctbrec.recorder.NextGenLocalRecorder;
@ -63,11 +65,15 @@ import javafx.application.Application;
import javafx.application.HostServices; import javafx.application.HostServices;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.WindowEvent; import javafx.stage.WindowEvent;
@ -83,7 +89,10 @@ public class CamrecApplication extends Application {
private OnlineMonitor onlineMonitor; private OnlineMonitor onlineMonitor;
static HostServices hostServices; static HostServices hostServices;
private SettingsTab settingsTab; 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<Site> sites = new ArrayList<>(); private List<Site> sites = new ArrayList<>();
public static HttpClient httpClient; public static HttpClient httpClient;
public static String title; public static String title;
@ -91,6 +100,9 @@ public class CamrecApplication extends Application {
private RecordedModelsTab modelsTab; private RecordedModelsTab modelsTab;
private RecordingsTab recordingsTab; private RecordingsTab recordingsTab;
private int activeRecordings = 0;
private double bytesPerSecond = 0;
@Override @Override
public void start(Stage primaryStage) throws Exception { public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage; this.primaryStage = primaryStage;
@ -109,6 +121,7 @@ public class CamrecApplication extends Application {
loadConfig(); loadConfig();
registerAlertSystem(); registerAlertSystem();
registerActiveRecordingsCounter(); registerActiveRecordingsCounter();
registerBandwidthMeterListener();
createHttpClient(); createHttpClient();
hostServices = getHostServices(); hostServices = getHostServices();
createRecorder(); createRecorder();
@ -158,26 +171,28 @@ public class CamrecApplication extends Application {
int windowWidth = Config.getInstance().getSettings().windowWidth; int windowWidth = Config.getInstance().getSettings().windowWidth;
int windowHeight = Config.getInstance().getSettings().windowHeight; int windowHeight = Config.getInstance().getSettings().windowHeight;
rootPane = new TabPane();
Scene scene = new Scene(rootPane, windowWidth, windowHeight); Scene scene = new Scene(rootPane, windowWidth, windowHeight);
primaryStage.setScene(scene); primaryStage.setScene(scene);
rootPane.setCenter(tabPane);
rootPane.setBottom(statusBar);
for (Iterator<Site> iterator = sites.iterator(); iterator.hasNext();) { for (Iterator<Site> iterator = sites.iterator(); iterator.hasNext();) {
Site site = iterator.next(); Site site = iterator.next();
if(site.isEnabled()) { if(site.isEnabled()) {
SiteTab siteTab = new SiteTab(site, scene); SiteTab siteTab = new SiteTab(site, scene);
rootPane.getTabs().add(siteTab); tabPane.getTabs().add(siteTab);
} }
} }
modelsTab = new RecordedModelsTab("Recording", recorder, sites); modelsTab = new RecordedModelsTab("Recording", recorder, sites);
rootPane.getTabs().add(modelsTab); tabPane.getTabs().add(modelsTab);
recordingsTab = new RecordingsTab("Recordings", recorder, config, sites); recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
rootPane.getTabs().add(recordingsTab); tabPane.getTabs().add(recordingsTab);
settingsTab = new SettingsTab(sites, recorder); settingsTab = new SettingsTab(sites, recorder);
rootPane.getTabs().add(settingsTab); tabPane.getTabs().add(settingsTab);
rootPane.getTabs().add(new NewsTab()); tabPane.getTabs().add(new NewsTab());
rootPane.getTabs().add(new DonateTabFx()); tabPane.getTabs().add(new DonateTabFx());
rootPane.getTabs().add(new HelpTab()); tabPane.getTabs().add(new HelpTab());
switchToStartTab(); switchToStartTab();
writeColorSchemeStyleSheet(); writeColorSchemeStyleSheet();
@ -205,7 +220,7 @@ public class CamrecApplication extends Application {
primaryStage.setOnCloseRequest(createShutdownHandler()); primaryStage.setOnCloseRequest(createShutdownHandler());
// register changelistener to activate / deactivate tabs, when the user switches between them // register changelistener to activate / deactivate tabs, when the user switches between them
rootPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (ov, from, to) -> { tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (ov, from, to) -> {
if (from instanceof TabSelectionListener) { if (from instanceof TabSelectionListener) {
((TabSelectionListener) from).deselected(); ((TabSelectionListener) from).deselected();
} }
@ -213,6 +228,9 @@ public class CamrecApplication extends Application {
((TabSelectionListener) to).selected(); ((TabSelectionListener) to).selected();
} }
}); });
statusBar.getChildren().add(statusLabel);
HBox.setMargin(statusLabel, new Insets(10, 10, 10, 10));
} }
private javafx.event.EventHandler<WindowEvent> createShutdownHandler() { private javafx.event.EventHandler<WindowEvent> 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) { if(evt.getType() == Event.Type.MODEL_ONLINE || evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) {
try { try {
List<Model> models = recorder.getCurrentlyRecording(); List<Model> models = recorder.getCurrentlyRecording();
long count = models.size(); activeRecordings = models.size();
String windowTitle = count > 0 ? "(" + count + ") " + title : title; String windowTitle = activeRecordings > 0 ? "(" + activeRecordings + ") " + title : title;
Platform.runLater(() -> primaryStage.setTitle(windowTitle)); Platform.runLater(() -> primaryStage.setTitle(windowTitle));
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Couldn't update window title", 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() { private void writeColorSchemeStyleSheet() {
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
try(FileOutputStream fos = new FileOutputStream(colorCss)) { try(FileOutputStream fos = new FileOutputStream(colorCss)) {
@ -340,15 +368,15 @@ public class CamrecApplication extends Application {
private void switchToStartTab() { private void switchToStartTab() {
String startTab = Config.getInstance().getSettings().startTab; String startTab = Config.getInstance().getSettings().startTab;
if(StringUtil.isNotBlank(startTab)) { if(StringUtil.isNotBlank(startTab)) {
for (Tab tab : rootPane.getTabs()) { for (Tab tab : tabPane.getTabs()) {
if(Objects.equals(startTab, tab.getText())) { if(Objects.equals(startTab, tab.getText())) {
rootPane.getSelectionModel().select(tab); tabPane.getSelectionModel().select(tab);
break; break;
} }
} }
} }
if(rootPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) { if(tabPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) {
((TabSelectionListener)rootPane.getSelectionModel().getSelectedItem()).selected(); ((TabSelectionListener)tabPane.getSelectionModel().getSelectedItem()).selected();
} }
} }
@ -412,7 +440,7 @@ public class CamrecApplication extends Application {
Version ctbrecVersion = getVersion(); Version ctbrecVersion = getVersion();
if (latestVersion.compareTo(ctbrecVersion) > 0) { if (latestVersion.compareTo(ctbrecVersion) > 0) {
LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion); LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion);
Platform.runLater(() -> rootPane.getTabs().add(new UpdateTab(latest))); Platform.runLater(() -> tabPane.getTabs().add(new UpdateTab(latest)));
} else { } else {
LOG.debug("ctbrec is up-to-date {}", ctbrecVersion); LOG.debug("ctbrec is up-to-date {}", ctbrecVersion);
} }

View File

@ -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<Record> records = new ArrayList<>(100);
private static Lock lock = new ReentrantLock(true);
private static List<Listener> 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<Record> 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);
}
}

View File

@ -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;
}
}

View File

@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.AbstractDownload; import ctbrec.recorder.download.AbstractDownload;
@ -217,6 +218,7 @@ public class DashDownload extends AbstractDownload {
int len = -1; int len = -1;
while ((len = in.read(b)) >= 0) { while ((len = in.read(b)) >= 0) {
out.write(b, 0, len); out.write(b, 0, len);
BandwidthMeter.add(len);
} }
out.flush(); out.flush();
} }

View File

@ -40,6 +40,7 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording.State; import ctbrec.Recording.State;
import ctbrec.UnknownModel; import ctbrec.UnknownModel;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException;
@ -95,7 +96,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
return new SegmentPlaylist(segmentsURL); 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); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
if (playlist.hasMediaPlaylist()) { if (playlist.hasMediaPlaylist()) {

View File

@ -45,6 +45,7 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.State; import ctbrec.Recording.State;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.PlaylistGenerator; import ctbrec.recorder.PlaylistGenerator;
@ -274,7 +275,7 @@ public class HlsDownload extends AbstractHlsDownload {
downloadThreadPool.shutdownNow(); downloadThreadPool.shutdownNow();
} }
private static class SegmentDownload implements Callable<Boolean> { private class SegmentDownload implements Callable<Boolean> {
private URL url; private URL url;
private Path file; private Path file;
private HttpClient client; private HttpClient client;
@ -308,6 +309,7 @@ public class HlsDownload extends AbstractHlsDownload {
int length = -1; int length = -1;
while( (length = in.read(b)) >= 0 && !Thread.currentThread().isInterrupted()) { while( (length = in.read(b)) >= 0 && !Thread.currentThread().isInterrupted()) {
fos.write(b, 0, length); fos.write(b, 0, length);
BandwidthMeter.add(length);
} }
return true; return true;
} }

View File

@ -34,6 +34,7 @@ import ctbrec.Hmac;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.io.StreamRedirectThread; import ctbrec.io.StreamRedirectThread;
@ -419,6 +420,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
try (Response response = client.execute(request)) { try (Response response = client.execute(request)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
byte[] segment = response.body().bytes(); byte[] segment = response.body().bytes();
BandwidthMeter.add(segment.length);
if (lsp.encrypted) { if (lsp.encrypted) {
segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment); segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment);
} }

View File

@ -12,6 +12,7 @@ import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload; import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
@ -44,6 +45,7 @@ public class ShowupMergedDownload extends MergedFfmpegHlsDownload {
int length = -1; int length = -1;
boolean keepGoing = true; boolean keepGoing = true;
while ((length = in.read(buffer)) >= 0 && keepGoing) { while ((length = in.read(buffer)) >= 0 && keepGoing) {
BandwidthMeter.add(length);
writeSegment(buffer, 0, length); writeSegment(buffer, 0, length);
keepGoing = running && !Thread.interrupted() && model.isOnline(true); keepGoing = running && !Thread.interrupted() && model.isOnline(true);
if (livestreamDownload && splitRecording()) { if (livestreamDownload && splitRecording()) {