mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 00:57:31 -05:00
fix src folder structure
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
292
src/main/java/audio/broadcast/icecast/IcecastConfiguration.java
Normal file
292
src/main/java/audio/broadcast/icecast/IcecastConfiguration.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
50
src/main/java/audio/broadcast/icecast/IcecastHeader.java
Normal file
50
src/main/java/audio/broadcast/icecast/IcecastHeader.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user