Add BandwidthMeter, which tracks the current bandwidth usage
This commit is contained in:
parent
bd48d6bf9c
commit
cd6175a7eb
|
@ -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<Site> 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<Site> 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<Tab>) (ov, from, to) -> {
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (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<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) {
|
||||
try {
|
||||
List<Model> 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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<Boolean> {
|
||||
private class SegmentDownload implements Callable<Boolean> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
Loading…
Reference in New Issue