fix src folder structure

This commit is contained in:
Roman Tsypuk
2017-12-15 23:22:32 +02:00
parent f2d1ac7e48
commit b1e07f762c
981 changed files with 7763 additions and 7775 deletions

View File

@@ -0,0 +1,52 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.IBroadcastMetadataUpdater;
public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
{
private IBroadcastMetadataUpdater mMetadataUpdater;
public IcecastAudioBroadcaster(BroadcastConfiguration broadcastConfiguration)
{
super(broadcastConfiguration);
}
/**
* Icecast broadcast configuration
*/
protected IcecastConfiguration getConfiguration()
{
return (IcecastConfiguration) getBroadcastConfiguration();
}
@Override
protected IBroadcastMetadataUpdater getMetadataUpdater()
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration());
}
return mMetadataUpdater;
}
}

View File

@@ -0,0 +1,279 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.IBroadcastMetadataUpdater;
import channel.metadata.Metadata;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.HttpMethod;
import org.apache.mina.http.api.HttpRequest;
import org.apache.mina.http.api.HttpVersion;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import util.ThreadPool;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.nio.channels.UnresolvedAddressException;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class);
private final static String UTF8 = "UTF-8";
private IcecastConfiguration mIcecastConfiguration;
private NioSocketConnector mSocketConnector;
private AtomicBoolean mUpdating = new AtomicBoolean();
private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
private Map<String,String> mHTTPHeaders;
private boolean mStackTraceLoggingSuppressed;
/**
* Icecast song metadata updater. Each metadata update is processed in the order received and the dispatch of
* the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
* sequence, those updates will be queued and processed in the order received.
*/
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration)
{
mIcecastConfiguration = icecastConfiguration;
}
/**
* Socket connector - lazy constructor.
*/
private NioSocketConnector getSocketConnector()
{
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastBroadcastMetadataUpdater.class));
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
//Each metadata update session is single-use, so we'll shut it down upon success or failure
mSocketConnector.setHandler(new IoHandlerAdapter()
{
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
//Single-use session - close it after we receive an error
session.closeNow();
mLog.error("Metadata update failed");
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception
{
//Single-use session - close it after we receive a response
session.closeNow();
}
});
}
return mSocketConnector;
}
/**
* Sends a song metadata update to the remote server. Additional metadata updates received while the updater
* is in the process of sending an existing metadata update will be queued and processed in received order.
*
* Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
* and will remain in the update queue until the next metadata update is requested. However, this is a design
* trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
*/
public void update(Metadata metadata)
{
mMetadataQueue.offer(getSong(metadata));
if(mUpdating.compareAndSet(false, true))
{
String song = mMetadataQueue.poll();
while(song != null)
{
HttpRequest updateRequest = createUpdateRequest(song);
if(updateRequest != null)
{
ThreadPool.SCHEDULED.schedule(new Runnable()
{
@Override
public void run()
{
try
{
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
mIcecastConfiguration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
if(session != null)
{
session.write(updateRequest);
}
}
catch(UnresolvedAddressException uae)
{
//Do nothing - the server is temporarily unavailable
}
catch(Exception e)
{
Throwable throwableCause = e.getCause();
if(throwableCause instanceof ConnectException)
{
//Do nothing, the server is unavailable
}
else if(throwableCause instanceof UnresolvedAddressException)
{
//Do nothing - the server is temporarily unavailable
}
else
{
if(!mStackTraceLoggingSuppressed)
{
mLog.error("Error sending metadata update. Future errors will " +
"be suppressed", e);
mStackTraceLoggingSuppressed = true;
}
}
}
}
}, 0l, TimeUnit.SECONDS);
}
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
mUpdating.set(false);
}
}
/**
* Creates the song information for a metadata update
*/
private static String getSong(Metadata metadata)
{
StringBuilder sb = new StringBuilder();
if(metadata != null)
{
String to = metadata.getPrimaryAddressTo().getIdentifier();
sb.append("TO:");
if(metadata.getPrimaryAddressTo().hasAlias())
{
sb.append(metadata.getPrimaryAddressTo().getAlias().getName());
}
else if(metadata.getPrimaryAddressTo().hasIdentifier())
{
sb.append(metadata.getPrimaryAddressTo().getIdentifier());
}
else
{
sb.append("UNKNOWN");
}
sb.append(" FROM:");
if(metadata.getPrimaryAddressFrom().hasAlias())
{
sb.append(metadata.getPrimaryAddressFrom().getAlias().getName());
}
else if(metadata.getPrimaryAddressFrom().hasIdentifier())
{
sb.append(metadata.getPrimaryAddressFrom().getIdentifier());
}
else
{
sb.append("UNKNOWN");
}
}
else
{
sb.append("Scanning ....");
}
return sb.toString();
}
/**
* Creates an HTTP GET request to update the metadata on the remote server
*/
private HttpRequest createUpdateRequest(String song)
{
try
{
StringBuilder sb = new StringBuilder();
sb.append("mode=updinfo");
sb.append("&mount=").append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8));
sb.append("&charset=UTF%2d8");
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
sb.toString(), getHTTPHeaders());
return request;
}
catch(UnsupportedEncodingException e)
{
//This should never happen
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
}
return null;
}
/**
* HTTP headers to use for a metadata update request.
*/
private Map<String,String> getHTTPHeaders()
{
if(mHTTPHeaders == null)
{
mHTTPHeaders = new HashMap<>();
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials());
}
return mHTTPHeaders;
}
}

