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
This commit is contained in:
Doug Hoffman
2024-09-15 08:48:00 -04:00
committed by GitHub
parent e00cd90aef
commit 056bccb89a
7 changed files with 111 additions and 27 deletions

View File

@@ -34,6 +34,7 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
private Listener<BroadcastEvent> mBroadcastEventListener; private Listener<BroadcastEvent> mBroadcastEventListener;
private T mBroadcastConfiguration; private T mBroadcastConfiguration;
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY); protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
protected int mStreamedAudioCount = 0; protected int mStreamedAudioCount = 0;
protected int mErrorAudioCount = 0; protected int mErrorAudioCount = 0;
protected int mAgedOffAudioCount = 0; protected int mAgedOffAudioCount = 0;
@@ -55,6 +56,14 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
return mBroadcastState; return mBroadcastState;
} }
/**
* Observable last bad broadcast state property
*/
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
{
return mLastBadBroadcastState;
}
/** /**
* Current state of the broadcastAudio connection * Current state of the broadcastAudio connection
*/ */
@@ -68,10 +77,26 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
*/ */
public void setBroadcastState(BroadcastState broadcastState) public void setBroadcastState(BroadcastState broadcastState)
{ {
if(broadcastState == BroadcastState.CONNECTED)
{
mLastBadBroadcastState.setValue(null);
}
else if(broadcastState.isErrorState() || broadcastState.isWarningState())
{
mLastBadBroadcastState.setValue(broadcastState);
}
mBroadcastState.setValue(broadcastState); mBroadcastState.setValue(broadcastState);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE)); 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 * Starts the broadcaster
*/ */

View File

@@ -211,9 +211,17 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
super.setBroadcastState(state); super.setBroadcastState(state);
if(mBroadcastState.get() != null && mBroadcastState.get().isErrorState()) if(mBroadcastState.get() != null)
{ {
stop(); if(mBroadcastState.get().isErrorState())
{
mLog.error("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
stop();
}
else if(mBroadcastState.get().isWarningState())
{
mLog.warn("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
}
} }
if(!connected()) if(!connected())

View File

@@ -23,90 +23,92 @@ public enum BroadcastState
/** /**
* General error in configuration that causes the remote server to reject the connection * General error in configuration that causes the remote server to reject the connection
*/ */
CONFIGURATION_ERROR("Configuration Error", true), CONFIGURATION_ERROR("Configuration Error", true, false),
/** /**
* Connected to the broadcastAudio server and capable of streaming * Connected to the broadcastAudio server and capable of streaming
*/ */
CONNECTED("Connected", false), CONNECTED("Connected", false, false),
/** /**
* Connection interrupted, attempting to reconnect. * Connection interrupted, attempting to reconnect.
*/ */
CONNECTING("Connecting", false), CONNECTING("Connecting", false, false),
/** /**
* Indicates the configuration is disabled * Indicates the configuration is disabled
*/ */
DISABLED("Disabled", true), DISABLED("Disabled", true, false),
/** /**
* Disconnected from the broadcastAudio server * Disconnected from the broadcastAudio server
*/ */
DISCONNECTED("Disconnected", false), DISCONNECTED("Disconnected", false, false),
/** /**
* Invalid credentials - user name or password * Invalid credentials - user name or password
*/ */
INVALID_CREDENTIALS("Invalid User Name/Password", true), INVALID_CREDENTIALS("Invalid User Name/Password", true, false),
/** /**
* Invalid mount point * Invalid mount point
*/ */
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true), INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true, false),
/** /**
* Invalid configuration settings * Invalid configuration settings
*/ */
INVALID_SETTINGS("Invalid Settings", true), INVALID_SETTINGS("Invalid Settings", true, false),
/** /**
* Remote server max sources has been exceeded * Remote server max sources has been exceeded
*/ */
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true), MAX_SOURCES_EXCEEDED("Max Sources Exceeded", false, true),
/** /**
* Specified mount point is already in use * Specified mount point is already in use
*/ */
MOUNT_POINT_IN_USE("Mount Point In Use", false), MOUNT_POINT_IN_USE("Mount Point In Use", false, true),
/** /**
* Network is unavailable or the server address cannot be resolved * Network is unavailable or the server address cannot be resolved
*/ */
NETWORK_UNAVAILABLE("Network Unavailable", false), NETWORK_UNAVAILABLE("Network Unavailable", false, false),
/** /**
* Server is not known or reachable * Server is not known or reachable
*/ */
NO_SERVER("No Server", false), NO_SERVER("No Server", false, false),
/** /**
* Initial state with no connection attempted. * Initial state with no connection attempted.
*/ */
READY("Ready", false), READY("Ready", false, false),
/** /**
* Error while broadcasting stream data. Temporary error state to allow connection to be reset. * Error while broadcasting stream data. Temporary error state to allow connection to be reset.
*/ */
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false), TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false, false),
/** /**
* Unsupported audio format * Unsupported audio format
*/ */
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true), UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true, false),
/** /**
* Unspecified error * Unspecified error
*/ */
ERROR("Error", true); ERROR("Error", true, false);
private String mLabel; private String mLabel;
private boolean mErrorState; private boolean mErrorState;
private boolean mWarningState;
BroadcastState(String label, boolean errorState) BroadcastState(String label, boolean errorState, boolean warningState)
{ {
mLabel = label; mLabel = label;
mErrorState = errorState; mErrorState = errorState;
mWarningState = warningState;
} }
public String toString() public String toString()
@@ -118,4 +120,9 @@ public enum BroadcastState
{ {
return mErrorState; return mErrorState;
} }
public boolean isWarningState()
{
return mWarningState;
}
} }

