From 056bccb89abbd31f8b4f630a24e91e8814ce7eec Mon Sep 17 00:00:00 2001 From: Doug Hoffman Date: Sun, 15 Sep 2024 08:48:00 -0400 Subject: [PATCH] Improvements to icecast failure handling (#1401) - Add mechanism to persist failure reason while continuing to reconnect - Add 'warning' attribute for failure conditions that may be temporary, but still need to be reported - Add logging for failures - Prevent possibly temporary server-side conditions from blocking reconnect attempts - Detect icecast 2.4 auth failures - Prevent generic errors from overriding existing error state --- .../broadcast/AbstractAudioBroadcaster.java | 25 +++++++++++ .../broadcast/AudioStreamingBroadcaster.java | 12 +++++- .../audio/broadcast/BroadcastState.java | 41 +++++++++++-------- .../audio/broadcast/ConfiguredBroadcast.java | 12 ++++++ .../icecast/IcecastHTTPAudioBroadcaster.java | 33 ++++++++++++--- .../icecast/IcecastTCPAudioBroadcaster.java | 10 ++++- .../playlist/streaming/StreamingEditor.java | 5 ++- 7 files changed, 111 insertions(+), 27 deletions(-) diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java index c6aa11ee..01d3518a 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java @@ -34,6 +34,7 @@ public abstract class AbstractAudioBroadcaster private Listener mBroadcastEventListener; private T mBroadcastConfiguration; protected ObjectProperty mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY); + protected ObjectProperty mLastBadBroadcastState = new SimpleObjectProperty<>(); protected int mStreamedAudioCount = 0; protected int mErrorAudioCount = 0; protected int mAgedOffAudioCount = 0; @@ -55,6 +56,14 @@ public abstract class AbstractAudioBroadcaster return mBroadcastState; } + /** + * Observable last bad broadcast state property + */ + public ObjectProperty lastBadBroadcastStateProperty() + { + return mLastBadBroadcastState; + } + /** * Current state of the broadcastAudio connection */ @@ -68,10 +77,26 @@ public abstract class AbstractAudioBroadcaster */ public void setBroadcastState(BroadcastState broadcastState) { + if(broadcastState == BroadcastState.CONNECTED) + { + mLastBadBroadcastState.setValue(null); + } + else if(broadcastState.isErrorState() || broadcastState.isWarningState()) + { + mLastBadBroadcastState.setValue(broadcastState); + } mBroadcastState.setValue(broadcastState); broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE)); } + /** + * Last bad state of the broadcastAudio connection + */ + public BroadcastState getLastBadBroadcastState() + { + return mLastBadBroadcastState.get(); + } + /** * Starts the broadcaster */ diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java index 2cc16b32..19aa94a7 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java @@ -211,9 +211,17 @@ public abstract class AudioStreamingBroadcaster mBroadcastState = new SimpleObjectProperty<>(); + private ObjectProperty mLastBadBroadcastState = new SimpleObjectProperty<>(); /** * Constructs an instance @@ -89,6 +90,14 @@ public class ConfiguredBroadcast return mBroadcastState; } + /** + * Last bad broadcast state of the configured audio broadcaster (optional) + */ + public ObjectProperty lastBadBroadcastStateProperty() + { + return mLastBadBroadcastState; + } + /** * Sets the audio broadcaster * @param audioBroadcaster to use for this configuration @@ -96,11 +105,13 @@ public class ConfiguredBroadcast public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster) { mBroadcastState.unbind(); + mLastBadBroadcastState.unbind(); mAudioBroadcaster = audioBroadcaster; if(audioBroadcaster != null) { mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty()); + mLastBadBroadcastState.bind(mAudioBroadcaster.lastBadBroadcastStateProperty()); } else { @@ -120,6 +131,7 @@ public class ConfiguredBroadcast { mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR); } + mLastBadBroadcastState.setValue(null); } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java index 2f0e617d..0f6d3db9 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java @@ -341,15 +341,29 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster } else { - mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message); + mLog.error("String [" + getStreamName() + "] - HTTP 403 protocol decoder error - message:\n\n" + message); + setBroadcastState(BroadcastState.DISCONNECTED); + disconnect(); + } + break; + case 401: //Unauthorized + if(message.toString().contains("Authentication Required")) + { + mLog.error("Stream [" + getStreamName() + "] - unable to connect - invalid credentials"); + setBroadcastState(BroadcastState.INVALID_CREDENTIALS); + disconnect(); + } + else + { + mLog.error("String [" + getStreamName() + "] - HTTP 401 protocol decoder error - message:\n\n" + message); setBroadcastState(BroadcastState.DISCONNECTED); disconnect(); } break; default: - mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message); - setBroadcastState(BroadcastState.DISCONNECTED); - disconnect(); + mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message); + setBroadcastState(BroadcastState.DISCONNECTED); + disconnect(); } } else @@ -401,9 +415,16 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster disconnect(); break; default: - setBroadcastState(BroadcastState.ERROR); - disconnect(); + /** + * Only allow a generic error to update state if we've not already experienced a more + * specific error. Otherwise, trailing messages will clear the more meaningful error state. + */ + if(!getBroadcastState().isErrorState()) + { + setBroadcastState(BroadcastState.ERROR); + } mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass()); + disconnect(); break; } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java index adb6064b..851e945c 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java @@ -347,7 +347,15 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster else { mLog.error("Unrecognized server response:" + message); - setBroadcastState(BroadcastState.ERROR); + + /** + * Only allow a generic error to update state if we've not already experienced a more + * specific error. Otherwise, trailing messages will clear the more meaningful error state. + */ + if(!getBroadcastState().isErrorState()) + { + setBroadcastState(BroadcastState.ERROR); + } } } } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamingEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamingEditor.java index 161e39e9..44bedc2e 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamingEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamingEditor.java @@ -440,7 +440,10 @@ public class StreamingEditor extends SplitPane TableColumn stateColumn = new TableColumn("Stream Status"); stateColumn.setCellValueFactory(new PropertyValueFactory<>("broadcastState")); - mConfiguredBroadcastTableView.getColumns().addAll(enabledColumn, nameColumn, typeColumn, stateColumn); + TableColumn errorColumn = new TableColumn("Last Error"); + errorColumn.setCellValueFactory(new PropertyValueFactory<>("lastBadBroadcastState")); + + mConfiguredBroadcastTableView.getColumns().addAll(enabledColumn, nameColumn, typeColumn, stateColumn, errorColumn); mConfiguredBroadcastTableView.getSelectionModel().selectedItemProperty() .addListener((observable, oldValue, newValue) -> setBroadcastConfiguration(newValue));