View File

@@ -0,0 +1,292 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import java.util.Base64;
public abstract class IcecastConfiguration extends BroadcastConfiguration
{
protected String mUserName = "source";
private String mMountPoint = "/stream";
private String mDescription;
private String mGenre;
private boolean mPublic;
private int mBitRate = 16;
private int mChannels = 1;
private int mSampleRate = 8000;
private String mURL;
public IcecastConfiguration(BroadcastFormat format)
{
super(format);
}
public IcecastConfiguration()
{
}
/**
* Base64 encoded version of the username and password with prepended 'Basic ' tag.
*/
@XmlTransient
public String getBase64EncodedCredentials()
{
StringBuilder sb = new StringBuilder();
if(hasUserName())
{
sb.append(getUserName()).append(":").append(getPassword());
}
else
{
sb.append(getPassword());
}
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
return "Basic " + base64;
}
@XmlAttribute( name="user_name" )
public String getUserName()
{
return mUserName;
}
public void setUserName(String userName)
{
mUserName = userName;
}
public boolean hasUserName()
{
return mUserName != null;
}
/**
* Mount point or path to the stream
* @return mount point
*/
@XmlAttribute( name="mount_point" )
public String getMountPoint()
{
return mMountPoint;
}
/**
* Sets the mount point (path) for the stream
* @param mountPoint
*/
public void setMountPoint(String mountPoint)
{
if(mountPoint != null && !mountPoint.isEmpty())
{
if(mountPoint.startsWith("/"))
{
mMountPoint = mountPoint;
}
else
{
mMountPoint = "/" + mountPoint;
}
}
}
public boolean hasMountPoint()
{
return mMountPoint != null;
}
@XmlAttribute( name="description" )
public String getDescription()
{
return mDescription;
}
public void setDescription(String description)
{
mDescription = description;
}
public boolean hasDescription()
{
return mDescription != null;
}
/**
* Stream genre
*/
@XmlAttribute( name="genre" )
public String getGenre()
{
return mGenre;
}
/**
* Stream genre
* @param genre
*/
public void setGenre(String genre)
{
mGenre = genre;
}
public boolean hasGenre()
{
return mGenre != null;
}
/**
* Public visibility of the broadcastAudio
*/
@XmlAttribute( name="public" )
public boolean isPublic()
{
return mPublic;
}
/**
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
mPublic = isPublic;
}
/**
* Number of audio channels in the broadcastAudio
*/
@XmlAttribute( name="channels" )
public int getChannels()
{
return mChannels;
}
/**
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
mChannels = channels;
}
public boolean hasChannels()
{
return mChannels > 0;
}
/**
* Bit rate in bits per second
*/
@XmlAttribute( name="bitrate" )
public int getBitRate()
{
return mBitRate;
}
/**
* Bit rate
* @param bitRate in samples per second
*/
public void setBitRate(int bitRate)
{
mBitRate = bitRate;
}
public boolean hasBitRate()
{
return mBitRate > 0;
}
@XmlAttribute( name="sample_rate" )
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
public boolean hasSampleRate()
{
return mSampleRate > 0;
}
/**
* URL associated with the broadcastAudio where users can find additional details.
*/
@XmlAttribute( name="url" )
public String getURL()
{
return mURL;
}
/**
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)
{
mURL = url;
}
public boolean hasURL()
{
return mURL != null;
}
@Override
public boolean isValid()
{
if(!super.isValid())
{
return false;
}
else if(getUserName() == null)
{
return false;
}
else if(getMountPoint() == null)
{
return false;
}
else if(getChannels() != 1)
{
return false;
}
else if(getSampleRate() <= 0)
{
return false;
}
else if(getBitRate() <= 0)
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,330 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.BroadcastState;
import audio.convert.MP3AudioConverter;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderException;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpException;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.DefaultHttpResponse;
import org.apache.mina.http.api.HttpMethod;
import org.apache.mina.http.api.HttpStatus;
import org.apache.mina.http.api.HttpVersion;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import util.ThreadPool;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private Map<String,String> mHTTPHeaders;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
* Creates an Icecast 2.4.x compatible broadcaster using HTTP 1.1 protocol. This broadcaster is
* compatible with Icecast version 2.4.x and newer versions of the server software.
*
* Note: use @see IcecastTCPAudioBroadcaster for Icecast version 2.3.x and older.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
* IcecastHTTPIOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
*
* @param configuration for the Icecast stream
*/
public IcecastHTTPAudioBroadcaster(IcecastHTTPConfiguration configuration)
{
super(configuration);
}
/**
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
IoBuffer buffer = IoBuffer.allocate(audio.length);
buffer.put(audio);
buffer.flip();
mStreamingSession.write(buffer);
}
}
/**
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
* is currently connected to the remote server following any connection attempts.
*
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
*
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
mLastConnectionAttempt = System.currentTimeMillis();
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
mSocketConnector.setHandler(new IcecastHTTPIOHandler());
}
mStreamingSession = null;
Runnable runnable = new Runnable()
{
@Override
public void run()
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect - no exception is available");
}
disconnect();
}
mConnecting.set(false);
}
};
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
}
return connected();
}
/**
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
if(mStreamingSession != null)
{
mStreamingSession.closeNow();
}
}
/**
* IO Handler for managing Icecast HTTP connection and credentials
*/
public class IcecastHTTPIOHandler extends IoHandlerAdapter
{
@Override
public void sessionOpened(IoSession session) throws Exception
{
//Send stream configuration and user credentials upon connecting to remote server
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.PUT,
getConfiguration().getMountPoint(), "", getHTTPHeaders());
session.write(request);
}
private Map<String,String> getHTTPHeaders()
{
if(mHTTPHeaders == null)
{
mHTTPHeaders = new HashMap<>();
mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
mHTTPHeaders.put(IcecastHeader.PUBLIC.getValue(), getConfiguration().isPublic() ? "1" : "0");
StringBuilder sb = new StringBuilder();
sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
sb.append("channels=").append(getConfiguration().getChannels());
mHTTPHeaders.put(IcecastHeader.AUDIO_INFO.getValue(), sb.toString());
if(getConfiguration().hasName())
{
mHTTPHeaders.put(IcecastHeader.NAME.getValue(), getConfiguration().getName());
}
if(getConfiguration().hasDescription())
{
mHTTPHeaders.put(IcecastHeader.DESCRIPTION.getValue(), getConfiguration().getDescription());
}
if(getConfiguration().hasURL())
{
mHTTPHeaders.put(IcecastHeader.URL.getValue(), getConfiguration().getURL());
}
if(getConfiguration().hasGenre())
{
mHTTPHeaders.put(IcecastHeader.GENRE.getValue(), getConfiguration().getGenre());
}
if(getConfiguration().hasBitRate())
{
mHTTPHeaders.put(IcecastHeader.BITRATE.getValue(), String.valueOf(getConfiguration().getBitRate()));
}
}
return mHTTPHeaders;
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mSocketConnector.dispose();
mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false);
super.sessionClosed(session);
}
@Override
public void exceptionCaught(IoSession session, Throwable throwable) throws Exception
{
if(throwable instanceof ProtocolDecoderException)
{
Throwable cause = ((ProtocolDecoderException) throwable).getCause();
if(cause instanceof HttpException &&
((HttpException) cause).getStatusCode() == HttpStatus.CLIENT_ERROR_LENGTH_REQUIRED.code())
{
//Ignore - Icecast 2.4 sometimes doesn't send any headers with their HTTP responses. Mina expects a
//content-length for HTTP responses.
}
else
{
mLog.error("HTTP protocol decoder error", throwable);
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
disconnect();
}
}
else
{
mLog.error("Broadcast error", throwable);
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
disconnect();
}
mConnecting.set(false);
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof DefaultHttpResponse)
{
DefaultHttpResponse response = (DefaultHttpResponse) object;
switch(response.getStatus())
{
case INFORMATIONAL_CONTINUE:
break;
case SUCCESS_OK:
setBroadcastState(BroadcastState.CONNECTED);
mConnecting.set(false);
break;
case CLIENT_ERROR_UNAUTHORIZED:
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
disconnect();
break;
case CLIENT_ERROR_FORBIDDEN:
setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
disconnect();
break;
default:
setBroadcastState(BroadcastState.ERROR);
disconnect();
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
break;
}
}
else
{
mLog.error("Icecast HTTP broadcaster - unrecognized message [ " + object.getClass() +
"] received:" + object.toString());
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import java.util.Base64;
public class IcecastHTTPConfiguration extends IcecastConfiguration
{
public IcecastHTTPConfiguration()
{
//no-arg JAXB constructor
this(BroadcastFormat.MP3);
}
/**
* Icecast 2.4.x+ compatible configuration
*/
public IcecastHTTPConfiguration(BroadcastFormat format)
{
super(format);
setPassword("change me!");
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setBitRate(getBitRate());
copy.setChannels(getChannels());
copy.setSampleRate(getSampleRate());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.ICECAST_HTTP;
}
}

View File

@@ -0,0 +1,378 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import icon.IconManager;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class IcecastHTTPConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPConfigurationEditor.class );
private static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mMountPoint;
private JTextField mUsername;
private JTextField mPassword;
private JTextField mDescription;
private JTextField mGenre;
private JCheckBox mPublic;
private JSlider mDelay;
private JLabel mDelayValue;
private JSlider mMaximumRecordingAge;
private JLabel mMaximumRecordingAgeValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public IcecastHTTPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
{
super(broadcastModel, aliasModel, iconManager);
init();
}
private void init()
{
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
"[][][][][][][][][][][][][][][][][grow,fill]" ) );
setPreferredSize(new Dimension(150,400));
JLabel channelLabel = new JLabel("Icecast 2 (v2.4+) Stream");
ImageIcon icon = mIconManager.getScaledIcon(new ImageIcon(BroadcastServerType.ICECAST_HTTP.getIconPath()), 25);
channelLabel.setIcon(icon);
add(channelLabel, "span, align center");
add(new JLabel("Name:"));
mName = new JTextField();
mName.getDocument().addDocumentListener(this);
mName.setToolTipText("Unique name for this stream");
add(mName);
add(new JLabel("Server:"));
mServer = new JTextField();
mServer.getDocument().addDocumentListener(this);
mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
add(mServer);
add(new JLabel("Port:"));
mPort = new JTextField();
mPort.getDocument().addDocumentListener(this);
mPort.setToolTipText("Server port number (e.g. 80)");
add(mPort);
add(new JLabel("Mount:"));
mMountPoint = new JTextField();
mMountPoint.getDocument().addDocumentListener(this);
mMountPoint.setToolTipText("Mount point (e.g. /12345)");
add(mMountPoint);
add(new JLabel("User Name:"));
mUsername = new JTextField();
mUsername.getDocument().addDocumentListener(this);
mUsername.setToolTipText("User name for the stream. Default: source");
add(mUsername);
add(new JLabel("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password for the stream");
add(mPassword);
add(new JLabel("Description:"));
mDescription = new JTextField();
mDescription.getDocument().addDocumentListener(this);
mDescription.setToolTipText("Description of the stream contents");
add(mDescription);
add(new JLabel("Genre:"));
mGenre = new JTextField();
mGenre.getDocument().addDocumentListener(this);
mGenre.setToolTipText("Genre (e.g. public safety)");
add(mGenre);
add(new JLabel("Public:"));
mPublic = new JCheckBox();
mPublic.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
mPublic.setToolTipText("Designate stream as public (checked) or private");
add(mPublic);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
add(new JLabel());
mMaximumRecordingAgeValue = new JLabel("1 Minute");
add(mMaximumRecordingAgeValue);
add(new JLabel("Age Limit:"));
mMaximumRecordingAge = new JSlider(1,60,5);
mMaximumRecordingAge.setMajorTickSpacing(10);
mMaximumRecordingAge.setMinorTickSpacing(5);
mMaximumRecordingAge.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
int value = mMaximumRecordingAge.getValue();
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
setModified(true);
}
});
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
add(mMaximumRecordingAge);
add(new JLabel("Enabled:"));
mEnabled = new JCheckBox();
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled);
mSaveButton = new JButton("Save");
mSaveButton.setEnabled(false);
mSaveButton.setToolTipText("Click to save configuration information");
mSaveButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
save();
}
});
add(mSaveButton);
mResetButton = new JButton("Reset");
mResetButton.setEnabled(false);
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
mResetButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(false);
setItem(getItem());
}
});
add(mResetButton);
}
@Override
public void setModified(boolean modified)
{
super.setModified(modified);
mSaveButton.setEnabled(modified);
mResetButton.setEnabled(modified);
}
@Override
public void setItem(BroadcastConfiguration item)
{
super.setItem(item);
if(hasItem())
{
IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mMountPoint.setText(config.getMountPoint());
mUsername.setText(config.getUserName());
mPassword.setText(config.getPassword());
mDescription.setText(config.getDescription());
mGenre.setText(config.getGenre());
mPublic.setSelected(config.isPublic());
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mMountPoint.setText(null);
mUsername.setText(null);
mPassword.setText(null);
mDescription.setText(null);
mGenre.setText(null);
mPublic.setSelected(true);
mDelay.setValue(0);
mMaximumRecordingAge.setValue(1);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
config.setUserName(mUsername.getText());
config.setPassword(mPassword.getText());
config.setDescription(mDescription.getText());
config.setGenre(mGenre.getText());
config.setPublic(mPublic.isSelected());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
config.setEnabled(mEnabled.isSelected());
setModified(false);
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
}
}
/**
* Validates the user specified values in the controls prior to saving them to the configuration
*/
private boolean validateConfiguration()
{
String name = mName.getText();
//A unique stream name is required
if(!mBroadcastModel.isUniqueName(name, getItem()))
{
String message = (name == null || name.isEmpty()) ?
"Please specify a stream name." : "Stream name " + name +
"is already in use.\nPlease choose another name.";
JOptionPane.showMessageDialog(IcecastHTTPConfigurationEditor.this,
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
mName.requestFocus();
return false;
}
//Server address is optional, but required
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
{
return true;
}
//Port is optional, but required
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
{
return true;
}
//Mount Point is required
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
{
return false;
}
//User name is required
if(!validateTextField(mUsername, "Invalid User Name", "Please specify a user name."))
{
return false;
}
//Password is required
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
{
return false;
}
return true;
}
/**
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
*/
private int getPort()
{
String port = mPort.getText();
if(port != null && !port.isEmpty())
{
try
{
return Integer.parseInt(port);
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
}
return 0;
}
}

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
public enum IcecastHeader
{
ACCEPT("Accept"),
AUDIO_INFO("ice-audio-info"),
AUTHORIZATION("Authorization"),
BITRATE("ice-bitrate"),
CONTENT_TYPE("Content-Type"),
DESCRIPTION("ice-description"),
EXPECT("Expect"),
GENRE("ice-genre"),
HOST("Host"),
NAME("ice-name"),
PUBLIC("ice-public"),
URL("ice-url"),
USER_AGENT("User-Agent"),
UNKNOWN("unknown");
private String mValue;
private IcecastHeader(String value)
{
mValue = value;
}
public String getValue()
{
return mValue;
}
}

View File

@@ -0,0 +1,355 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.BroadcastState;
import audio.broadcast.icecast.codec.IcecastCodecFactory;
import audio.convert.MP3AudioConverter;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.WriteTimeoutException;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import util.ThreadPool;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.channels.UnresolvedAddressException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class);
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
* compatible with Icecast version 2.3.2 and older versions of the server software.
*
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
* ShoutcastV2IOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
*
* @param configuration for the Icecast stream
*/
public IcecastTCPAudioBroadcaster(IcecastTCPConfiguration configuration)
{
super(configuration);
}
/**
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
mStreamingSession.write(audio);
}
}
/**
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
* is currently connected to the remote server following any connection attempts.
*
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
*
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
mLastConnectionAttempt = System.currentTimeMillis();
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new IcecastCodecFactory()));
mSocketConnector.setHandler(new IcecastTCPIOHandler());
}
mStreamingSession = null;
Runnable runnable = new Runnable()
{
@Override
public void run()
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
mStreamingSession = future.getSession();
}
catch(UnresolvedAddressException uae)
{
//What do you do with this? Nothing.
}
catch(InterruptedException ie)
{
//We couldn't connect within the timeout period -- disconnect so that we can try again later
disconnect();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("[" + getStreamName() + "] failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
}
disconnect();
}
catch(Throwable t)
{
disconnect();
}
mConnecting.set(false);
}
};
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
}
return connected();
}
/**
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
if(connected() && mStreamingSession != null)
{
mStreamingSession.closeNow();
}
else
{
mLastConnectionAttempt = System.currentTimeMillis();
}
}
/**
* IO Handler for managing Icecast TCP connection and credentials
*/
public class IcecastTCPIOHandler extends IoHandlerAdapter
{
/**
* Sends stream configuration and user credentials upon connecting to remote server
*/
@Override
public void sessionOpened(IoSession session) throws Exception
{
StringBuilder sb = new StringBuilder();
sb.append("SOURCE ").append(getConfiguration().getMountPoint());
sb.append(" HTTP/1.0").append(TERMINATOR);
sb.append("Authorization: ").append(getConfiguration().getBase64EncodedCredentials()).append(TERMINATOR);
sb.append(IcecastHeader.USER_AGENT.getValue()).append(SEPARATOR)
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
sb.append(IcecastHeader.AUDIO_INFO.getValue()).append(SEPARATOR);
sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
sb.append("channels=").append(getConfiguration().getChannels()).append(TERMINATOR);
if(getConfiguration().hasName())
{
sb.append(IcecastHeader.NAME.getValue()).append(SEPARATOR).append(getConfiguration().getName()).append(TERMINATOR);
}
if(getConfiguration().hasGenre())
{
sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR)
.append(getConfiguration().getGenre()).append(TERMINATOR);
}
if(getConfiguration().hasDescription())
{
sb.append(IcecastHeader.DESCRIPTION.getValue()).append(SEPARATOR)
.append(getConfiguration().getDescription()).append(TERMINATOR);
}
sb.append(TERMINATOR).append(TERMINATOR);
session.write(sb.toString());
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mStreamingSession = null;
mConnecting.set(false);
super.sessionClosed(session);
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if(cause instanceof IOException)
{
IOException ioe = (IOException)cause;
if(ioe.getMessage() != null)
{
String reason = ioe.getMessage();
if(connected())
{
disconnect();
}
else if(reason.startsWith("Connection reset"))
{
disconnect();
}
else if(reason.startsWith("Operation timed out"))
{
disconnect();
}
else
{
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
}
else if(ioe instanceof WriteTimeoutException)
{
disconnect();
}
else
{
setBroadcastState(BroadcastState.ERROR);
disconnect();
mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
}
}
else
{
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof String)
{
String message = (String) object;
if(message != null && !message.trim().isEmpty())
{
if(message.startsWith("HTTP/1.0 200 OK"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if(message.startsWith("HTTP/1.0 403 Mountpoint in use"))
{
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
}
else if(message.contains("Content-Type: text/html"))
{
//Disregard ... this normally follows the mount point in use error
}
else if(message.contains("Invalid Password") ||
message.contains("Authentication Required"))
{
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
}
else if(message.contains("HTTP/1.1 501"))
{
disconnect(); //So that we can reconnect later
}
else
{
mLog.error("Unrecognized server response:" + message);
setBroadcastState(BroadcastState.ERROR);
}
}
}
else
{
mLog.error("[" + getStreamName() + "]Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
"] received:" + object.toString());
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.annotation.XmlSeeAlso;
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
public class IcecastTCPConfiguration extends IcecastConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
public IcecastTCPConfiguration()
{
this(BroadcastFormat.MP3);
}
/**
* Icecast 2.3.x and 2.4.x compatible configuration
* @param format of audio
*/
public IcecastTCPConfiguration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.ICECAST_TCP;
}
}

View File

@@ -0,0 +1,362 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import icon.IconManager;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class IcecastTCPConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfigurationEditor.class );
private static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mMountPoint;
private JTextField mPassword;
private JTextField mDescription;
private JTextField mGenre;
private JCheckBox mPublic;
private JSlider mDelay;
private JLabel mDelayValue;
private JSlider mMaximumRecordingAge;
private JLabel mMaximumRecordingAgeValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public IcecastTCPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
{
super(broadcastModel, aliasModel, iconManager);
init();
}
private void init()
{
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
"[][][][][][][][][][][][][][][][grow,fill]" ) );
setPreferredSize(new Dimension(150,400));
JLabel channelLabel = new JLabel("Icecast (v2.3) Stream");
ImageIcon icon = mIconManager.getScaledIcon(new ImageIcon(BroadcastServerType.ICECAST_TCP.getIconPath()), 25);
channelLabel.setIcon(icon);
add(channelLabel, "span, align center");
add(new JLabel("Name:"));
mName = new JTextField();
mName.getDocument().addDocumentListener(this);
mName.setToolTipText("Unique name for this stream");
add(mName);
add(new JLabel("Server:"));
mServer = new JTextField();
mServer.getDocument().addDocumentListener(this);
mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
add(mServer);
add(new JLabel("Port:"));
mPort = new JTextField();
mPort.getDocument().addDocumentListener(this);
mPort.setToolTipText("Server port number (e.g. 80)");
add(mPort);
add(new JLabel("Mount:"));
mMountPoint = new JTextField();
mMountPoint.getDocument().addDocumentListener(this);
mMountPoint.setToolTipText("Mount point (e.g. /12345)");
add(mMountPoint);
add(new JLabel("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password for the stream");
add(mPassword);
add(new JLabel("Description:"));
mDescription = new JTextField();
mDescription.getDocument().addDocumentListener(this);
mDescription.setToolTipText("Description of the stream contents");
add(mDescription);
add(new JLabel("Genre:"));
mGenre = new JTextField();
mGenre.getDocument().addDocumentListener(this);
mGenre.setToolTipText("Genre (e.g. public safety)");
add(mGenre);
add(new JLabel("Public:"));
mPublic = new JCheckBox();
mPublic.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
mPublic.setToolTipText("Designate stream as public (checked) or private");
add(mPublic);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
add(new JLabel());
mMaximumRecordingAgeValue = new JLabel("1 Minute");
add(mMaximumRecordingAgeValue);
add(new JLabel("Age Limit:"));
mMaximumRecordingAge = new JSlider(1,60,5);
mMaximumRecordingAge.setMajorTickSpacing(10);
mMaximumRecordingAge.setMinorTickSpacing(5);
mMaximumRecordingAge.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
int value = mMaximumRecordingAge.getValue();
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
setModified(true);
}
});
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
add(mMaximumRecordingAge);
add(new JLabel("Enabled:")); //Empty
mEnabled = new JCheckBox();
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled);
mSaveButton = new JButton("Save");
mSaveButton.setEnabled(false);
mSaveButton.setToolTipText("Click to save configuration information");
mSaveButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
save();
}
});
add(mSaveButton);
mResetButton = new JButton("Reset");
mResetButton.setEnabled(false);
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
mResetButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(false);
setItem(getItem());
}
});
add(mResetButton);
}
@Override
public void setModified(boolean modified)
{
super.setModified(modified);
mSaveButton.setEnabled(modified);
mResetButton.setEnabled(modified);
}
@Override
public void setItem(BroadcastConfiguration item)
{
super.setItem(item);
if(hasItem())
{
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mMountPoint.setText(config.getMountPoint());
mPassword.setText(config.getPassword());
mDescription.setText(config.getDescription());
mGenre.setText(config.getGenre());
mPublic.setSelected(config.isPublic());
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mMountPoint.setText(null);
mPassword.setText(null);
mDescription.setText(null);
mGenre.setText(null);
mPublic.setSelected(true);
mDelay.setValue(0);
mMaximumRecordingAge.setValue(1);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
config.setPassword(mPassword.getText());
config.setDescription(mDescription.getText());
config.setGenre(mGenre.getText());
config.setPublic(mPublic.isSelected());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
config.setEnabled(mEnabled.isSelected());
setModified(false);
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
}
}
/**
* Validates the user specified values in the controls prior to saving them to the configuration
*/
private boolean validateConfiguration()
{
String name = mName.getText();
//A unique stream name is required
if(!mBroadcastModel.isUniqueName(name, getItem()))
{
String message = (name == null || name.isEmpty()) ?
"Please specify a stream name." : "Stream name " + name +
"is already in use.\nPlease choose another name.";
JOptionPane.showMessageDialog(IcecastTCPConfigurationEditor.this,
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
mName.requestFocus();
return false;
}
//Server address is optional, but required
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
{
return true;
}
//Port is optional, but required
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
{
return true;
}
//Mount Point is optional, but required
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
{
return true;
}
//Password is optional, but required
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
{
return true;
}
return true;
}
/**
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
*/
private int getPort()
{
String port = mPort.getText();
if(port != null && !port.isEmpty())
{
try
{
return Integer.parseInt(port);
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
}
return 0;
}
}

View File

@@ -0,0 +1,63 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast.codec;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineDecoder;
import java.nio.charset.StandardCharsets;
public class IcecastCodecFactory implements ProtocolCodecFactory
{
private ProtocolEncoder mProtocolEncoder;
private ProtocolDecoder mProtocolDecoder;
/**
* Factory for codecs to process Icecast string protocol and byte array audio frames for use with Apache Mina
*/
public IcecastCodecFactory()
{
}
@Override
public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
{
if(mProtocolEncoder == null)
{
mProtocolEncoder = new IcecastEncoder();
}
return mProtocolEncoder;
}
@Override
public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
{
if(mProtocolDecoder == null)
{
mProtocolDecoder = new TextLineDecoder(StandardCharsets.UTF_8, LineDelimiter.AUTO);
}
return mProtocolDecoder;
}
}

View File

@@ -0,0 +1,94 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.icecast.codec;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineEncoder;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
public class IcecastEncoder extends ProtocolEncoderAdapter
{
private static final AttributeKey ENCODER = new AttributeKey(TextLineEncoder.class, "encoder");
private static final Charset mCharset = StandardCharsets.UTF_8;
private static final int MAX_LINE_LENGTH = 2147483647;
/**
* Icecast Protocol Encoder is an Apache Mina compliant protocol encoder that supports Icecast string-based
* connection negotiation as well as byte array audio frame data.
*/
public IcecastEncoder()
{
}
@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception
{
//Audio frame data
if(message instanceof byte[])
{
byte[] data = (byte[])message;
IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
buf.put(data);
buf.flip();
out.write(buf);
}
//Icecast connection messages
else if(message instanceof String)
{
CharsetEncoder encoder = (CharsetEncoder)session.getAttribute(ENCODER);
if(encoder == null)
{
encoder = mCharset.newEncoder();
session.setAttribute(ENCODER, encoder);
}
String value = (String)message;
IoBuffer buf = IoBuffer.allocate(value.length()).setAutoExpand(true);
buf.putString(value, encoder);
if(buf.position() > MAX_LINE_LENGTH)
{
throw new IllegalArgumentException("Line length: " + buf.position());
}
else
{
buf.putString(LineDelimiter.UNIX.getValue(), encoder);
buf.flip();
out.write(buf);
}
}
else
{
throw new IllegalArgumentException("Can't encode - unrecognized object type: " + message.getClass());
}
}
}