mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#30 WIP extracted the IcecastMetadataUpdater and merged some of the icecast configuration method into a new superclass.
This commit is contained in:
292
src/audio/broadcast/icecast/IcecastConfiguration.java
Normal file
292
src/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;
|
||||
}
|
||||
}
|
||||
@@ -24,18 +24,8 @@ import audio.broadcast.BroadcastServerType;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
||||
public class IcecastHTTPConfiguration extends IcecastConfiguration
|
||||
{
|
||||
private String mUserName = "source";
|
||||
private String mMountPoint = "stream";
|
||||
private String mDescription;
|
||||
private String mGenre;
|
||||
private boolean mPublic;
|
||||
private int mBitRate = 16; //kHz
|
||||
private int mChannels = 1;
|
||||
private int mSampleRate = 8000;
|
||||
private String mURL;
|
||||
|
||||
public IcecastHTTPConfiguration()
|
||||
{
|
||||
//no-arg JAXB constructor
|
||||
@@ -84,234 +74,5 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
||||
return BroadcastServerType.ICECAST_HTTP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encoded version of the username and password with prepended 'Basic ' tag.
|
||||
*/
|
||||
public String getEncodedCredentials()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* User name. Default is 'source'.
|
||||
*/
|
||||
public String getUserName()
|
||||
{
|
||||
return mUserName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user name.
|
||||
*/
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
mUserName = userName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration has a user name
|
||||
*/
|
||||
public boolean hasUserName()
|
||||
{
|
||||
return mUserName != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount point or path to the stream
|
||||
* @return mount point
|
||||
*/
|
||||
public String getMountPoint()
|
||||
{
|
||||
return mMountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mount point (path) for the stream
|
||||
* @param mountPoint
|
||||
*/
|
||||
public void setMountPoint(String mountPoint)
|
||||
{
|
||||
mMountPoint = mountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration has a mount point
|
||||
*/
|
||||
public boolean hasMountPoint()
|
||||
{
|
||||
return mMountPoint != null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stream description
|
||||
*/
|
||||
public String getDescription()
|
||||
{
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream description
|
||||
*/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
mDescription = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration contains a stream description
|
||||
*/
|
||||
public boolean hasDescription()
|
||||
{
|
||||
return mDescription != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream genre
|
||||
*/
|
||||
public String getGenre()
|
||||
{
|
||||
return mGenre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream genre
|
||||
*/
|
||||
public void setGenre(String genre)
|
||||
{
|
||||
mGenre = genre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration contains a genre
|
||||
*/
|
||||
public boolean hasGenre()
|
||||
{
|
||||
return mGenre != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public visibility of the broadcastAudio
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public int getBitRate()
|
||||
{
|
||||
return mBitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate
|
||||
* @param bitRate in samples per second
|
||||
*/
|
||||
public void setBitRate(int bitRate)
|
||||
{
|
||||
mBitRate = bitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration has a bit rate value
|
||||
*/
|
||||
public boolean hasBitRate()
|
||||
{
|
||||
return mBitRate > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample rate of the input PCM audio samples. Default is 8 kHz
|
||||
*/
|
||||
public int getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input PCM audio sample rate
|
||||
*/
|
||||
public void setSampleRate(int sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration contains an audio sample rate
|
||||
*/
|
||||
public boolean hasSampleRate()
|
||||
{
|
||||
return mSampleRate > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL associated with the broadcastAudio where users can find additional details.
|
||||
*/
|
||||
public String getURL()
|
||||
{
|
||||
return mURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL associated with the broadcastAudio where users can find additional details.
|
||||
*/
|
||||
public void setURL(String url)
|
||||
{
|
||||
mURL = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration contains a stream URL
|
||||
*/
|
||||
public boolean hasURL()
|
||||
{
|
||||
return mURL != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public class IcecastHTTPConfigurationEditor extends BroadcastConfigurationEditor
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. \\12345)");
|
||||
mMountPoint.setToolTipText("Mount point (e.g. /12345)");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("User Name:"));
|
||||
|
||||
209
src/audio/broadcast/icecast/IcecastMetadataUpdater.java
Normal file
209
src/audio/broadcast/icecast/IcecastMetadataUpdater.java
Normal file
@@ -0,0 +1,209 @@
|
||||
/*******************************************************************************
|
||||
* 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 controller.ThreadPoolManager;
|
||||
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 java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
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 IcecastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadataUpdater.class);
|
||||
private final static String UTF8 = "UTF-8";
|
||||
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
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 IcecastMetadataUpdater(ThreadPoolManager threadPoolManager, IcecastConfiguration icecastConfiguration)
|
||||
{
|
||||
mThreadPoolManager = threadPoolManager;
|
||||
mIcecastConfiguration = icecastConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the network socket and HTTP headers map
|
||||
*/
|
||||
private void init()
|
||||
{
|
||||
if (mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastMetadataUpdater.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
|
||||
{
|
||||
// mLog.debug("Session " + session.getId() + " metadata update complete - ERROR received");
|
||||
session.closeNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object message) throws Exception
|
||||
{
|
||||
// mLog.debug("Session " + session.getId() + " metadata update complete - response received");
|
||||
session.closeNow();
|
||||
}
|
||||
});
|
||||
|
||||
mHTTPHeaders = new HashMap<>();
|
||||
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(String metadata)
|
||||
{
|
||||
if (metadata != null)
|
||||
{
|
||||
mMetadataQueue.offer(metadata);
|
||||
|
||||
if (mUpdating.compareAndSet(false, true))
|
||||
{
|
||||
String metadataUpdate = mMetadataQueue.poll();
|
||||
|
||||
while (metadataUpdate != null)
|
||||
{
|
||||
//Ensure we're setup for network communications (only happens once)
|
||||
init();
|
||||
|
||||
HttpRequest updateRequest = createUpdateRequest(metadataUpdate);
|
||||
|
||||
if (updateRequest != null)
|
||||
{
|
||||
mThreadPoolManager.scheduleOnce(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectFuture connectFuture = mSocketConnector
|
||||
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
||||
mIcecastConfiguration.getPort()));
|
||||
connectFuture.awaitUninterruptibly();
|
||||
IoSession session = connectFuture.getSession();
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
session.write(updateRequest);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if (throwableCause instanceof ConnectException)
|
||||
{
|
||||
//Do nothing, the server is 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
|
||||
metadataUpdate = mMetadataQueue.poll();
|
||||
}
|
||||
|
||||
mUpdating.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP GET request to update the metadata on the remote server
|
||||
*/
|
||||
private HttpRequest createUpdateRequest(String metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
String songEncoded = URLEncoder.encode(metadata, UTF8);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("mode=updinfo");
|
||||
sb.append("&mount=").append(mIcecastConfiguration.getMountPoint());
|
||||
sb.append("&charset=UTF%2d8");
|
||||
sb.append("&song=").append(songEncoded);
|
||||
|
||||
return new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
|
||||
sb.toString(), mHTTPHeaders);
|
||||
}
|
||||
catch (UnsupportedEncodingException e)
|
||||
{
|
||||
//This should never happen
|
||||
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,6 @@ import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
|
||||
import org.apache.mina.filter.logging.LoggingFilter;
|
||||
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.asynchttpclient.DefaultAsyncHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
@@ -43,22 +38,15 @@ import org.slf4j.LoggerFactory;
|
||||
import properties.SystemProperties;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
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 IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPAudioBroadcaster.class );
|
||||
private final static String UTF8 = "UTF-8";
|
||||
private final static String TERMINATOR = "\r\n";
|
||||
private final static String SEPARATOR = ":";
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
@@ -171,7 +159,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
|
||||
if(mIcecastMetadataUpdater == null)
|
||||
{
|
||||
mIcecastMetadataUpdater = new IcecastMetadataUpdater();
|
||||
mIcecastMetadataUpdater = new IcecastMetadataUpdater(getThreadPoolManager(), getConfiguration());
|
||||
}
|
||||
|
||||
mIcecastMetadataUpdater.update(sb.toString());
|
||||
@@ -358,7 +346,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
sb.append("SOURCE ").append(getConfiguration().getMountPoint());
|
||||
sb.append(" HTTP/1.0").append(TERMINATOR);
|
||||
|
||||
sb.append("Authorization: ").append(getConfiguration().getEncodedCredentials()).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)
|
||||
@@ -459,162 +447,4 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
super.inputClosed(session);
|
||||
}
|
||||
}
|
||||
|
||||
public class IcecastMetadataUpdater
|
||||
{
|
||||
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 IcecastMetadataUpdater()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the network socket and HTTP headers map
|
||||
*/
|
||||
private void init()
|
||||
{
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastMetadataUpdater.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
|
||||
{
|
||||
// mLog.debug("Session " + session.getId() + " metadata update complete - ERROR received");
|
||||
session.closeNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object message) throws Exception
|
||||
{
|
||||
// mLog.debug("Session " + session.getId() + " metadata update complete - response received");
|
||||
session.closeNow();
|
||||
}
|
||||
});
|
||||
|
||||
mHTTPHeaders = new HashMap<>();
|
||||
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getEncodedCredentials());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(String metadata)
|
||||
{
|
||||
if(metadata != null)
|
||||
{
|
||||
mMetadataQueue.offer(metadata);
|
||||
|
||||
if(mUpdating.compareAndSet(false, true))
|
||||
{
|
||||
String metadataUpdate = mMetadataQueue.poll();
|
||||
|
||||
while(metadataUpdate != null)
|
||||
{
|
||||
//Ensure we're setup for network communications (only happens once)
|
||||
init();
|
||||
|
||||
HttpRequest updateRequest = createUpdateRequest(metadataUpdate);
|
||||
|
||||
if(updateRequest != null)
|
||||
{
|
||||
getThreadPoolManager().scheduleOnce(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectFuture connectFuture = mSocketConnector
|
||||
.connect(new InetSocketAddress(getConfiguration().getHost(),
|
||||
getConfiguration().getPort()));
|
||||
connectFuture.awaitUninterruptibly();
|
||||
IoSession session = connectFuture.getSession();
|
||||
|
||||
if(session != null)
|
||||
{
|
||||
session.write(updateRequest);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
//Do nothing, the server is 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
|
||||
metadataUpdate = mMetadataQueue.poll();
|
||||
}
|
||||
|
||||
mUpdating.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP GET request to update the metadata on the remote server
|
||||
*/
|
||||
private HttpRequest createUpdateRequest(String metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
String songEncoded = URLEncoder.encode(metadata, UTF8);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("mode=updinfo");
|
||||
sb.append("&mount=").append(getConfiguration().getMountPoint());
|
||||
sb.append("&charset=UTF%2d8");
|
||||
sb.append("&song=").append(songEncoded);
|
||||
|
||||
return new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
|
||||
sb.toString(), mHTTPHeaders);
|
||||
}
|
||||
catch(UnsupportedEncodingException e)
|
||||
{
|
||||
//This should never happen
|
||||
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,40 +25,25 @@ import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.util.Base64;
|
||||
|
||||
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
|
||||
public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
public class IcecastTCPConfiguration extends IcecastConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
|
||||
|
||||
private String mUserName;
|
||||
private String mMountPoint;
|
||||
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 IcecastTCPConfiguration()
|
||||
{
|
||||
setUserName("source");
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast 2.3.x and 2.4.x compatible configuration
|
||||
* @param format
|
||||
* @param format of audio
|
||||
*/
|
||||
public IcecastTCPConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
setUserName("source");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,249 +75,4 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
return BroadcastServerType.ICECAST_TCP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encoded version of the username and password with prepended 'Basic ' tag.
|
||||
*/
|
||||
@XmlTransient
|
||||
public String getEncodedCredentials()
|
||||
{
|
||||
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(mUserName == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(mMountPoint == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(mChannels != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(mSampleRate <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(mBitRate <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public class IcecastTCPConfigurationEditor extends BroadcastConfigurationEditor
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. \\12345)");
|
||||
mMountPoint.setToolTipText("Mount point (e.g. /12345)");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
|
||||
Reference in New Issue
Block a user