View File

@@ -37,6 +37,7 @@ public class ConfiguredBroadcast
private BroadcastConfiguration mBroadcastConfiguration; private BroadcastConfiguration mBroadcastConfiguration;
private AbstractAudioBroadcaster mAudioBroadcaster; private AbstractAudioBroadcaster mAudioBroadcaster;
private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(); private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>();
private ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
/** /**
* Constructs an instance * Constructs an instance
@@ -89,6 +90,14 @@ public class ConfiguredBroadcast
return mBroadcastState; return mBroadcastState;
} }
/**
* Last bad broadcast state of the configured audio broadcaster (optional)
*/
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
{
return mLastBadBroadcastState;
}
/** /**
* Sets the audio broadcaster * Sets the audio broadcaster
* @param audioBroadcaster to use for this configuration * @param audioBroadcaster to use for this configuration
@@ -96,11 +105,13 @@ public class ConfiguredBroadcast
public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster) public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster)
{ {
mBroadcastState.unbind(); mBroadcastState.unbind();
mLastBadBroadcastState.unbind();
mAudioBroadcaster = audioBroadcaster; mAudioBroadcaster = audioBroadcaster;
if(audioBroadcaster != null) if(audioBroadcaster != null)
{ {
mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty()); mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty());
mLastBadBroadcastState.bind(mAudioBroadcaster.lastBadBroadcastStateProperty());
} }
else else
{ {
@@ -120,6 +131,7 @@ public class ConfiguredBroadcast
{ {
mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR); mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR);
} }
mLastBadBroadcastState.setValue(null);
} }
} }

View File

@@ -341,15 +341,29 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
} }
else 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); setBroadcastState(BroadcastState.DISCONNECTED);
disconnect(); disconnect();
} }
break; break;
default: default:
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message); mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED); setBroadcastState(BroadcastState.DISCONNECTED);
disconnect(); disconnect();
} }
} }
else else
@@ -401,9 +415,16 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
disconnect(); disconnect();
break; break;
default: 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()); mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
disconnect();
break; break;
} }
} }

View File

@@ -347,7 +347,15 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
else else
{ {
mLog.error("Unrecognized server response:" + message); 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);
}
} }
} }
} }

View File

@@ -440,7 +440,10 @@ public class StreamingEditor extends SplitPane
TableColumn stateColumn = new TableColumn("Stream Status"); TableColumn stateColumn = new TableColumn("Stream Status");
stateColumn.setCellValueFactory(new PropertyValueFactory<>("broadcastState")); 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() mConfiguredBroadcastTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> setBroadcastConfiguration(newValue)); .addListener((observable, oldValue, newValue) -> setBroadcastConfiguration(newValue));