=min-latency}
*
*/
- private static Pattern s_pattern_sdp_a = Pattern.compile("^([a-z]+):(.*)$");
+ protected static Pattern s_pattern_sdp_a = Pattern.compile("^(\\w+):?(.*)$"); //changed to support dash in the attribute name
/**
* SDP {@code a} attribute {@code rtpmap}. Format is
@@ -321,19 +389,37 @@ else if (RaopRtspMethods.GET_PARAMETER.equals(method)) {
*
*/
public synchronized void announceReceived(final ChannelHandlerContext ctx, final HttpRequest req)
- throws Exception
- {
+ throws Exception {
+
/* ANNOUNCE must contain stream information in SDP format */
- if (!req.containsHeader("Content-Type"))
+ if ( ! req.containsHeader("Content-Type")){
throw new ProtocolException("No Content-Type header");
- if (!"application/sdp".equals(req.getHeader("Content-Type")))
+ }
+ if ( ! "application/sdp".equals(req.getHeader("Content-Type")) ){
throw new ProtocolException("Invalid Content-Type header, expected application/sdp but got " + req.getHeader("Content-Type"));
-
+ }
+
reset();
/* Get SDP stream information */
- final String dsp = req.getContent().toString(Charset.forName("ASCII")).replace("\r", "");
+ final String sdp = req.getContent().toString(Charset.forName("ASCII")).replace("\r", "");
+ /**
+ * Sample sdp content:
+ *
+ v=0
+ o=iTunes 3413821438 0 IN IP4 fe80::217:f2ff:fe0f:e0f6
+ s=iTunes
+ c=IN IP4 fe80::5a55:caff:fe1a:e187
+ t=0 0
+ m=audio 0 RTP/AVP 96
+ a=rtpmap:96 AppleLossless
+ a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100
+ a=fpaeskey:RlBMWQECAQAAAAA8AAAAAPFOnNe+zWb5/n4L5KZkE2AAAAAQlDx69reTdwHF9LaNmhiRURTAbcL4brYAceAkZ49YirXm62N4
+ a=aesiv:5b+YZi9Ikb845BmNhaVo+Q
+ */
+
+ //TODO: move this parsing into a SDP class.
SecretKey aesKey = null;
IvParameterSpec aesIv = null;
int alacFormatIndex = -1;
@@ -341,13 +427,17 @@ public synchronized void announceReceived(final ChannelHandlerContext ctx, final
int descriptionFormatIndex = -1;
String[] formatOptions = null;
- for(final String line: dsp.split("\n")) {
+ //go through each line and parse the sdp parameters
+ for(final String line: sdp.split("\n")) {
/* Split SDP line into attribute and setting */
- final Matcher line_matcher = s_pattern_sdp_line.matcher(line);
- if (!line_matcher.matches())
+ final Matcher lineMatcher = s_pattern_sdp_line.matcher(line);
+
+ if ( ! lineMatcher.matches()){
throw new ProtocolException("Cannot parse SDP line " + line);
- final char attribute = line_matcher.group(1).charAt(0);
- final String setting = line_matcher.group(2);
+ }
+
+ final char attribute = lineMatcher.group(1).charAt(0);
+ final String setting = lineMatcher.group(2);
/* Handle attributes */
switch (attribute) {
@@ -360,10 +450,15 @@ public synchronized void announceReceived(final ChannelHandlerContext ctx, final
break;
case 'a':
+ LOG.info("setting: " + setting);
+
/* Attribute a. Defines various session properties */
final Matcher a_matcher = s_pattern_sdp_a.matcher(setting);
- if (!a_matcher.matches())
+
+ if ( ! a_matcher.matches() ){
throw new ProtocolException("Cannot parse SDP " + attribute + "'s setting " + setting);
+ }
+
final String key = a_matcher.group(1);
final String value = a_matcher.group(2);
@@ -392,8 +487,8 @@ else if ("rsaaeskey".equals(key)) {
*/
byte[] aesKeyRaw;
- m_rsaPkCS1OaepCipher.init(Cipher.DECRYPT_MODE, AirTunesCrytography.PrivateKey);
- aesKeyRaw = m_rsaPkCS1OaepCipher.doFinal(Base64.decodeUnpadded(value));
+ rsaPkCS1OaepCipher.init(Cipher.DECRYPT_MODE, AirTunesCryptography.PrivateKey);
+ aesKeyRaw = rsaPkCS1OaepCipher.doFinal(Base64.decodeUnpadded(value));
aesKey = new SecretKeySpec(aesKeyRaw, "AES");
}
@@ -412,35 +507,40 @@ else if ("aesiv".equals(key)) {
/* Validate SDP information */
/* The format index of the stream must match the format index from the rtpmap attribute */
- if (alacFormatIndex != audioFormatIndex)
+ if (alacFormatIndex != audioFormatIndex){
throw new ProtocolException("Audio format " + audioFormatIndex + " not supported");
+ }
/* The format index from the rtpmap attribute must match the format index from the fmtp attribute */
- if (audioFormatIndex != descriptionFormatIndex)
+ if (audioFormatIndex != descriptionFormatIndex){
throw new ProtocolException("Auido format " + audioFormatIndex + " lacks fmtp line");
+ }
/* The fmtp attribute must have contained format options */
- if (formatOptions == null)
+ if (formatOptions == null){
throw new ProtocolException("Auido format " + audioFormatIndex + " incomplete, format options not set");
+ }
/* Create decryption handler if an AES key and IV was specified */
- if ((aesKey != null) && (aesIv != null))
- m_decryptionHandler = new RaopRtpAudioDecryptionHandler(aesKey, aesIv);
+ if ((aesKey != null) && (aesIv != null)){
+ decryptionHandler = new RaopRtpAudioDecryptionHandler(aesKey, aesIv);
+ }
/* Create an ALAC decoder. The ALAC decoder is our stream information provider */
final RaopRtpAudioAlacDecodeHandler handler = new RaopRtpAudioAlacDecodeHandler(formatOptions);
- m_audioStreamInformationProvider = handler;
- m_audioDecodeHandler = handler;
+ audioStreamInformationProvider = handler;
+ audioDecodeHandler = handler;
/* Create audio output queue with the format information provided by the ALAC decoder */
- m_audioOutputQueue = new AudioOutputQueue(m_audioStreamInformationProvider);
+ audioOutputQueue = new AudioOutputQueue(audioStreamInformationProvider);
/* Create timing handle, using the AudioOutputQueue as time source */
- m_timingHandler = new RaopRtpTimingHandler(m_audioOutputQueue);
+ timingHandler = new RaopRtpTimingHandler(audioOutputQueue);
/* Create retransmit request handler using the audio output queue as time source */
- m_resendRequestHandler = new RaopRtpRetransmitRequestHandler(m_audioStreamInformationProvider, m_audioOutputQueue);
+ resendRequestHandler = new RaopRtpRetransmitRequestHandler(audioStreamInformationProvider, audioOutputQueue);
+ //send response back to the client
final HttpResponse response = new DefaultHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
ctx.getChannel().write(response);
}
@@ -456,70 +556,80 @@ else if ("aesiv".equals(key)) {
*
* For RAOP/AirTunes, {@code } is always {@code RTP/AVP/UDP}.
*/
- private static Pattern s_pattern_transportOption = Pattern.compile("^([A-Za-z0-9_-]+)(=(.*))?$");
+ private static Pattern PATTERN_TRANSPORT_OPTION = Pattern.compile("^([A-Za-z0-9_-]+)(=(.*))?$");
/**
* Handles SETUP requests and creates the audio, control and timing RTP channels
*/
- public synchronized void setupReceived(final ChannelHandlerContext ctx, final HttpRequest req)
- throws ProtocolException
- {
+ public synchronized void setupReceived(final ChannelHandlerContext ctx, final HttpRequest req) throws ProtocolException {
/* Request must contain a Transport header */
- if (!req.containsHeader(HeaderTransport))
+ if ( ! req.containsHeader(HEADER_TRANSPORT)){
throw new ProtocolException("No Transport header");
+ }
- /* Split Transport header into individual options and prepare reponse options list */
- final Deque requestOptions = new java.util.LinkedList(Arrays.asList(req.getHeader(HeaderTransport).split(";")));
+ /* Split Transport header into individual options and prepare response options list */
+ final Deque requestOptions = new java.util.LinkedList(Arrays.asList(req.getHeader(HEADER_TRANSPORT).split(";")));
final List responseOptions = new java.util.LinkedList();
/* Transport header. Protocol must be RTP/AVP/UDP */
final String requestProtocol = requestOptions.removeFirst();
- if (!"RTP/AVP/UDP".equals(requestProtocol))
+ if ( ! "RTP/AVP/UDP".equals(requestProtocol)){
throw new ProtocolException("Transport protocol must be RTP/AVP/UDP, but was " + requestProtocol);
+ }
+
responseOptions.add(requestProtocol);
/* Parse incoming transport options and build response options */
for(final String requestOption: requestOptions) {
/* Split option into key and value */
- final Matcher m_transportOption = s_pattern_transportOption.matcher(requestOption);
- if (!m_transportOption.matches())
+ final Matcher transportOption = PATTERN_TRANSPORT_OPTION.matcher(requestOption);
+ if ( ! transportOption.matches() ){
throw new ProtocolException("Cannot parse Transport option " + requestOption);
- final String key = m_transportOption.group(1);
- final String value = m_transportOption.group(3);
+ }
+ final String key = transportOption.group(1);
+ final String value = transportOption.group(3);
if ("interleaved".equals(key)) {
/* Probably means that two channels are interleaved in the stream. Included in the response options */
- if (!"0-1".equals(value))
+ if ( ! "0-1".equals(value)){
throw new ProtocolException("Unsupported Transport option, interleaved must be 0-1 but was " + value);
+ }
responseOptions.add("interleaved=0-1");
}
else if ("mode".equals(key)) {
/* Means the we're supposed to receive audio data, not send it. Included in the response options */
- if (!"record".equals(value))
+ if ( ! "record".equals(value)){
throw new ProtocolException("Unsupported Transport option, mode must be record but was " + value);
+ }
responseOptions.add("mode=record");
}
else if ("control_port".equals(key)) {
/* Port number of the client's control socket. Response includes port number of *our* control port */
final int clientControlPort = Integer.valueOf(value);
- m_controlChannel = createRtpChannel(
- substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 0),
+
+ controlChannel = createRtpChannel(
+ substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 53670),
substitutePort((InetSocketAddress)ctx.getChannel().getRemoteAddress(), clientControlPort),
RaopRtpChannelType.Control
);
- s_logger.info("Launched RTP control service on " + m_controlChannel.getLocalAddress());
- responseOptions.add("control_port=" + ((InetSocketAddress)m_controlChannel.getLocalAddress()).getPort());
+
+ LOG.info("Launched RTP control service on " + controlChannel.getLocalAddress());
+
+ responseOptions.add("control_port=" + ((InetSocketAddress)controlChannel.getLocalAddress()).getPort());
}
else if ("timing_port".equals(key)) {
/* Port number of the client's timing socket. Response includes port number of *our* timing port */
final int clientTimingPort = Integer.valueOf(value);
- m_timingChannel = createRtpChannel(
- substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 0),
+
+ timingChannel = createRtpChannel(
+ substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 53669),
substitutePort((InetSocketAddress)ctx.getChannel().getRemoteAddress(), clientTimingPort),
RaopRtpChannelType.Timing
);
- s_logger.info("Launched RTP timing service on " + m_timingChannel.getLocalAddress());
- responseOptions.add("timing_port=" + ((InetSocketAddress)m_timingChannel.getLocalAddress()).getPort());
+
+ LOG.info("Launched RTP timing service on " + timingChannel.getLocalAddress());
+
+ responseOptions.add("timing_port=" + ((InetSocketAddress)timingChannel.getLocalAddress()).getPort());
}
else {
/* Ignore unknown options */
@@ -528,26 +638,29 @@ else if ("timing_port".equals(key)) {
}
/* Create audio socket and include it's port in our response */
- m_audioChannel = createRtpChannel(
- substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 0),
+ audioChannel = createRtpChannel(
+ substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 53671),
null,
RaopRtpChannelType.Audio
);
- s_logger.info("Launched RTP audio service on " + m_audioChannel.getLocalAddress());
- responseOptions.add("server_port=" + ((InetSocketAddress)m_audioChannel.getLocalAddress()).getPort());
+
+ LOG.info("Launched RTP audio service on " + audioChannel.getLocalAddress());
+
+ responseOptions.add("server_port=" + ((InetSocketAddress)audioChannel.getLocalAddress()).getPort());
/* Build response options string */
final StringBuilder transportResponseBuilder = new StringBuilder();
for(final String responseOption: responseOptions) {
- if (transportResponseBuilder.length() > 0)
+ if (transportResponseBuilder.length() > 0){
transportResponseBuilder.append(";");
+ }
transportResponseBuilder.append(responseOption);
}
/* Send response */
final HttpResponse response = new DefaultHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
- response.addHeader(HeaderTransport, transportResponseBuilder.toString());
- response.addHeader(HeaderSession, "DEADBEEEF");
+ response.addHeader(HEADER_TRANSPORT, transportResponseBuilder.toString());
+ response.addHeader(HEADER_SESSION, "DEADBEEEF");
ctx.getChannel().write(response);
}
@@ -558,14 +671,15 @@ else if ("timing_port".equals(key)) {
* iTunes reports the initial RTP sequence and playback time here, which would actually be
* helpful. But iOS doesn't, so we ignore it all together.
*/
- public synchronized void recordReceived(final ChannelHandlerContext ctx, final HttpRequest req)
- throws Exception
- {
- if (m_audioStreamInformationProvider == null)
+ public synchronized void recordReceived(final ChannelHandlerContext ctx, final HttpRequest req) throws Exception {
+ if (audioStreamInformationProvider == null){
throw new ProtocolException("Audio stream not configured, cannot start recording");
-
- s_logger.info("Client started streaming");
-
+ }
+ LOG.info("Client started streaming");
+
+ audioOutputQueue.startAudioProcessing();
+ timingHandler.startTimeSync();
+
final HttpResponse response = new DefaultHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
ctx.getChannel().write(response);
}
@@ -577,10 +691,11 @@ public synchronized void recordReceived(final ChannelHandlerContext ctx, final H
* helpful. But iOS doesn't, so we ignore it all together.
*/
private synchronized void flushReceived(final ChannelHandlerContext ctx, final HttpRequest req) {
- if (m_audioOutputQueue != null)
- m_audioOutputQueue.flush();
+ if (audioOutputQueue != null){
+ audioOutputQueue.flush();
+ }
- s_logger.info("Client paused streaming, flushed audio output queue");
+ LOG.info("Client paused streaming, flushed audio output queue");
final HttpResponse response = new DefaultHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
ctx.getChannel().write(response);
@@ -596,7 +711,7 @@ private synchronized void teardownReceived(final ChannelHandlerContext ctx, fina
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
future.getChannel().close();
- s_logger.info("RTSP connection closed after client initiated teardown");
+ LOG.info("RTSP connection closed after client initiated teardown");
}
});
}
@@ -612,9 +727,7 @@ public void operationComplete(final ChannelFuture future) throws Exception {
/**
* Handle SET_PARAMETER request. Currently only {@code volume} is supported
*/
- public synchronized void setParameterReceived(final ChannelHandlerContext ctx, final HttpRequest req)
- throws ProtocolException
- {
+ public synchronized void setParameterReceived(final ChannelHandlerContext ctx, final HttpRequest req) throws ProtocolException {
/* Body in ASCII encoding with unix newlines */
final String body = req.getContent().toString(Charset.forName("ASCII")).replace("\r", "");
@@ -623,16 +736,18 @@ public synchronized void setParameterReceived(final ChannelHandlerContext ctx, f
try {
/* Split parameter into name and value */
final Matcher m_parameter = s_pattern_parameter.matcher(line);
- if (!m_parameter.matches())
+ if (!m_parameter.matches()){
throw new ProtocolException("Cannot parse line " + line);
+ }
final String name = m_parameter.group(1);
final String value = m_parameter.group(2);
if ("volume".equals(name)) {
/* Set output gain */
- if (m_audioOutputQueue != null)
- m_audioOutputQueue.setGain(Float.parseFloat(value));
+ if (audioOutputQueue != null){
+ audioOutputQueue.setRequestedVolume(Float.parseFloat(value));
+ }
}
}
@@ -648,15 +763,13 @@ public synchronized void setParameterReceived(final ChannelHandlerContext ctx, f
/**
* Handle GET_PARAMETER request. Currently only {@code volume} is supported
*/
- public synchronized void getParameterReceived(final ChannelHandlerContext ctx, final HttpRequest req)
- throws ProtocolException
- {
+ public synchronized void getParameterReceived(final ChannelHandlerContext ctx, final HttpRequest req) throws ProtocolException {
final StringBuilder body = new StringBuilder();
- if (m_audioOutputQueue != null) {
+ if (audioOutputQueue != null) {
/* Report output gain */
body.append("volume: ");
- body.append(m_audioOutputQueue.getGain());
+ body.append(audioOutputQueue.getRequestedVolume());
body.append("\r\n");
}
@@ -673,10 +786,12 @@ public synchronized void getParameterReceived(final ChannelHandlerContext ctx, f
* @param channelType channel type. Determines which handlers are put into the pipeline
* @return open data-gram channel
*/
- private Channel createRtpChannel(final SocketAddress local, final SocketAddress remote, final RaopRtpChannelType channelType)
- {
+ private Channel createRtpChannel(final SocketAddress local, final SocketAddress remote, final RaopRtpChannelType channelType) {
/* Create bootstrap helper for a data-gram socket using NIO */
- final ConnectionlessBootstrap bootstrap = new ConnectionlessBootstrap(new NioDatagramChannelFactory(m_rtpExecutorService));
+ //final ConnectionlessBootstrap bootstrap = new ConnectionlessBootstrap(new NioDatagramChannelFactory(rtpExecutorService));
+ final ConnectionlessBootstrap bootstrap = new ConnectionlessBootstrap(new OioDatagramChannelFactory(rtpExecutorService));
+
+
/* Set the buffer size predictor to 1500 bytes to ensure that
* received packets will fit into the buffer. Packets are
@@ -685,7 +800,7 @@ private Channel createRtpChannel(final SocketAddress local, final SocketAddress
bootstrap.setOption("receiveBufferSizePredictorFactory", new FixedReceiveBufferSizePredictorFactory(1500));
/* Set the socket's receive buffer size. We set it to 1MB */
- bootstrap.setOption("receiveBufferSize", 1024*1024);
+ bootstrap.setOption("receiveBufferSize", 1024 * 1024);
/* Set pipeline factory for the RTP channel */
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@@ -693,29 +808,38 @@ private Channel createRtpChannel(final SocketAddress local, final SocketAddress
public ChannelPipeline getPipeline() throws Exception {
final ChannelPipeline pipeline = Channels.pipeline();
- pipeline.addLast("executionHandler", AirReceiver.ChannelExecutionHandler);
- pipeline.addLast("exceptionLogger", m_exceptionLoggingHandler);
- pipeline.addLast("decoder", m_decodeHandler);
- pipeline.addLast("encoder", m_encodeHandler);
+ final AirPlayServer airPlayServer = AirPlayServer.getIstance();
+
+ pipeline.addLast("executionHandler", airPlayServer.getChannelExecutionHandler());
+ pipeline.addLast("exceptionLogger", exceptionLoggingHandler);
+ pipeline.addLast("decoder", decodeHandler);
+ pipeline.addLast("encoder", encodeHandler);
+
/* We pretend that all communication takes place on the audio channel,
* and simply re-route packets from and to the control and timing channels
*/
- if (!channelType.equals(RaopRtpChannelType.Audio)) {
- pipeline.addLast("inputToAudioRouter", m_inputToAudioRouterDownstreamHandler);
+ if ( ! channelType.equals(RaopRtpChannelType.Audio)) {
+ pipeline.addLast("inputToAudioRouter", inputToAudioRouterDownstreamHandler);
+
/* Must come *after* the router, otherwise incoming packets are logged twice */
- pipeline.addLast("packetLogger", m_packetLoggingHandler);
+ pipeline.addLast("packetLogger", packetLoggingHandler);
}
else {
/* Must come *before* the router, otherwise outgoing packets are logged twice */
- pipeline.addLast("packetLogger", m_packetLoggingHandler);
- pipeline.addLast("audioToOutputRouter", m_audioToOutputRouterUpstreamHandler);
- pipeline.addLast("timing", m_timingHandler);
- pipeline.addLast("resendRequester", m_resendRequestHandler);
- if (m_decryptionHandler != null)
- pipeline.addLast("decrypt", m_decryptionHandler);
- if (m_audioDecodeHandler != null)
- pipeline.addLast("audioDecode", m_audioDecodeHandler);
- pipeline.addLast("enqueue", m_audioEnqueueHandler);
+ pipeline.addLast("packetLogger", packetLoggingHandler);
+ pipeline.addLast("audioToOutputRouter", audioToOutputRouterUpstreamHandler);
+ pipeline.addLast("timing", timingHandler);
+ pipeline.addLast("resendRequester", resendRequestHandler);
+
+ if (decryptionHandler != null){
+ pipeline.addLast("decrypt", decryptionHandler);
+ }
+
+ if (audioDecodeHandler != null){
+ pipeline.addLast("audioDecode", audioDecodeHandler);
+ }
+
+ pipeline.addLast("enqueue", audioEnqueueHandler);
}
return pipeline;
@@ -729,18 +853,20 @@ public ChannelPipeline getPipeline() throws Exception {
channel = bootstrap.bind(local);
/* Add to group of RTP channels beloging to this RTSP connection */
- m_rtpChannels.add(channel);
+ rtpChannels.add(channel);
/* Connect to remote address if one was provided */
- if (remote != null)
+ if (remote != null){
channel.connect(remote);
-
+ }
+
didThrow = false;
return channel;
}
finally {
- if (didThrow && (channel != null))
+ if (didThrow && (channel != null)){
channel.close();
+ }
}
}
@@ -763,6 +889,7 @@ private InetSocketAddress substitutePort(final InetSocketAddress address, final
* http://stackoverflow.com/questions/1512578/jvm-crash-on-opening-a-return-socketchannel
* converting to address to a string first fixes the problem.
*/
- return new InetSocketAddress(address.getAddress().getHostAddress(), port);
+ //return new InetSocketAddress(address.getAddress().getHostAddress(), port);
+ return new InetSocketAddress(address.getAddress(), port);
}
}
diff --git a/src/main/java/nz/co/iswe/android/airplay/audio/RaopRtpAudioAlacDecodeHandler.java b/src/main/java/nz/co/iswe/android/airplay/audio/RaopRtpAudioAlacDecodeHandler.java
new file mode 100644
index 0000000..7c11269
--- /dev/null
+++ b/src/main/java/nz/co/iswe/android/airplay/audio/RaopRtpAudioAlacDecodeHandler.java
@@ -0,0 +1,215 @@
+/*
+ * This file is part of AirReceiver.
+ *
+ * AirReceiver 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.
+
+ * AirReceiver 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 AirReceiver. If not, see .
+ */
+
+package nz.co.iswe.android.airplay.audio;
+
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import nz.co.iswe.android.airplay.network.raop.RaopRtpPacket;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
+import org.phlo.AirReceiver.ProtocolException;
+
+import android.media.AudioFormat;
+
+import com.beatofthedrum.alacdecoder.AlacDecodeUtils;
+import com.beatofthedrum.alacdecoder.AlacFile;
+
+/**
+ * Decodes the ALAC audio data in incoming audio packets to big endian unsigned PCM.
+ * Also serves as an {@link AudioStreamInformationProvider}
+ *
+ * This class assumes that ALAC requires no inter-packet state - it doesn't make
+ * any effort to feed the packets to ALAC in the correct order.
+ */
+public class RaopRtpAudioAlacDecodeHandler extends OneToOneDecoder implements AudioStreamInformationProvider {
+
+ private static Logger LOG = Logger.getLogger(RaopRtpAudioAlacDecodeHandler.class.getName());
+
+ /* There are the indices into the SDP format options at which
+ * the sessions appear
+ */
+
+ public static final int FORMAT_OPTION_SAMPLES_PER_FRAME = 0;
+ public static final int FORMAT_OPTION_7a = 1;
+ public static final int FORMAT_OPTION_BITS_PER_SAMPLE = 2;
+ public static final int FORMAT_OPTION_RICE_HISTORY_MULT = 3;
+ public static final int FORMAT_OPTION_RICE_INITIAL_HISTORY = 4;
+ public static final int FORMAT_OPTION_RICE_KMODIFIER = 5;
+ public static final int FORMAT_OPTION_7f = 6;
+ public static final int FORMAT_OPTION_80 = 7;
+ public static final int FORMAT_OPTION_82 = 8;
+ public static final int FORMAT_OPTION_86 = 9;
+ public static final int FORMAT_OPTION_8a_RATE = 10;
+
+ /**
+ * Number of samples per ALAC frame (packet).
+ * One sample here means *two* amplitues, one
+ * for the left channel and one for the right
+ */
+ private final int samplesPerFrame;
+
+ /* We support only 44100 kHz */
+ private final int sampleRate = 44100;
+
+ private final int channels = 2;
+
+ //16 bit
+ private final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
+
+ private final int sampleSizeInBits = 16;
+
+ /**
+ * Decoder state
+ */
+ private final AlacFile alacFile;
+
+ /**
+ * Creates an ALAC decoder instance from a list of format options as
+ * they appear in the SDP session announcement.
+ *
+ * @param formatOptions list of format options
+ * @throws ProtocolException if the format options are invalid for ALAC
+ */
+ public RaopRtpAudioAlacDecodeHandler(final String[] formatOptions) throws ProtocolException {
+
+ samplesPerFrame = Integer.valueOf(formatOptions[FORMAT_OPTION_SAMPLES_PER_FRAME]);
+
+ /* We support only 16-bit ALAC */
+ if ( Integer.valueOf(formatOptions[FORMAT_OPTION_BITS_PER_SAMPLE]) != sampleSizeInBits ) {
+ throw new ProtocolException("Sample size must be 16, but was " + Integer.valueOf(formatOptions[FORMAT_OPTION_BITS_PER_SAMPLE]));
+ }
+
+ /* We support only 44100 kHz */
+ int tempSampleRate = Integer.valueOf(formatOptions[FORMAT_OPTION_8a_RATE]);
+ if (tempSampleRate != getSampleRate()){
+ throw new ProtocolException("Sample rate must be " + getSampleRate() + ", but was " + tempSampleRate);
+ }
+
+ alacFile = AlacDecodeUtils.create_alac(getSampleSizeInBits(), getChannels());
+
+ alacFile.setinfo_max_samples_per_frame = samplesPerFrame;
+ alacFile.setinfo_7a = Integer.valueOf(formatOptions[FORMAT_OPTION_7a]);
+ alacFile.setinfo_sample_size = getSampleSizeInBits();
+ alacFile.setinfo_rice_historymult = Integer.valueOf(formatOptions[FORMAT_OPTION_RICE_HISTORY_MULT]);
+ alacFile.setinfo_rice_initialhistory = Integer.valueOf(formatOptions[FORMAT_OPTION_RICE_INITIAL_HISTORY]);
+ alacFile.setinfo_rice_kmodifier = Integer.valueOf(formatOptions[FORMAT_OPTION_RICE_KMODIFIER]);
+ alacFile.setinfo_7f = Integer.valueOf(formatOptions[FORMAT_OPTION_7f]);
+ alacFile.setinfo_80 = Integer.valueOf(formatOptions[FORMAT_OPTION_80]);
+ alacFile.setinfo_82 = Integer.valueOf(formatOptions[FORMAT_OPTION_82]);
+ alacFile.setinfo_86 = Integer.valueOf(formatOptions[FORMAT_OPTION_86]);
+ alacFile.setinfo_8a_rate = sampleRate;
+
+ LOG.info("Created ALAC decode for options " + Arrays.toString(formatOptions));
+ }
+
+ @Override
+ protected synchronized Object decode(final ChannelHandlerContext ctx, final Channel channel, final Object msg)
+ throws Exception {
+
+ //check for a Audio message
+ if ( ! (msg instanceof RaopRtpPacket.Audio)){
+ return msg;
+ }
+
+ final RaopRtpPacket.Audio alacPacket = (RaopRtpPacket.Audio)msg;
+
+ /* The ALAC decode sometimes reads beyond the input's bounds
+ * (but later discards the data). To alleviate, we allocate
+ * 3 spare bytes at input buffer's end.
+ */
+ final byte[] alacBytes = new byte[alacPacket.getPayload().capacity() + 3];
+ alacPacket.getPayload().getBytes(0, alacBytes, 0, alacPacket.getPayload().capacity());
+
+ /* Decode ALAC to PCM */
+ final int[] pcmSamples = new int[samplesPerFrame * 2];
+ final int pcmSamplesBytes = AlacDecodeUtils.decode_frame(alacFile, alacBytes, pcmSamples, samplesPerFrame);
+
+ /* decode_frame() returns the number of *bytes*, not samples! */
+ final int pcmSamplesLength = pcmSamplesBytes / 4;
+ final Level level = Level.FINEST;
+ if (LOG.isLoggable(level)){
+ LOG.log(level, "Decoded " + alacBytes.length + " bytes of ALAC audio data to " + pcmSamplesLength + " PCM samples");
+ }
+
+ /* Complain if the sender doesn't honour it's commitment */
+ if (pcmSamplesLength != samplesPerFrame){
+ throw new ProtocolException("Frame declared to contain " + samplesPerFrame + ", but contained " + pcmSamplesLength);
+ }
+
+ /* Assemble PCM audio packet from original packet header and decoded data.
+ * The ALAC decode emits signed PCM samples as integers. We store them as
+ * as unsigned big endian integers in the packet.
+ */
+
+ RaopRtpPacket.Audio pcmPacket;
+ if (alacPacket instanceof RaopRtpPacket.AudioTransmit) {
+ pcmPacket = new RaopRtpPacket.AudioTransmit(pcmSamplesLength * 4);
+ alacPacket.getBuffer().getBytes(0, pcmPacket.getBuffer(), 0, RaopRtpPacket.AudioTransmit.LENGTH);
+ }
+ else if (alacPacket instanceof RaopRtpPacket.AudioRetransmit) {
+ pcmPacket = new RaopRtpPacket.AudioRetransmit(pcmSamplesLength * 4);
+ alacPacket.getBuffer().getBytes(0, pcmPacket.getBuffer(), 0, RaopRtpPacket.AudioRetransmit.LENGTH);
+ }
+ else{
+ throw new ProtocolException("Packet type " + alacPacket.getClass() + " is not supported by the ALAC decoder");
+ }
+
+ //for each PCM sample
+ for(int i=0; i < pcmSamples.length; ++i) {
+ /* Convert sample to big endian unsigned integer PCM */
+ final int pcmSampleUnsigned = pcmSamples[i] + 0x8000;
+
+ pcmPacket.getPayload().setByte(2*i, (pcmSampleUnsigned & 0xff00) >> 8);
+ pcmPacket.getPayload().setByte(2*i + 1, pcmSampleUnsigned & 0x00ff);
+ }
+
+ return pcmPacket;
+ }
+
+ @Override
+ public int getFramesPerPacket() {
+ return samplesPerFrame;
+ }
+
+ @Override
+ public double getPacketsPerSecond() {
+ //return getAudioFormat().getSampleRate() / (double)getFramesPerPacket();
+
+ return getSampleRate() / (double)getFramesPerPacket();
+ }
+
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ public int getChannels() {
+ return channels;
+ }
+
+ public int getAudioFormat() {
+ return audioFormat;
+ }
+
+ public int getSampleSizeInBits() {
+ return sampleSizeInBits;
+ }
+}
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtpAudioDecryptionHandler.java b/src/main/java/nz/co/iswe/android/airplay/audio/RaopRtpAudioDecryptionHandler.java
similarity index 55%
rename from src/main/java/org/phlo/AirReceiver/RaopRtpAudioDecryptionHandler.java
rename to src/main/java/nz/co/iswe/android/airplay/audio/RaopRtpAudioDecryptionHandler.java
index ca5dbde..c67d8f0 100644
--- a/src/main/java/org/phlo/AirReceiver/RaopRtpAudioDecryptionHandler.java
+++ b/src/main/java/nz/co/iswe/android/airplay/audio/RaopRtpAudioDecryptionHandler.java
@@ -15,25 +15,37 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.audio;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
-import org.jboss.netty.buffer.*;
-import org.jboss.netty.channel.*;
+import nz.co.iswe.android.airplay.network.raop.RaopRtpPacket;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
/**
* De-crypt AES encoded audio data
*/
public class RaopRtpAudioDecryptionHandler extends OneToOneDecoder {
+
+ private static final Logger LOG = Logger.getLogger(RaopRtpAudioDecryptionHandler.class.getName());
+
/**
* The AES cipher. We request no padding because RAOP/AirTunes only encrypts full
* block anyway and leaves the trailing byte unencrypted
*/
- private final Cipher m_aesCipher = AirTunesCrytography.getCipher("AES/CBC/NoPadding");
+ //private final Cipher m_aesCipher = AirTunesCrytography.getCipher("AES/CBC/NoPadding");
+ private Cipher aesCipher;
/**
* AES key */
@@ -47,12 +59,29 @@ public class RaopRtpAudioDecryptionHandler extends OneToOneDecoder {
public RaopRtpAudioDecryptionHandler(final SecretKey aesKey, final IvParameterSpec aesIv) {
m_aesKey = aesKey;
m_aesIv = aesIv;
+
+
+ String transformation = "AES/CBC/NoPadding";
+ try {
+ aesCipher = Cipher.getInstance(transformation);
+
+ LOG.info("Cipher acquired sucessfully. transformation: " + transformation);
+ }
+ catch (NoSuchAlgorithmException e) {
+ LOG.log(Level.SEVERE, "Error getting the Cipher. transformation: " + transformation, e);
+ }
+ catch (NoSuchPaddingException e) {
+ LOG.log(Level.SEVERE, "Error getting the Cipher. transformation: " + transformation, e);
+ }
+
+
}
@Override
protected synchronized Object decode(final ChannelHandlerContext ctx, final Channel channel, final Object msg)
- throws Exception
- {
+ throws Exception {
+
+ //check the message type
if (msg instanceof RaopRtpPacket.Audio) {
final RaopRtpPacket.Audio audioPacket = (RaopRtpPacket.Audio)msg;
final ChannelBuffer audioPayload = audioPacket.getPayload();
@@ -60,11 +89,15 @@ protected synchronized Object decode(final ChannelHandlerContext ctx, final Chan
/* Cipher is restarted for every packet. We simply overwrite the
* encrypted data with the corresponding plain text
*/
- m_aesCipher.init(Cipher.DECRYPT_MODE, m_aesKey, m_aesIv);
- for(int i=0; (i + 16) <= audioPayload.capacity(); i += 16) {
- byte[] block = new byte[16];
+ aesCipher.init(Cipher.DECRYPT_MODE, m_aesKey, m_aesIv);
+
+ for(int i = 0; (i + 16) <= audioPayload.capacity(); i += 16) {
+ byte[] block = new byte[16];//buffer for decrypting the data
+ //copy the bytes to the buffer
audioPayload.getBytes(i, block);
- block = m_aesCipher.update(block);
+ //decrypt the 16 bytes block
+ block = aesCipher.update(block);
+ //set it back to the channel
audioPayload.setBytes(i, block);
}
}
diff --git a/src/main/java/org/phlo/AirReceiver/AirTunesCrytography.java b/src/main/java/nz/co/iswe/android/airplay/crypto/AirTunesCryptography.java
similarity index 95%
rename from src/main/java/org/phlo/AirReceiver/AirTunesCrytography.java
rename to src/main/java/nz/co/iswe/android/airplay/crypto/AirTunesCryptography.java
index d38bc56..0ab3e1d 100644
--- a/src/main/java/org/phlo/AirReceiver/AirTunesCrytography.java
+++ b/src/main/java/nz/co/iswe/android/airplay/crypto/AirTunesCryptography.java
@@ -15,7 +15,7 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.crypto;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -30,15 +30,13 @@
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
-public final class AirTunesCrytography {
- /**
- * Class is not meant to be instantiated
- */
- private AirTunesCrytography() {
- throw new RuntimeException();
- }
+import org.phlo.AirReceiver.Base64;
+
+//TODO: Change it to a normal POJO without static methods..
+//user IoC to inject this class where necessary as a singleton
+public final class AirTunesCryptography {
- private static final Logger s_logger = Logger.getLogger(AirTunesCrytography.class.getName());
+ private static final Logger LOG = Logger.getLogger(AirTunesCryptography.class.getName());
/**
* The JCA/JCE Provider who supplies the necessary cryptographic algorithms
@@ -84,11 +82,18 @@ private AirTunesCrytography() {
public static final RSAPrivateKey PrivateKey = rsaPrivateKeyDecode(PrivateKeyData);
static final Pattern s_transformation_pattern = Pattern.compile("^([A-Za-z0-9_.-]+)(/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+))?");
+
+ /**
+ * Class is not meant to be instantiated
+ */
+ private AirTunesCryptography() {
+ throw new RuntimeException();
+ }
+
/**
* Replacement for JCA/JCE's {@link javax.crypto.Cipher#getInstance}.
* The original method only accepts JCE providers from signed jars,
- * which prevents us from bundling our cryptography provider Bouncy Caster
- * with the application.
+ * which prevents us from bundling our cryptography provider Bouncy Caster with the application.
*
* @param transformation the transformation to find an implementation for
*/
@@ -156,7 +161,7 @@ else if (resolveProperty(Provider, "Cipher", algorithm) != null) {
/* Create a {@link javax.crypto.Cipher} instance from the {@link javax.crypto.CipherSpi} the provider gave us */
- s_logger.info("Using SPI " + cipherSpi.getClass() + " for " + transformation);
+ LOG.info("Using SPI " + cipherSpi.getClass() + " for " + transformation);
return getCipher(cipherSpi, transformation.toUpperCase());
}
catch (final RuntimeException e) {
@@ -197,6 +202,10 @@ private static RSAPrivateKey rsaPrivateKeyDecode(final String privateKey) {
* @throws Throwable in case of an error
*/
private static Cipher getCipher(final CipherSpi cipherSpi, final String transformation) throws Throwable {
+
+ //06-19 15:22:00.727: W/erverSocketPipelineSink(31810): Caused by: java.lang.NoSuchMethodException: [class javax.crypto.CipherSpi, class java.lang.String]
+
+
/* This API isn't public - usually you're expected to simply use Cipher.getInstance().
* Due to the signed-jar restriction for JCE providers that is not an option, so we
* use one of the private constructors of Cipher.
diff --git a/src/main/java/org/phlo/AirReceiver/ExceptionLoggingHandler.java b/src/main/java/nz/co/iswe/android/airplay/network/ExceptionLoggingHandler.java
similarity index 84%
rename from src/main/java/org/phlo/AirReceiver/ExceptionLoggingHandler.java
rename to src/main/java/nz/co/iswe/android/airplay/network/ExceptionLoggingHandler.java
index 4145848..044000e 100644
--- a/src/main/java/org/phlo/AirReceiver/ExceptionLoggingHandler.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/ExceptionLoggingHandler.java
@@ -15,7 +15,7 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -26,11 +26,11 @@
* Logs exceptions thrown by other channel handlers
*/
public class ExceptionLoggingHandler extends SimpleChannelHandler {
- private static Logger s_logger = Logger.getLogger(ExceptionLoggingHandler.class.getName());
+ private static Logger LOG = Logger.getLogger(ExceptionLoggingHandler.class.getName());
@Override
public void exceptionCaught(final ChannelHandlerContext ctx, final ExceptionEvent evt) throws Exception {
super.exceptionCaught(ctx, evt);
- s_logger.log(Level.WARNING, "Handler raised exception", evt.getCause());
+ LOG.log(Level.WARNING, "Handler raised exception", evt.getCause());
}
}
diff --git a/src/main/java/nz/co/iswe/android/airplay/network/NetworkUtils.java b/src/main/java/nz/co/iswe/android/airplay/network/NetworkUtils.java
new file mode 100644
index 0000000..1a72016
--- /dev/null
+++ b/src/main/java/nz/co/iswe/android/airplay/network/NetworkUtils.java
@@ -0,0 +1,128 @@
+package nz.co.iswe.android.airplay.network;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.logging.Logger;
+
+public class NetworkUtils {
+
+ private static final Logger LOG = Logger.getLogger(NetworkUtils.class.getName());
+
+ private static NetworkUtils instance;
+ public static NetworkUtils getInstance(){
+ if(instance == null){
+ instance = new NetworkUtils();
+ }
+ return instance;
+ }
+
+ private NetworkUtils(){
+
+ }
+
+
+ /**
+ * Returns a suitable hardware address.
+ *
+ * @return a MAC address
+ */
+ public byte[] getHardwareAddress() {
+ try {
+ /* Search network interfaces for an interface with a valid, non-blocked hardware address */
+ for(final NetworkInterface iface: Collections.list(NetworkInterface.getNetworkInterfaces())) {
+ if (iface.isLoopback()){
+ continue;
+ }
+ if (iface.isPointToPoint()){
+ continue;
+ }
+
+ try {
+ final byte[] ifaceMacAddress = iface.getHardwareAddress();
+ if ((ifaceMacAddress != null) && (ifaceMacAddress.length == 6) && !isBlockedHardwareAddress(ifaceMacAddress)) {
+ LOG.info("Hardware address is " + toHexString(ifaceMacAddress) + " (" + iface.getDisplayName() + ")");
+ return Arrays.copyOfRange(ifaceMacAddress, 0, 6);
+ }
+ }
+ catch (final Throwable e) {
+ /* Ignore */
+ }
+ }
+ }
+ catch (final Throwable e) {
+ /* Ignore */
+ }
+
+ /* Fallback to the IP address padded to 6 bytes */
+ try {
+ final byte[] hostAddress = Arrays.copyOfRange(InetAddress.getLocalHost().getAddress(), 0, 6);
+ LOG.info("Hardware address is " + toHexString(hostAddress) + " (IP address)");
+ return hostAddress;
+ }
+ catch (final Throwable e) {
+ /* Ignore */
+ }
+
+ /* Fallback to a constant */
+ LOG.info("Hardware address is 00DEADBEEF00 (last resort)");
+ return new byte[] {(byte)0x00, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF, (byte)0x00};
+ }
+
+ /**
+ * Converts an array of bytes to a hexadecimal string
+ *
+ * @param bytes array of bytes
+ * @return hexadecimal representation
+ */
+ private String toHexString(final byte[] bytes) {
+ final StringBuilder s = new StringBuilder();
+ for(final byte b: bytes) {
+ final String h = Integer.toHexString(0x100 | b);
+ s.append(h.substring(h.length() - 2, h.length()).toUpperCase());
+ }
+ return s.toString();
+ }
+
+ /**
+ * Decides whether or nor a given MAC address is the address of some
+ * virtual interface, like e.g. VMware's host-only interface (server-side).
+ *
+ * @param addr a MAC address
+ * @return true if the MAC address is unsuitable as the device's hardware address
+ */
+ public boolean isBlockedHardwareAddress(final byte[] addr) {
+ if ((addr[0] & 0x02) != 0)
+ /* Locally administered */
+ return true;
+ else if ((addr[0] == 0x00) && (addr[1] == 0x50) && (addr[2] == 0x56))
+ /* VMware */
+ return true;
+ else if ((addr[0] == 0x00) && (addr[1] == 0x1C) && (addr[2] == 0x42))
+ /* Parallels */
+ return true;
+ else if ((addr[0] == 0x00) && (addr[1] == 0x25) && (addr[2] == (byte)0xAE))
+ /* Microsoft */
+ return true;
+ else
+ return false;
+ }
+
+ public String getHostUtils() {
+ String hostName = "DroidAirPlay";
+ try {
+ hostName = InetAddress.getLocalHost().getHostName().split("\\.")[0];
+ }
+ catch (final Throwable e) {
+ //do nothing
+ }
+ return hostName;
+ }
+
+ public String getHardwareAddressString() {
+ byte[] hardwareAddressBytes = getHardwareAddress();
+ return toHexString(hardwareAddressBytes);
+ }
+
+}
diff --git a/src/main/java/org/phlo/AirReceiver/RtpEncodeHandler.java b/src/main/java/nz/co/iswe/android/airplay/network/RtpEncodeHandler.java
similarity index 83%
rename from src/main/java/org/phlo/AirReceiver/RtpEncodeHandler.java
rename to src/main/java/nz/co/iswe/android/airplay/network/RtpEncodeHandler.java
index 73bfdde..2e5b2e6 100644
--- a/src/main/java/org/phlo/AirReceiver/RtpEncodeHandler.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/RtpEncodeHandler.java
@@ -15,7 +15,9 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network;
+
+import nz.co.iswe.android.airplay.network.rtp.RtpPacket;
import org.jboss.netty.channel.*;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
@@ -25,12 +27,12 @@
*/
public class RtpEncodeHandler extends OneToOneEncoder {
@Override
- protected Object encode(final ChannelHandlerContext ctx, final Channel channel, final Object msg)
- throws Exception
- {
- if (msg instanceof RtpPacket)
+ protected Object encode(final ChannelHandlerContext ctx, final Channel channel, final Object msg) throws Exception {
+ if (msg instanceof RtpPacket){
return ((RtpPacket)msg).getBuffer();
- else
+ }
+ else{
return msg;
+ }
}
}
diff --git a/src/main/java/org/phlo/AirReceiver/RtpLoggingHandler.java b/src/main/java/nz/co/iswe/android/airplay/network/RtpLoggingHandler.java
similarity index 69%
rename from src/main/java/org/phlo/AirReceiver/RtpLoggingHandler.java
rename to src/main/java/nz/co/iswe/android/airplay/network/RtpLoggingHandler.java
index cdeda05..0c08e6b 100644
--- a/src/main/java/org/phlo/AirReceiver/RtpLoggingHandler.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/RtpLoggingHandler.java
@@ -15,11 +15,14 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network;
import java.util.logging.Level;
import java.util.logging.Logger;
+import nz.co.iswe.android.airplay.network.raop.RaopRtpPacket;
+import nz.co.iswe.android.airplay.network.rtp.RtpPacket;
+
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
@@ -28,46 +31,46 @@
* Logs incoming and outgoing RTP packets
*/
public class RtpLoggingHandler extends SimpleChannelHandler {
- private static final Logger s_logger = Logger.getLogger(RtpLoggingHandler.class.getName());
+
+ private static final Logger LOG = Logger.getLogger(RtpLoggingHandler.class.getName());
@Override
- public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt)
- throws Exception
- {
+ public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt) throws Exception {
if (evt.getMessage() instanceof RtpPacket) {
final RtpPacket packet = (RtpPacket)evt.getMessage();
-
final Level level = getPacketLevel(packet);
- if (s_logger.isLoggable(level))
- s_logger.log(level, evt.getRemoteAddress() + "> " + packet.toString());
+ if (LOG.isLoggable(level)){
+ LOG.log(level, evt.getRemoteAddress() + "> " + packet.toString());
+ }
}
-
super.messageReceived(ctx, evt);
}
@Override
- public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent evt)
- throws Exception
- {
+ public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent evt) throws Exception {
if (evt.getMessage() instanceof RtpPacket) {
final RtpPacket packet = (RtpPacket)evt.getMessage();
final Level level = getPacketLevel(packet);
- if (s_logger.isLoggable(level))
- s_logger.log(level, evt.getRemoteAddress() + "< " + packet.toString());
+ if (LOG.isLoggable(level)){
+ LOG.log(level, evt.getRemoteAddress() + "< " + packet.toString());
+ }
}
-
super.writeRequested(ctx, evt);
}
private Level getPacketLevel(final RtpPacket packet) {
- if (packet instanceof RaopRtpPacket.Audio)
- return Level.FINEST;
- else if (packet instanceof RaopRtpPacket.RetransmitRequest)
+ if (packet instanceof RaopRtpPacket.Audio){
return Level.FINEST;
- else if (packet instanceof RaopRtpPacket.Timing)
+ }
+ else if (packet instanceof RaopRtpPacket.RetransmitRequest){
return Level.FINEST;
- else
+ }
+ else if (packet instanceof RaopRtpPacket.Timing){
+ return Level.INFO;
+ }
+ else{
return Level.FINE;
+ }
}
}
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtpDecodeHandler.java b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpDecodeHandler.java
similarity index 82%
rename from src/main/java/org/phlo/AirReceiver/RaopRtpDecodeHandler.java
rename to src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpDecodeHandler.java
index c4255be..7a051b6 100644
--- a/src/main/java/org/phlo/AirReceiver/RaopRtpDecodeHandler.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpDecodeHandler.java
@@ -15,24 +15,25 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network.raop;
import java.util.logging.Logger;
+
import org.jboss.netty.buffer.*;
import org.jboss.netty.channel.*;
import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
+import org.phlo.AirReceiver.InvalidPacketException;
/**
* Decodes incoming packets, emitting instances of {@link RaopRtpPacket}
*/
public class RaopRtpDecodeHandler extends OneToOneDecoder {
- private static final Logger s_logger = Logger.getLogger(RaopRtpDecodeHandler.class.getName());
+
+ private static final Logger LOG = Logger.getLogger(RaopRtpDecodeHandler.class.getName());
@Override
- protected Object decode(final ChannelHandlerContext ctx, final Channel channel, final Object msg)
- throws Exception
- {
+ protected Object decode(final ChannelHandlerContext ctx, final Channel channel, final Object msg) throws Exception {
if (msg instanceof ChannelBuffer) {
final ChannelBuffer buffer = (ChannelBuffer)msg;
@@ -40,7 +41,7 @@ protected Object decode(final ChannelHandlerContext ctx, final Channel channel,
return RaopRtpPacket.decode(buffer);
}
catch (final InvalidPacketException e1) {
- s_logger.warning(e1.getMessage());
+ LOG.warning(e1.getMessage());
return buffer;
}
}
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtpPacket.java b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpPacket.java
similarity index 78%
rename from src/main/java/org/phlo/AirReceiver/RaopRtpPacket.java
rename to src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpPacket.java
index c6c986a..04a516b 100644
--- a/src/main/java/org/phlo/AirReceiver/RaopRtpPacket.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpPacket.java
@@ -15,14 +15,21 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network.raop;
+
+import java.util.logging.Logger;
+
+import nz.co.iswe.android.airplay.network.rtp.RtpPacket;
import org.jboss.netty.buffer.ChannelBuffer;
+import org.phlo.AirReceiver.ProtocolException;
/**
* Base class for the various RTP packet types of RAOP/AirTunes
*/
public abstract class RaopRtpPacket extends RtpPacket {
+ private static final Logger LOG = Logger.getLogger(RaopRtpPacket.class.getName());
+
/**
* Reads an 32-bit unsigned integer from a channel buffer
* @param buffer the channel buffer
@@ -85,29 +92,29 @@ public static void setBeUInt16(final ChannelBuffer buffer, final int index, fina
* point number with 32 fractional bits.
*/
public static final class NtpTime {
- public static final int Length = 8;
+ public static final int LENGTH = 8;
- private final ChannelBuffer m_buffer;
+ private final ChannelBuffer buffer;
protected NtpTime(final ChannelBuffer buffer) {
- assert buffer.capacity() == Length;
- m_buffer = buffer;
+ assert buffer.capacity() == LENGTH;
+ this.buffer = buffer;
}
public long getSeconds() {
- return getBeUInt(m_buffer, 0);
+ return getBeUInt(buffer, 0);
}
public void setSeconds(final long seconds) {
- setBeUInt(m_buffer, 0, seconds);
+ setBeUInt(buffer, 0, seconds);
}
public long getFraction() {
- return getBeUInt(m_buffer, 4);
+ return getBeUInt(buffer, 4);
}
public void setFraction(final long fraction) {
- setBeUInt(m_buffer, 4, fraction);
+ setBeUInt(buffer, 4, fraction);
}
public double getDouble() {
@@ -124,10 +131,10 @@ public void setDouble(final double v) {
* Base class for {@link TimingRequest} and {@link TimingResponse}
*/
public static class Timing extends RaopRtpPacket {
- public static final int Length = RaopRtpPacket.Length + 4 + 8 + 8 + 8;
+ public static final int LENGTH = RaopRtpPacket.LENGTH + 4 + 8 + 8 + 8;
protected Timing() {
- super(Length);
+ super(LENGTH);
setMarker(true);
setSequence(7);
}
@@ -142,7 +149,7 @@ protected Timing(final ChannelBuffer buffer, final int minimumSize) throws Proto
* @return
*/
public NtpTime getReferenceTime() {
- return new NtpTime(getBuffer().slice(RaopRtpPacket.Length + 4, 8));
+ return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 4, 8));
}
/**
@@ -151,7 +158,7 @@ public NtpTime getReferenceTime() {
* @return
*/
public NtpTime getReceivedTime() {
- return new NtpTime(getBuffer().slice(RaopRtpPacket.Length + 12, 8));
+ return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 12, 8));
}
/**
@@ -160,7 +167,7 @@ public NtpTime getReceivedTime() {
* @return
*/
public NtpTime getSendTime() {
- return new NtpTime(getBuffer().slice(RaopRtpPacket.Length + 20, 8));
+ return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 20, 8));
}
@Override
@@ -186,14 +193,14 @@ public String toString() {
* at least iOS ignores the packet.
*/
public static final class TimingRequest extends Timing {
- public static final byte PayloadType = 0x52;
+ public static final byte PAYLOAD_TYPE = 0x52;
public TimingRequest() {
- setPayloadType(PayloadType);
+ setPayloadType(PAYLOAD_TYPE);
}
protected TimingRequest(final ChannelBuffer buffer) throws ProtocolException {
- super(buffer, Length);
+ super(buffer, LENGTH);
}
}
@@ -207,14 +214,14 @@ protected TimingRequest(final ChannelBuffer buffer) throws ProtocolException {
* sequence, which is always 7.
*/
public static final class TimingResponse extends Timing {
- public static final byte PayloadType = 0x53;
+ public static final byte PAYLOAD_TYPE = 0x53;
public TimingResponse() {
- setPayloadType(PayloadType);
+ setPayloadType(PAYLOAD_TYPE);
}
protected TimingResponse(final ChannelBuffer buffer) throws ProtocolException {
- super(buffer, Length);
+ super(buffer, LENGTH);
}
}
@@ -229,16 +236,16 @@ protected TimingResponse(final ChannelBuffer buffer) throws ProtocolException {
*
*/
public static final class Sync extends RaopRtpPacket {
- public static final byte PayloadType = 0x54;
- public static final int Length = RaopRtpPacket.Length + 4 + 8 + 4;
+ public static final byte PAYLOAD_TYPE = 0x54;
+ public static final int LENGTH = RaopRtpPacket.LENGTH + 4 + 8 + 4;
public Sync() {
- super(Length);
- setPayloadType(PayloadType);
+ super(LENGTH);
+ setPayloadType(PAYLOAD_TYPE);
}
protected Sync(final ChannelBuffer buffer) throws ProtocolException {
- super(buffer, Length);
+ super(buffer, LENGTH);
}
/**
@@ -248,7 +255,7 @@ protected Sync(final ChannelBuffer buffer) throws ProtocolException {
* @return the source's RTP time corresponding to {@link #getTime()} minus the latency
*/
public long getTimeStampMinusLatency() {
- return getBeUInt(getBuffer(), RaopRtpPacket.Length);
+ return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH);
}
/**
@@ -258,7 +265,7 @@ public long getTimeStampMinusLatency() {
* @return the source's RTP time corresponding to {@link #getTime()} minus the latency
*/
public void setTimeStampMinusLatency(final long value) {
- setBeUInt(getBuffer(), RaopRtpPacket.Length, value);
+ setBeUInt(getBuffer(), RaopRtpPacket.LENGTH, value);
}
/**
@@ -266,7 +273,7 @@ public void setTimeStampMinusLatency(final long value) {
* @return the source's NTP time corresponding to the RTP time returned by {@link #getTimeStamp()}
*/
public NtpTime getTime() {
- return new NtpTime(getBuffer().slice(RaopRtpPacket.Length + 4, 8));
+ return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 4, 8));
}
/**
@@ -276,7 +283,7 @@ public NtpTime getTime() {
* @return the source's RTP time corresponding to {@link #getTime()}
*/
public long getTimeStamp() {
- return getBeUInt(getBuffer(), RaopRtpPacket.Length + 4 + 8);
+ return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 8);
}
/**
@@ -286,7 +293,7 @@ public long getTimeStamp() {
* @param value the source's RTP time corresponding to {@link #getTime()}
*/
public void setTimeStamp(final long value) {
- setBeUInt(getBuffer(), RaopRtpPacket.Length + 4 + 8, value);
+ setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 8, value);
}
@Override
@@ -316,18 +323,18 @@ public String toString() {
*
*/
public static final class RetransmitRequest extends RaopRtpPacket {
- public static final byte PayloadType = 0x55;
- public static final int Length = RaopRtpPacket.Length + 4;
+ public static final byte PAYLOAD_TYPE = 0x55;
+ public static final int LENGTH = RaopRtpPacket.LENGTH + 4;
public RetransmitRequest() {
- super(Length);
- setPayloadType(PayloadType);
+ super(LENGTH);
+ setPayloadType(PAYLOAD_TYPE);
setMarker(true);
setSequence(1);
}
protected RetransmitRequest(final ChannelBuffer buffer) throws ProtocolException {
- super(buffer, Length);
+ super(buffer, LENGTH);
}
/**
@@ -335,7 +342,7 @@ protected RetransmitRequest(final ChannelBuffer buffer) throws ProtocolException
* @return sequence number
*/
public int getSequenceFirst() {
- return getBeUInt16(getBuffer(), RaopRtpPacket.Length);
+ return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH);
}
/**
@@ -343,7 +350,7 @@ public int getSequenceFirst() {
* @param value sequence number
*/
public void setSequenceFirst(final int value) {
- setBeUInt16(getBuffer(), RaopRtpPacket.Length, value);
+ setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH, value);
}
/**
@@ -351,7 +358,7 @@ public void setSequenceFirst(final int value) {
* @return number of missing packets
*/
public int getSequenceCount() {
- return getBeUInt16(getBuffer(), RaopRtpPacket.Length + 2);
+ return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2);
}
/**
@@ -359,7 +366,7 @@ public int getSequenceCount() {
* @param value number of missing packets
*/
public void setSequenceCount(final int value) {
- setBeUInt16(getBuffer(), RaopRtpPacket.Length + 2, value);
+ setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2, value);
}
@Override
@@ -393,7 +400,7 @@ protected Audio(final ChannelBuffer buffer, final int minimumSize) throws Protoc
abstract public long getTimeStamp();
/**
- * Gets the packet's RTP time stamp (frame time)
+ * Sets the packet's RTP time stamp (frame time)
* @param timeStamp RTP timestamp in frames
*/
abstract public void setTimeStamp(long timeStamp);
@@ -421,43 +428,43 @@ protected Audio(final ChannelBuffer buffer, final int minimumSize) throws Protoc
* Sent by the source (iTunes/iOS) on the audio channel.
*/
public static final class AudioTransmit extends Audio {
- public static final byte PayloadType = 0x60;
- public static final int Length = RaopRtpPacket.Length + 4 + 4;
+ public static final byte PAYLOAD_TYPE = 0x60;
+ public static final int LENGTH = RaopRtpPacket.LENGTH + 4 + 4;
public AudioTransmit(final int payloadLength) {
- super(Length + payloadLength);
+ super(LENGTH + payloadLength);
assert payloadLength >= 0;
- setPayloadType(PayloadType);
+ setPayloadType(PAYLOAD_TYPE);
}
protected AudioTransmit(final ChannelBuffer buffer) throws ProtocolException {
- super(buffer, Length);
+ super(buffer, LENGTH);
}
@Override
public long getTimeStamp() {
- return getBeUInt(getBuffer(), RaopRtpPacket.Length);
+ return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH);
}
@Override
public void setTimeStamp(final long timeStamp) {
- setBeUInt(getBuffer(), RaopRtpPacket.Length, timeStamp);
+ setBeUInt(getBuffer(), RaopRtpPacket.LENGTH, timeStamp);
}
@Override
public long getSSrc() {
- return getBeUInt(getBuffer(), RaopRtpPacket.Length + 4);
+ return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4);
}
@Override
public void setSSrc(final long sSrc) {
- setBeUInt(getBuffer(), RaopRtpPacket.Length + 4, sSrc);
+ setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4, sSrc);
}
@Override
public ChannelBuffer getPayload() {
- return getBuffer().slice(Length, getLength() - Length);
+ return getBuffer().slice(LENGTH, getLength() - LENGTH);
}
@Override
@@ -480,32 +487,32 @@ public String toString() {
* to {@link RetransmitRequest}.
*/
public static final class AudioRetransmit extends Audio {
- public static final byte PayloadType = 0x56;
- public static final int Length = RaopRtpPacket.Length + 4 + 4 + 4;
+ public static final byte PAYLOAD_TYPE = 0x56;
+ public static final int LENGTH = RaopRtpPacket.LENGTH + 4 + 4 + 4;
public AudioRetransmit(final int payloadLength) {
- super(Length + payloadLength);
+ super(LENGTH + payloadLength);
assert payloadLength >= 0;
- setPayloadType(PayloadType);
+ setPayloadType(PAYLOAD_TYPE);
}
protected AudioRetransmit(final ChannelBuffer buffer) throws ProtocolException {
- super(buffer, Length);
+ super(buffer, LENGTH);
}
/**
* First two bytes after RTP header
*/
public int getUnknown2Bytes() {
- return getBeUInt16(getBuffer(), RaopRtpPacket.Length);
+ return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH);
}
/**
* First two bytes after RTP header
*/
public void setUnknown2Bytes(final int b) {
- setBeUInt16(getBuffer(), RaopRtpPacket.Length, b);
+ setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH, b);
}
/**
@@ -514,7 +521,7 @@ public void setUnknown2Bytes(final int b) {
* retransmitted).
*/
public int getOriginalSequence() {
- return getBeUInt16(getBuffer(), RaopRtpPacket.Length + 2);
+ return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2);
}
/**
@@ -523,32 +530,32 @@ public int getOriginalSequence() {
* retransmitted).
*/
public void setOriginalSequence(final int seq) {
- setBeUInt16(getBuffer(), RaopRtpPacket.Length + 2, seq);
+ setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2, seq);
}
@Override
public long getTimeStamp() {
- return getBeUInt(getBuffer(), RaopRtpPacket.Length + 4);
+ return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4);
}
@Override
public void setTimeStamp(final long timeStamp) {
- setBeUInt(getBuffer(), RaopRtpPacket.Length + 4, timeStamp);
+ setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4, timeStamp);
}
@Override
public long getSSrc() {
- return getBeUInt(getBuffer(), RaopRtpPacket.Length + 4 + 4);
+ return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 4);
}
@Override
public void setSSrc(final long sSrc) {
- setBeUInt(getBuffer(), RaopRtpPacket.Length + 4 + 4, sSrc);
+ setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 4, sSrc);
}
@Override
public ChannelBuffer getPayload() {
- return getBuffer().slice(Length, getLength() - Length);
+ return getBuffer().slice(LENGTH, getLength() - LENGTH);
}
@Override
@@ -578,15 +585,18 @@ public String toString() {
public static RaopRtpPacket decode(final ChannelBuffer buffer)
throws ProtocolException
{
- final RtpPacket rtpPacket = new RtpPacket(buffer, Length);
+ final RtpPacket rtpPacket = new RtpPacket(buffer, LENGTH);
+ //add log here and compare between the java and android implementations
+ LOG.finest("decode packet. RtpPacket: " + rtpPacket);
+
switch (rtpPacket.getPayloadType()) {
- case TimingRequest.PayloadType: return new TimingRequest(buffer);
- case TimingResponse.PayloadType: return new TimingResponse(buffer);
- case Sync.PayloadType: return new Sync(buffer);
- case RetransmitRequest.PayloadType: return new RetransmitRequest(buffer);
- case AudioRetransmit.PayloadType: return new AudioRetransmit(buffer);
- case AudioTransmit.PayloadType: return new AudioTransmit(buffer);
+ case TimingRequest.PAYLOAD_TYPE: return new TimingRequest(buffer);
+ case TimingResponse.PAYLOAD_TYPE: return new TimingResponse(buffer);
+ case Sync.PAYLOAD_TYPE: return new Sync(buffer);
+ case RetransmitRequest.PAYLOAD_TYPE: return new RetransmitRequest(buffer);
+ case AudioRetransmit.PAYLOAD_TYPE: return new AudioRetransmit(buffer);
+ case AudioTransmit.PAYLOAD_TYPE: return new AudioTransmit(buffer);
default: throw new ProtocolException("Invalid PayloadType " + rtpPacket.getPayloadType());
}
}
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtpTimingHandler.java b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpTimingHandler.java
similarity index 58%
rename from src/main/java/org/phlo/AirReceiver/RaopRtpTimingHandler.java
rename to src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpTimingHandler.java
index 3018041..c98e28c 100644
--- a/src/main/java/org/phlo/AirReceiver/RaopRtpTimingHandler.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtpTimingHandler.java
@@ -15,11 +15,19 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network.raop;
import java.util.logging.Logger;
-import org.jboss.netty.channel.*;
+import nz.co.iswe.android.airplay.network.raop.RaopRtpPacket.TimingRequest;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelHandler;
+import org.phlo.AirReceiver.AudioClock;
+import org.phlo.AirReceiver.RunningExponentialAverage;
/**
* Handles RTP timing.
@@ -29,34 +37,37 @@
* sync packet.
*/
public class RaopRtpTimingHandler extends SimpleChannelHandler {
- private static Logger s_logger = Logger.getLogger(RaopRtpTimingHandler.class.getName());
+ private static Logger LOG = Logger.getLogger(RaopRtpTimingHandler.class.getName());
/**
* Number of seconds between {@link TimingRequest}s.
*/
- public static final double TimeRequestInterval = 0.2;
+ public static final double TIME_REQUEST_INTERVAL = 3;
/**
* Thread which sends out {@link TimingRequests}s.
*/
private class TimingRequester implements Runnable {
- private final Channel m_channel;
+ private final Channel channel;
public TimingRequester(final Channel channel) {
- m_channel = channel;
+ this.channel = channel;
}
@Override
public void run() {
- while (!Thread.currentThread().isInterrupted()) {
+ while ( ! Thread.currentThread().isInterrupted() ) {
final RaopRtpPacket.TimingRequest timingRequestPacket = new RaopRtpPacket.TimingRequest();
+
timingRequestPacket.getReceivedTime().setDouble(0); /* Set by the source */
timingRequestPacket.getReferenceTime().setDouble(0); /* Set by the source */
- timingRequestPacket.getSendTime().setDouble(m_audioClock.getNowSecondsTime());
+ timingRequestPacket.getSendTime().setDouble(audioClock.getNowSecondsTime());
- m_channel.write(timingRequestPacket);
+ LOG.info("sending timingRequestPacket: " + timingRequestPacket);
+
+ channel.write(timingRequestPacket);
try {
- Thread.sleep(Math.round(TimeRequestInterval * 1000));
+ Thread.sleep(Math.round(TIME_REQUEST_INTERVAL * 1000));
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
@@ -68,47 +79,55 @@ public void run() {
/**
* Audio time source
*/
- private final AudioClock m_audioClock;
+ private final AudioClock audioClock;
/**
* Exponential averager used to smooth the remote seconds offset
*/
- private final RunningExponentialAverage m_remoteSecondsOffset = new RunningExponentialAverage();
+ private final RunningExponentialAverage averageRemoteSecondsOffset = new RunningExponentialAverage();
/**
* The {@link TimingRequester} thread.
*/
- private Thread m_synchronizationThread;
+ private Thread synchronizationThread;
+ private boolean started = false;
+
public RaopRtpTimingHandler(final AudioClock audioClock) {
- m_audioClock = audioClock;
+ this.audioClock = audioClock;
}
@Override
- public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent evt)
- throws Exception
- {
+ public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent evt) throws Exception {
+
channelClosed(ctx, evt);
- /* Start synchronization thread if it isn't already running */
- if (m_synchronizationThread == null) {
- m_synchronizationThread = new Thread(new TimingRequester(ctx.getChannel()));
- m_synchronizationThread.setDaemon(true);
- m_synchronizationThread.setName("Time Synchronizer");
- m_synchronizationThread.start();
- s_logger.fine("Time synchronizer started");
+ /* create synchronization thread if it isn't already running */
+ if (synchronizationThread == null) {
+ synchronizationThread = new Thread(new TimingRequester(ctx.getChannel()));
+ synchronizationThread.setDaemon(true);
+ synchronizationThread.setName("Time Synchronizer");
}
-
+
super.channelOpen(ctx, evt);
}
+
+ public synchronized void startTimeSync(){
+ /* Start synchronization thread if it isn't already running */
+ if (synchronizationThread != null && ! started) {
+ synchronizationThread.start();
+ LOG.info("Time synchronizer started");
+ started = true;
+ }
+ }
@Override
public void channelClosed(final ChannelHandlerContext ctx, final ChannelStateEvent evt)
throws Exception
{
synchronized(this) {
- if (m_synchronizationThread != null)
- m_synchronizationThread.interrupt();
+ if (synchronizationThread != null)
+ synchronizationThread.interrupt();
}
}
@@ -116,26 +135,26 @@ public void channelClosed(final ChannelHandlerContext ctx, final ChannelStateEve
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt)
throws Exception
{
- if (evt.getMessage() instanceof RaopRtpPacket.Sync)
+ if (evt.getMessage() instanceof RaopRtpPacket.Sync){
syncReceived((RaopRtpPacket.Sync)evt.getMessage());
- else if (evt.getMessage() instanceof RaopRtpPacket.TimingResponse)
+ }
+ else if (evt.getMessage() instanceof RaopRtpPacket.TimingResponse){
timingResponseReceived((RaopRtpPacket.TimingResponse)evt.getMessage());
+ }
super.messageReceived(ctx, evt);
}
private synchronized void timingResponseReceived(final RaopRtpPacket.TimingResponse timingResponsePacket) {
- final double localReceiveSecondsTime = m_audioClock.getNowSecondsTime();
+ final double localReceiveSecondsTime = audioClock.getNowSecondsTime();
/* Compute remove seconds offset, assuming that the transmission times of
* the timing requests and the timing response are equal
*/
- final double localSecondsTime =
- localReceiveSecondsTime * 0.5 +
- timingResponsePacket.getReferenceTime().getDouble() * 0.5;
- final double remoteSecondsTime =
- timingResponsePacket.getReceivedTime().getDouble() * 0.5 +
- timingResponsePacket.getSendTime().getDouble() * 0.5;
+ final double localSecondsTime = localReceiveSecondsTime * 0.5 + timingResponsePacket.getReferenceTime().getDouble() * 0.5;
+
+ final double remoteSecondsTime = timingResponsePacket.getReceivedTime().getDouble() * 0.5 + timingResponsePacket.getSendTime().getDouble() * 0.5;
+
final double remoteSecondsOffset = remoteSecondsTime - localSecondsTime;
/*
@@ -151,30 +170,31 @@ private synchronized void timingResponseReceived(final RaopRtpPacket.TimingRespo
* 1e-3, and starts to decrease rapidly for transmission times significantly
* larger than 1ms.
*/
- final double localInterval =
- localReceiveSecondsTime -
- timingResponsePacket.getReferenceTime().getDouble();
- final double remoteInterval =
- timingResponsePacket.getSendTime().getDouble() -
- timingResponsePacket.getReceivedTime().getDouble();
+ final double localInterval = localReceiveSecondsTime - timingResponsePacket.getReferenceTime().getDouble();
+
+ final double remoteInterval = timingResponsePacket.getSendTime().getDouble() - timingResponsePacket.getReceivedTime().getDouble();
+
final double transmissionTime = Math.max(localInterval - remoteInterval, 0);
+
final double weight = 1e-6 / (transmissionTime + 1e-3);
/* Update exponential average */
- final double remoteSecondsOffsetPrevious = (!m_remoteSecondsOffset.isEmpty() ? m_remoteSecondsOffset.get() : 0.0);
- m_remoteSecondsOffset.add(remoteSecondsOffset, weight);
- final double secondsTimeAdjustment = m_remoteSecondsOffset.get() - remoteSecondsOffsetPrevious;
+ final double remoteSecondsOffsetPrevious = ( ! averageRemoteSecondsOffset.isEmpty() ? averageRemoteSecondsOffset.get() : 0.0);
+ averageRemoteSecondsOffset.add(remoteSecondsOffset, weight);
+
+ final double secondsTimeAdjustment = averageRemoteSecondsOffset.get() - remoteSecondsOffsetPrevious;
- s_logger.finest("Timing response with weight " + weight + " indicated offset " + remoteSecondsOffset + " thereby adjusting the averaged offset by " + secondsTimeAdjustment + " leading to the new averaged offset " + m_remoteSecondsOffset.get());
+ LOG.info("Timing response with weight " + weight + " indicated offset " + remoteSecondsOffset + " thereby adjusting the averaged offset by " + secondsTimeAdjustment + " leading to the new averaged offset " + averageRemoteSecondsOffset.get());
}
private synchronized void syncReceived(final RaopRtpPacket.Sync syncPacket) {
- if (!m_remoteSecondsOffset.isEmpty()) {
+ LOG.info("sync received : " + syncPacket);
+ if ( ! averageRemoteSecondsOffset.isEmpty() ) {
/* If the times are synchronized, we can correct for the transmission
* time of the sync packet since it contains the time it was sent as
* a source's NTP time.
*/
- m_audioClock.setFrameTime(
+ audioClock.setFrameTime(
syncPacket.getTimeStampMinusLatency(),
convertRemoteToLocalSecondsTime(syncPacket.getTime().getDouble())
);
@@ -183,11 +203,11 @@ private synchronized void syncReceived(final RaopRtpPacket.Sync syncPacket) {
/* If the times aren't yet synchronized, we simply assume the sync
* packet's transmission time is zero.
*/
- m_audioClock.setFrameTime(
+ audioClock.setFrameTime(
syncPacket.getTimeStampMinusLatency(),
0.0
);
- s_logger.warning("Times synchronized, cannot correct latency of sync packet");
+ LOG.warning("Times synchronized, cannot correct latency of sync packet");
}
}
@@ -199,6 +219,6 @@ private synchronized void syncReceived(final RaopRtpPacket.Sync syncPacket) {
* @return local NTP time
*/
private double convertRemoteToLocalSecondsTime(final double remoteSecondsTime) {
- return remoteSecondsTime - m_remoteSecondsOffset.get();
+ return remoteSecondsTime - averageRemoteSecondsOffset.get();
}
}
diff --git a/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtspPipelineFactory.java b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtspPipelineFactory.java
new file mode 100644
index 0000000..9964cbd
--- /dev/null
+++ b/src/main/java/nz/co/iswe/android/airplay/network/raop/RaopRtspPipelineFactory.java
@@ -0,0 +1,73 @@
+/*
+ * This file is part of AirReceiver.
+ *
+ * AirReceiver 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.
+
+ * AirReceiver 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 AirReceiver. If not, see .
+ */
+
+package nz.co.iswe.android.airplay.network.raop;
+
+import nz.co.iswe.android.airplay.AirPlayServer;
+import nz.co.iswe.android.airplay.audio.RaopAudioHandler;
+import nz.co.iswe.android.airplay.network.ExceptionLoggingHandler;
+import nz.co.iswe.android.airplay.network.NetworkUtils;
+
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.handler.codec.rtsp.RtspRequestDecoder;
+import org.jboss.netty.handler.codec.rtsp.RtspResponseEncoder;
+import org.phlo.AirReceiver.RaopRtspChallengeResponseHandler;
+import org.phlo.AirReceiver.RaopRtspHeaderHandler;
+import org.phlo.AirReceiver.RaopRtspOptionsHandler;
+import org.phlo.AirReceiver.RtspErrorResponseHandler;
+import org.phlo.AirReceiver.RtspLoggingHandler;
+import org.phlo.AirReceiver.RtspUnsupportedResponseHandler;
+
+/**
+ * Factory for AirTunes/RAOP RTSP channels
+ */
+public class RaopRtspPipelineFactory implements ChannelPipelineFactory {
+
+ @Override
+ public ChannelPipeline getPipeline() throws Exception {
+ final ChannelPipeline pipeline = Channels.pipeline();
+
+ final AirPlayServer airPlayServer = AirPlayServer.getIstance();
+
+ pipeline.addLast("executionHandler", airPlayServer.getChannelExecutionHandler());
+ pipeline.addLast("closeOnShutdownHandler", new SimpleChannelUpstreamHandler() {
+ @Override
+ public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception {
+ airPlayServer.getChannelGroup().add(e.getChannel());
+ super.channelOpen(ctx, e);
+ }
+ });
+ pipeline.addLast("exceptionLogger", new ExceptionLoggingHandler());
+ pipeline.addLast("decoder", new RtspRequestDecoder());
+ pipeline.addLast("encoder", new RtspResponseEncoder());
+ pipeline.addLast("logger", new RtspLoggingHandler());
+ pipeline.addLast("errorResponse", new RtspErrorResponseHandler());
+ pipeline.addLast("challengeResponse", new RaopRtspChallengeResponseHandler(NetworkUtils.getInstance().getHardwareAddress()));
+ pipeline.addLast("header", new RaopRtspHeaderHandler());
+ pipeline.addLast("options", new RaopRtspOptionsHandler());
+ pipeline.addLast("audio", new RaopAudioHandler(airPlayServer.getExecutorService()));
+ pipeline.addLast("unsupportedResponse", new RtspUnsupportedResponseHandler());
+
+ return pipeline;
+ }
+
+}
diff --git a/src/main/java/org/phlo/AirReceiver/RtpPacket.java b/src/main/java/nz/co/iswe/android/airplay/network/rtp/RtpPacket.java
similarity index 75%
rename from src/main/java/org/phlo/AirReceiver/RtpPacket.java
rename to src/main/java/nz/co/iswe/android/airplay/network/rtp/RtpPacket.java
index 821e116..2780b7c 100644
--- a/src/main/java/org/phlo/AirReceiver/RtpPacket.java
+++ b/src/main/java/nz/co/iswe/android/airplay/network/rtp/RtpPacket.java
@@ -15,41 +15,44 @@
* along with AirReceiver. If not, see .
*/
-package org.phlo.AirReceiver;
+package nz.co.iswe.android.airplay.network.rtp;
import org.jboss.netty.buffer.*;
+import org.phlo.AirReceiver.InvalidPacketException;
+import org.phlo.AirReceiver.ProtocolException;
/**
* Basic RTP packet as described by RFC 3550
*/
public class RtpPacket {
- public static final int Length = 4;
+ public static final int LENGTH = 4;
- final private ChannelBuffer m_buffer;
+ final private ChannelBuffer buffer;
protected RtpPacket(final int size) {
- assert size >= Length;
- m_buffer = ChannelBuffers.buffer(size);
- m_buffer.writeZero(size);
+ assert size >= LENGTH;
+ buffer = ChannelBuffers.buffer(size);
+ buffer.writeZero(size);
setVersion((byte)2);
}
public RtpPacket(final ChannelBuffer buffer) throws ProtocolException {
- m_buffer = buffer;
+ this.buffer = buffer;
}
public RtpPacket(final ChannelBuffer buffer, final int minimumSize) throws ProtocolException {
this(buffer);
- if (buffer.capacity() < minimumSize)
+ if (buffer.capacity() < minimumSize){
throw new InvalidPacketException("Packet had invalid size " + buffer.capacity() + " instead of at least " + minimumSize);
+ }
}
public ChannelBuffer getBuffer() {
- return m_buffer;
+ return buffer;
}
public int getLength() {
- return m_buffer.capacity();
+ return buffer.capacity();
}
/**
@@ -57,7 +60,7 @@ public int getLength() {
* @return RTP version number
*/
public byte getVersion() {
- return (byte)((m_buffer.getByte(0) & (0xC0)) >> 6);
+ return (byte)((buffer.getByte(0) & (0xC0)) >> 6);
}
/**
@@ -66,7 +69,7 @@ public byte getVersion() {
*/
public void setVersion(final byte version) {
assert (version & ~0x03) == 0;
- m_buffer.setByte(0, (m_buffer.getByte(0) & ~(0xC0)) | (version << 6));
+ buffer.setByte(0, (buffer.getByte(0) & ~(0xC0)) | (version << 6));
}
/**
@@ -74,7 +77,7 @@ public void setVersion(final byte version) {
* @return RTP padding flag
*/
public boolean getPadding() {
- return (m_buffer.getByte(0) & (0x20)) != 0;
+ return (buffer.getByte(0) & (0x20)) != 0;
}
/**
@@ -82,7 +85,7 @@ public boolean getPadding() {
* @param padding RTP padding flag
*/
public void setPadding(final boolean padding) {
- m_buffer.setByte(0, (m_buffer.getByte(0) & ~0x20) | (padding ? 0x20 : 0x00));
+ buffer.setByte(0, (buffer.getByte(0) & ~0x20) | (padding ? 0x20 : 0x00));
}
/**
@@ -90,7 +93,7 @@ public void setPadding(final boolean padding) {
* @return RTP padding flag
*/
public boolean getExtension() {
- return (m_buffer.getByte(0) & (0x10)) != 0;
+ return (buffer.getByte(0) & (0x10)) != 0;
}
/**
@@ -98,7 +101,7 @@ public boolean getExtension() {
* @param padding RTP padding flag
*/
public void setExtension(final boolean extension) {
- m_buffer.setByte(0, (m_buffer.getByte(0) & ~0x10) | (extension ? 0x10 : 0x00));
+ buffer.setByte(0, (buffer.getByte(0) & ~0x10) | (extension ? 0x10 : 0x00));
}
/**
@@ -108,7 +111,7 @@ public void setExtension(final boolean extension) {
* @return nubmer of CSRC ids
*/
public byte getCsrcCount() {
- return (byte)(m_buffer.getByte(0) & (0x0f));
+ return (byte)(buffer.getByte(0) & (0x0f));
}
/**
@@ -120,7 +123,7 @@ public byte getCsrcCount() {
*/
public void setCsrcCount(final byte csrcCount) {
assert (csrcCount & ~0x0f) == 0;
- m_buffer.setByte(0, (m_buffer.getByte(0) & ~0x0f) | csrcCount);
+ buffer.setByte(0, (buffer.getByte(0) & ~0x0f) | csrcCount);
}
/**
@@ -128,7 +131,7 @@ public void setCsrcCount(final byte csrcCount) {
* @param marker RTP marker flag
*/
public boolean getMarker() {
- return (m_buffer.getByte(1) & (0x80)) != 0;
+ return (buffer.getByte(1) & (0x80)) != 0;
}
/**
@@ -136,7 +139,7 @@ public boolean getMarker() {
* @param marker RTP marker flag
*/
public void setMarker(final boolean marker) {
- m_buffer.setByte(1, (m_buffer.getByte(1) & ~0x80) | (marker ? 0x80 : 0x00));
+ buffer.setByte(1, (buffer.getByte(1) & ~0x80) | (marker ? 0x80 : 0x00));
}
/**
@@ -144,7 +147,7 @@ public void setMarker(final boolean marker) {
* @return packet's payload type
*/
public byte getPayloadType() {
- return (byte)(m_buffer.getByte(1) & (0x7f));
+ return (byte)(buffer.getByte(1) & (0x7f));
}
/**
@@ -153,7 +156,7 @@ public byte getPayloadType() {
*/
public void setPayloadType(final byte payloadType) {
assert (payloadType & ~0x7f) == 0;
- m_buffer.setByte(1, (m_buffer.getByte(1) & ~0x7f) | payloadType);
+ buffer.setByte(1, (buffer.getByte(1) & ~0x7f) | payloadType);
}
/**
@@ -162,8 +165,8 @@ public void setPayloadType(final byte payloadType) {
*/
public int getSequence() {
return (
- ((m_buffer.getByte(2) & 0xff) << 8) |
- ((m_buffer.getByte(3) & 0xff) << 0)
+ ((buffer.getByte(2) & 0xff) << 8) |
+ ((buffer.getByte(3) & 0xff) << 0)
);
}
@@ -173,8 +176,8 @@ public int getSequence() {
*/
public void setSequence(final int sequence) {
assert (sequence & ~0xffff) == 0;
- m_buffer.setByte(2, (sequence & 0xff00) >> 8);
- m_buffer.setByte(3, (sequence & 0x00ff) >> 0);
+ buffer.setByte(2, (sequence & 0xff00) >> 8);
+ buffer.setByte(3, (sequence & 0x00ff) >> 0);
}
@Override
diff --git a/src/main/java/org/phlo/AirReceiver/AirReceiver.java b/src/main/java/org/phlo/AirReceiver/AirReceiver.java
index b6abd8e..f585950 100644
--- a/src/main/java/org/phlo/AirReceiver/AirReceiver.java
+++ b/src/main/java/org/phlo/AirReceiver/AirReceiver.java
@@ -17,28 +17,52 @@
package org.phlo.AirReceiver;
+import java.awt.Button;
+import java.awt.Dialog;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.MenuItem;
+import java.awt.PopupMenu;
+import java.awt.SystemTray;
+import java.awt.TextArea;
+import java.awt.TrayIcon;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;
-import java.net.*;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Map;
import java.util.List;
+import java.util.Map;
import java.util.Properties;
-import java.util.concurrent.*;
-import java.util.logging.*;
-import java.awt.*;
-import java.awt.event.*;
-
-import javax.swing.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
-import javax.jmdns.*;
+import javax.jmdns.JmDNS;
+import javax.jmdns.ServiceInfo;
+import javax.swing.ImageIcon;
import org.jboss.netty.bootstrap.ServerBootstrap;
-import org.jboss.netty.channel.*;
-import org.jboss.netty.channel.group.*;
-import org.jboss.netty.channel.socket.nio.*;
-import org.jboss.netty.handler.execution.*;
+import org.jboss.netty.channel.ChannelHandler;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+import org.jboss.netty.channel.group.ChannelGroupFuture;
+import org.jboss.netty.channel.group.DefaultChannelGroup;
+import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
+import org.jboss.netty.handler.execution.ExecutionHandler;
+import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
public class AirReceiver {
/* Load java.util.logging configuration */
diff --git a/src/main/java/org/phlo/AirReceiver/AudioClock.java b/src/main/java/org/phlo/AirReceiver/AudioClock.java
index 9864f84..5b8f125 100644
--- a/src/main/java/org/phlo/AirReceiver/AudioClock.java
+++ b/src/main/java/org/phlo/AirReceiver/AudioClock.java
@@ -33,7 +33,7 @@ public interface AudioClock {
*
* @return time of currently played sample
*/
- long getNowFrameTime();
+ //long getNowFrameTime();
/**
* Returns the earliest time in samples for which data
diff --git a/src/main/java/org/phlo/AirReceiver/AudioOutputQueue.java b/src/main/java/org/phlo/AirReceiver/AudioOutputQueue.java
deleted file mode 100644
index 5164e42..0000000
--- a/src/main/java/org/phlo/AirReceiver/AudioOutputQueue.java
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * This file is part of AirReceiver.
- *
- * AirReceiver 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.
-
- * AirReceiver 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 AirReceiver. If not, see .
- */
-
-package org.phlo.AirReceiver;
-
-import java.util.Arrays;
-import java.util.concurrent.ConcurrentSkipListMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.sound.sampled.*;
-
-/**
- * Audio output queue.
- *
- * Serves an an {@link AudioClock} and allows samples to be queued
- * for playback at a specific time.
- */
-public class AudioOutputQueue implements AudioClock {
- private static Logger s_logger = Logger.getLogger(AudioOutputQueue.class.getName());
-
- private static final double QueueLengthMaxSeconds = 10;
- private static final double BufferSizeSeconds = 0.05;
- private static final double TimingPrecision = 0.001;
-
- /**
- * Signals that the queue is being closed.
- * Never transitions from true to false!
- */
- private volatile boolean m_closing = false;
-
- /**
- * The line's audio format
- */
- private final AudioFormat m_format;
-
- /**
- * True if the line's audio format is signed but
- * the requested format was unsigned
- */
- private final boolean m_convertUnsignedToSigned;
-
- /**
- * Bytes per frame, i.e. number of bytes
- * per sample times the number of channels
- */
- private final int m_bytesPerFrame;
-
- /**
- * Sample rate
- */
- private final double m_sampleRate;
-
- /**
- * Average packet size in frames.
- * We use this as the number of silence frames
- * to write on a queue underrun
- */
- private final int m_packetSizeFrames;
-
- /**
- * JavaSounds audio output line
- */
- private final SourceDataLine m_line;
-
- /**
- * The last frame written to the line.
- * Used to generate filler data
- */
- private final byte[] m_lineLastFrame;
-
- /**
- * Packet queue, indexed by playback time
- */
- private final ConcurrentSkipListMap m_queue = new ConcurrentSkipListMap();
-
- /**
- * Enqueuer thread
- */
- private final Thread m_queueThread = new Thread(new EnQueuer());
-
- /**
- * Number of frames appended to the line
- */
- private long m_lineFramesWritten = 0;
-
- /**
- * Largest frame time seen so far
- */
- private long m_latestSeenFrameTime = 0;
-
- /**
- * The frame time corresponding to line time zero
- */
- private long m_frameTimeOffset = 0;
-
- /**
- * The seconds time corresponding to line time zero
- */
- private final double m_secondsTimeOffset;
-
- /**
- * Requested line gain
- */
- private float m_requestedGain = 0.0f;
-
- /**
- * Enqueuer thread
- */
- private class EnQueuer implements Runnable {
- /**
- * Enqueuer thread main method
- */
- @Override
- public void run() {
- try {
- /* Mute line initially to prevent clicks */
- setLineGain(Float.NEGATIVE_INFINITY);
-
- /* Start the line */
- m_line.start();
-
- boolean lineMuted = true;
- boolean didWarnGap = false;
- while (!m_closing) {
- if (!m_queue.isEmpty()) {
- /* Queue filled */
-
- /* If the gap between the next packet and the end of line is
- * negligible (less than one packet), we write it to the line.
- * Otherwise, we fill the line buffer with silence and hope for
- * further packets to appear in the queue
- */
- final long entryFrameTime = m_queue.firstKey();
- final long entryLineTime = convertFrameToLineTime(entryFrameTime);
- final long gapFrames = entryLineTime - getNextLineTime();
- if (gapFrames < -m_packetSizeFrames) {
- /* Too late for playback */
- s_logger.warning("Audio data was scheduled for playback " + (-gapFrames) + " frames ago, skipping");
-
- m_queue.remove(entryFrameTime);
- continue;
- }
- else if (gapFrames < m_packetSizeFrames) {
- /* Negligible gap between packet and line end. Prepare packet for playback */
- didWarnGap = false;
-
- /* Unmute line in case it was muted previously */
- if (lineMuted) {
- s_logger.info("Audio data available, un-muting line");
-
- lineMuted = false;
- applyGain();
- }
- else if (getLineGain() != m_requestedGain) {
- applyGain();
- }
-
- /* Get sample data and do sanity checks */
- final byte[] nextPlaybackSamples = m_queue.remove(entryFrameTime);
- int nextPlaybackSamplesLength = nextPlaybackSamples.length;
- if (nextPlaybackSamplesLength % m_bytesPerFrame != 0) {
- s_logger.severe("Audio data contains non-integral number of frames, ignore last " + (nextPlaybackSamplesLength % m_bytesPerFrame) + " bytes");
-
- nextPlaybackSamplesLength -= nextPlaybackSamplesLength % m_bytesPerFrame;
- }
-
- /* Append packet to line */
- s_logger.finest("Audio data containing " + nextPlaybackSamplesLength / m_bytesPerFrame + " frames for playback time " + entryFrameTime + " found in queue, appending to the output line");
- appendFrames(nextPlaybackSamples, 0, nextPlaybackSamplesLength, entryLineTime);
- continue;
- }
- else {
- /* Gap between packet and line end. Warn */
-
- if (!didWarnGap) {
- didWarnGap = true;
- s_logger.warning("Audio data missing for frame time " + getNextLineTime() + " (currently " + gapFrames + " frames), writing " + m_packetSizeFrames + " frames of silence");
- }
- }
- }
- else {
- /* Queue empty */
-
- if (!lineMuted) {
- lineMuted = true;
- setLineGain(Float.NEGATIVE_INFINITY);
- s_logger.fine("Audio data ended at frame time " + getNextLineTime() + ", writing " + m_packetSizeFrames + " frames of silence and muted line");
- }
- }
-
- appendSilence(m_packetSizeFrames);
- }
-
- /* Before we exit, we fill the line's buffer with silence. This should prevent
- * noise from being output while the line is being stopped
- */
- appendSilence(m_line.available() / m_bytesPerFrame);
- }
- catch (final Throwable e) {
- s_logger.log(Level.SEVERE, "Audio output thread died unexpectedly", e);
- }
- finally {
- setLineGain(Float.NEGATIVE_INFINITY);
- m_line.stop();
- m_line.close();
- }
- }
-
- /**
- * Append the range [off,off+len) from the provided sample data to the line.
- * If the requested playback time does not match the line end time, samples are
- * skipped or silence is inserted as necessary. If the data is marked as being
- * just a filler, some warnings are suppressed.
- *
- * @param samples sample data
- * @param off sample data offset
- * @param len sample data length
- * @param time playback time
- * @param warnNonContinous warn about non-continous samples
- */
- private void appendFrames(final byte[] samples, int off, final int len, long lineTime) {
- assert off % m_bytesPerFrame == 0;
- assert len % m_bytesPerFrame == 0;
-
- while (true) {
- /* Fetch line end time only once per iteration */
- final long endLineTime = getNextLineTime();
-
- final long timingErrorFrames = lineTime - endLineTime;
- final double timingErrorSeconds = timingErrorFrames / m_sampleRate;
-
- if (Math.abs(timingErrorSeconds) <= TimingPrecision) {
- /* Samples to append scheduled exactly at line end. Just append them and be done */
-
- appendFrames(samples, off, len);
- break;
- }
- else if (timingErrorFrames > 0) {
- /* Samples to append scheduled after the line end. Fill the gap with silence */
- s_logger.warning("Audio output non-continous (gap of " + timingErrorFrames + " frames), filling with silence");
-
- appendSilence((int)(lineTime - endLineTime));
- }
- else if (timingErrorFrames < 0) {
- /* Samples to append scheduled before the line end. Remove the overlapping
- * part and retry
- */
- s_logger.warning("Audio output non-continous (overlap of " + (-timingErrorFrames) + "), skipping overlapping frames");
-
- off += (endLineTime - lineTime) * m_bytesPerFrame;
- lineTime += endLineTime - lineTime;
- }
- else {
- /* Strange universe... */
- assert false;
- }
- }
- }
-
- private void appendSilence(final int frames) {
- final byte[] silenceFrames = new byte[frames * m_bytesPerFrame];
- for(int i = 0; i < silenceFrames.length; ++i)
- silenceFrames[i] = m_lineLastFrame[i % m_bytesPerFrame];
- appendFrames(silenceFrames, 0, silenceFrames.length);
- }
-
- /**
- * Append the range [off,off+len) from the provided sample data to the line.
- *
- * @param samples sample data
- * @param off sample data offset
- * @param len sample data length
- */
- private void appendFrames(final byte[] samples, int off, int len) {
- assert off % m_bytesPerFrame == 0;
- assert len % m_bytesPerFrame == 0;
-
- /* Make sure that [off,off+len) does not exceed sample's bounds */
- off = Math.min(off, (samples != null) ? samples.length : 0);
- len = Math.min(len, (samples != null) ? samples.length - off : 0);
- if (len <= 0)
- return;
-
- /* Convert samples if necessary */
- final byte[] samplesConverted = Arrays.copyOfRange(samples, off, off+len);
- if (m_convertUnsignedToSigned) {
- /* The line expects signed PCM samples, so we must
- * convert the unsigned PCM samples to signed.
- * Note that this only affects the high bytes!
- */
- for(int i=0; i < samplesConverted.length; i += 2)
- samplesConverted[i] = (byte)((samplesConverted[i] & 0xff) - 0x80);
- }
-
- /* Write samples to line */
- final int bytesWritten = m_line.write(samplesConverted, 0, samplesConverted.length);
- if (bytesWritten != len)
- s_logger.warning("Audio output line accepted only " + bytesWritten + " bytes of sample data while trying to write " + samples.length + " bytes");
-
- /* Update state */
- synchronized(AudioOutputQueue.this) {
- m_lineFramesWritten += bytesWritten / m_bytesPerFrame;
- for(int b=0; b < m_bytesPerFrame; ++b)
- m_lineLastFrame[b] = samples[off + len - (m_bytesPerFrame - b)];
-
- s_logger.finest("Audio output line end is now at " + getNextLineTime() + " after writing " + len / m_bytesPerFrame + " frames");
- }
- }
- }
-
- AudioOutputQueue(final AudioStreamInformationProvider streamInfoProvider) throws LineUnavailableException {
- final AudioFormat audioFormat = streamInfoProvider.getAudioFormat();
-
- /* OSX does not support unsigned PCM lines. We thust always request
- * a signed line, and convert from unsigned to signed if necessary
- */
- if (AudioFormat.Encoding.PCM_SIGNED.equals(audioFormat.getEncoding())) {
- m_format = audioFormat;
- m_convertUnsignedToSigned = false;
- }
- else if (AudioFormat.Encoding.PCM_UNSIGNED.equals(audioFormat.getEncoding())) {
- m_format = new AudioFormat(
- audioFormat.getSampleRate(),
- audioFormat.getSampleSizeInBits(),
- audioFormat.getChannels(),
- true,
- audioFormat.isBigEndian()
- );
- m_convertUnsignedToSigned = true;
- }
- else {
- throw new LineUnavailableException("Audio encoding " + audioFormat.getEncoding() + " is not supported");
- }
-
- /* Audio format-dependent stuff */
- m_packetSizeFrames = streamInfoProvider.getFramesPerPacket();
- m_bytesPerFrame = m_format.getChannels() * m_format.getSampleSizeInBits() / 8;
- m_sampleRate = m_format.getSampleRate();
- m_lineLastFrame = new byte[m_bytesPerFrame];
- for(int b=0; b < m_lineLastFrame.length; ++b)
- m_lineLastFrame[b] = (b % 2 == 0) ? (byte)-128 : (byte)0;
-
- /* Compute desired line buffer size and obtain a line */
- final int desiredBufferSize = (int)Math.pow(2, Math.ceil(Math.log(BufferSizeSeconds * m_sampleRate * m_bytesPerFrame) / Math.log(2.0)));
- final DataLine.Info lineInfo = new DataLine.Info(
- SourceDataLine.class,
- m_format,
- desiredBufferSize
- );
- m_line = (SourceDataLine)AudioSystem.getLine(lineInfo);
- m_line.open(m_format, desiredBufferSize);
- s_logger.info("Audio output line created and openend. Requested buffer of " + desiredBufferSize / m_bytesPerFrame + " frames, got " + m_line.getBufferSize() / m_bytesPerFrame + " frames");
-
- /* Start enqueuer thread and wait for the line to start.
- * The wait guarantees that the AudioClock functions return
- * sensible values right after construction
- */
- m_queueThread.setDaemon(true);
- m_queueThread.setName("Audio Enqueuer");
- m_queueThread.setPriority(Thread.MAX_PRIORITY);
- m_queueThread.start();
- while (m_queueThread.isAlive() && !m_line.isActive())
- Thread.yield();
-
- /* Initialize the seconds time offset now that the line is running. */
- m_secondsTimeOffset = 2208988800.0 + System.currentTimeMillis() * 1e-3;
- }
-
- /**
- * Sets the line's MASTER_GAIN control to the provided value,
- * or complains to the log of the line does not support a MASTER_GAIN control
- *
- * @param gain gain to set
- */
- private void setLineGain(final float gain) {
- if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
- /* Bound gain value by min and max declared by the control */
- final FloatControl gainControl = (FloatControl)m_line.getControl(FloatControl.Type.MASTER_GAIN);
- if (gain < gainControl.getMinimum())
- gainControl.setValue(gainControl.getMinimum());
- else if (gain > gainControl.getMaximum())
- gainControl.setValue(gainControl.getMaximum());
- else
- gainControl.setValue(gain);
- }
- else
- s_logger.severe("Audio output line doesn not support volume control");
- }
-
- /**
- * Returns the line's MASTER_GAIN control's value.
- */
- private float getLineGain() {
- if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
- /* Bound gain value by min and max declared by the control */
- final FloatControl gainControl = (FloatControl)m_line.getControl(FloatControl.Type.MASTER_GAIN);
- return gainControl.getValue();
- }
- else {
- s_logger.severe("Audio output line doesn not support volume control");
- return 0.0f;
- }
- }
-
- private synchronized void applyGain() {
- setLineGain(m_requestedGain);
- }
-
- /**
- * Sets the desired output gain.
- *
- * @param gain desired gain
- */
- public synchronized void setGain(final float gain) {
- m_requestedGain = gain;
- }
-
- /**
- * Returns the desired output gain.
- *
- * @param gain desired gain
- */
- public synchronized float getGain() {
- return m_requestedGain;
- }
-
- /**
- * Stops audio output
- */
- public void close() {
- m_closing = true;
- m_queueThread.interrupt();
- }
-
- /**
- * Adds sample data to the queue
- *
- * @param playbackRemoteStartFrameTime start time of sample data
- * @param playbackSamples sample data
- * @return true if the sample data was added to the queue
- */
- public synchronized boolean enqueue(final long frameTime, final byte[] frames) {
- /* Playback time of packet */
- final double packetSeconds = (double)frames.length / (double)(m_bytesPerFrame * m_sampleRate);
-
- /* Compute playback delay, i.e., the difference between the last sample's
- * playback time and the current line time
- */
- final double delay =
- (convertFrameToLineTime(frameTime) + frames.length / m_bytesPerFrame - getNextLineTime()) /
- m_sampleRate;
-
- m_latestSeenFrameTime = Math.max(m_latestSeenFrameTime, frameTime);
-
- if (delay < -packetSeconds) {
- /* The whole packet is scheduled to be played in the past */
- s_logger.warning("Audio data arrived " + -(delay) + " seconds too late, dropping");
- return false;
- }
- else if (delay > QueueLengthMaxSeconds) {
- /* The packet extends further into the future that our maximum queue size.
- * We reject it, since this is probably the result of some timing discrepancies
- */
- s_logger.warning("Audio data arrived " + delay + " seconds too early, dropping");
- return false;
- }
-
- m_queue.put(frameTime, frames);
- return true;
- }
-
- /**
- * Removes all currently queued sample data
- */
- public void flush() {
- m_queue.clear();
- }
-
- @Override
- public synchronized void setFrameTime(final long frameTime, final double secondsTime) {
- final double ageSeconds = getNowSecondsTime() - secondsTime;
- final long lineTime = Math.round((secondsTime - m_secondsTimeOffset) * m_sampleRate);
-
- final long frameTimeOffsetPrevious = m_frameTimeOffset;
- m_frameTimeOffset = frameTime - lineTime;
-
- s_logger.fine("Frame time adjusted by " + (m_frameTimeOffset - frameTimeOffsetPrevious) + " based on timing information " + ageSeconds + " seconds old and " + (m_latestSeenFrameTime - frameTime) + " frames before latest seen frame time");
- }
-
- @Override
- public double getNowSecondsTime() {
- return m_secondsTimeOffset + getNowLineTime() / m_sampleRate;
- }
-
- @Override
- public long getNowFrameTime() {
- return m_frameTimeOffset + getNowLineTime();
- }
-
- @Override
- public double getNextSecondsTime() {
- return m_secondsTimeOffset + getNextLineTime() / m_sampleRate;
- }
-
- @Override
- public long getNextFrameTime() {
- return m_frameTimeOffset + getNextLineTime();
- }
-
- @Override
- public double convertFrameToSecondsTime(final long frameTime) {
- return m_secondsTimeOffset + (frameTime - m_frameTimeOffset) / m_sampleRate;
- }
-
- private synchronized long getNextLineTime() {
- return m_lineFramesWritten;
- }
-
- private long getNowLineTime() {
- return m_line.getLongFramePosition();
- }
-
- private synchronized long convertFrameToLineTime(final long entryFrameTime) {
- return entryFrameTime - m_frameTimeOffset;
- }
-}
diff --git a/src/main/java/org/phlo/AirReceiver/ProtocolException.java b/src/main/java/org/phlo/AirReceiver/ProtocolException.java
index 6642c26..0a03910 100644
--- a/src/main/java/org/phlo/AirReceiver/ProtocolException.java
+++ b/src/main/java/org/phlo/AirReceiver/ProtocolException.java
@@ -19,7 +19,7 @@
@SuppressWarnings("serial")
public class ProtocolException extends Exception {
- ProtocolException(final String message) {
+ public ProtocolException(final String message) {
super(message);
}
}
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtpAudioAlacDecodeHandler.java b/src/main/java/org/phlo/AirReceiver/RaopRtpAudioAlacDecodeHandler.java
deleted file mode 100644
index ccdd285..0000000
--- a/src/main/java/org/phlo/AirReceiver/RaopRtpAudioAlacDecodeHandler.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * This file is part of AirReceiver.
- *
- * AirReceiver 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.
-
- * AirReceiver 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 AirReceiver. If not, see .
- */
-
-package org.phlo.AirReceiver;
-
-import java.util.Arrays;
-import java.util.logging.*;
-
-import javax.sound.sampled.AudioFormat;
-
-import org.jboss.netty.channel.*;
-import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
-
-import com.beatofthedrum.alacdecoder.*;
-
-/**
- * Decodes the ALAC audio data in incoming audio packets to big endian unsigned PCM.
- * Also serves as an {@link AudioStreamInformationProvider}
- *
- * This class assumes that ALAC requires no inter-packet state - it doesn't make
- * any effort to feed the packets to ALAC in the correct order.
- */
-public class RaopRtpAudioAlacDecodeHandler extends OneToOneDecoder implements AudioStreamInformationProvider {
- private static Logger s_logger = Logger.getLogger(RaopRtpAudioAlacDecodeHandler.class.getName());
-
- /* There are the indices into the SDP format options at which
- * the sessions appear
- */
-
- public static final int FormatOptionSamplesPerFrame = 0;
- public static final int FormatOption7a = 1;
- public static final int FormatOptionBitsPerSample = 2;
- public static final int FormatOptionRiceHistoryMult = 3;
- public static final int FormatOptionRiceInitialHistory = 4;
- public static final int FormatOptionRiceKModifier = 5;
- public static final int FormatOption7f = 6;
- public static final int FormatOption80 = 7;
- public static final int FormatOption82 = 8;
- public static final int FormatOption86 = 9;
- public static final int FormatOption8a_rate = 10;
-
- /**
- * The {@link AudioFormat} that corresponds to the output produced by the decoder
- */
- private static final AudioFormat AudioOutputFormat = new AudioFormat(
- 44100 /* sample rate */,
- 16 /* bits per sample */,
- 2 /* number of channels */,
- false /* unsigned */,
- true /* big endian */
- );
-
- /**
- * Number of samples per ALAC frame (packet).
- * One sample here means *two* amplitues, one
- * for the left channel and one for the right
- */
- private final int m_samplesPerFrame;
-
- /**
- * Decoder state
- */
- private final AlacFile m_alacFile;
-
- /**
- * Creates an ALAC decoder instance from a list of format options as
- * they appear in the SDP session announcement.
- *
- * @param formatOptions list of format options
- * @throws ProtocolException if the format options are invalid for ALAC
- */
- public RaopRtpAudioAlacDecodeHandler(final String[] formatOptions)
- throws ProtocolException
- {
- m_samplesPerFrame = Integer.valueOf(formatOptions[FormatOptionSamplesPerFrame]);
-
- /* We support only 16-bit ALAC */
- final int bitsPerSample = Integer.valueOf(formatOptions[FormatOptionBitsPerSample]);
- if (bitsPerSample != 16)
- throw new ProtocolException("Sample size must be 16, but was " + bitsPerSample);
-
- /* We support only 44100 kHz */
- final int sampleRate = Integer.valueOf(formatOptions[FormatOption8a_rate]);
- if (sampleRate != 44100)
- throw new ProtocolException("Sample rate must be 44100, but was " + sampleRate);
-
- m_alacFile = AlacDecodeUtils.create_alac(bitsPerSample, 2);
- m_alacFile.setinfo_max_samples_per_frame = m_samplesPerFrame;
- m_alacFile.setinfo_7a = Integer.valueOf(formatOptions[FormatOption7a]);
- m_alacFile.setinfo_sample_size = bitsPerSample;
- m_alacFile.setinfo_rice_historymult = Integer.valueOf(formatOptions[FormatOptionRiceHistoryMult]);
- m_alacFile.setinfo_rice_initialhistory = Integer.valueOf(formatOptions[FormatOptionRiceInitialHistory]);
- m_alacFile.setinfo_rice_kmodifier = Integer.valueOf(formatOptions[FormatOptionRiceKModifier]);
- m_alacFile.setinfo_7f = Integer.valueOf(formatOptions[FormatOption7f]);
- m_alacFile.setinfo_80 = Integer.valueOf(formatOptions[FormatOption80]);
- m_alacFile.setinfo_82 = Integer.valueOf(formatOptions[FormatOption82]);
- m_alacFile.setinfo_86 = Integer.valueOf(formatOptions[FormatOption86]);
- m_alacFile.setinfo_8a_rate = sampleRate;
-
- s_logger.info("Created ALAC decode for options " + Arrays.toString(formatOptions));
- }
-
- @Override
- protected synchronized Object decode(final ChannelHandlerContext ctx, final Channel channel, final Object msg)
- throws Exception
- {
- if (!(msg instanceof RaopRtpPacket.Audio))
- return msg;
-
- final RaopRtpPacket.Audio alacPacket = (RaopRtpPacket.Audio)msg;
-
- /* The ALAC decode sometimes reads beyond the input's bounds
- * (but later discards the data). To alleviate, we allocate
- * 3 spare bytes at input buffer's end.
- */
- final byte[] alacBytes = new byte[alacPacket.getPayload().capacity() + 3];
- alacPacket.getPayload().getBytes(0, alacBytes, 0, alacPacket.getPayload().capacity());
-
- /* Decode ALAC to PCM */
- final int[] pcmSamples = new int[m_samplesPerFrame * 2];
- final int pcmSamplesBytes = AlacDecodeUtils.decode_frame(m_alacFile, alacBytes, pcmSamples, m_samplesPerFrame);
-
- /* decode_frame() returns the number of *bytes*, not samples! */
- final int pcmSamplesLength = pcmSamplesBytes / 4;
- final Level level = Level.FINEST;
- if (s_logger.isLoggable(level))
- s_logger.log(level, "Decoded " + alacBytes.length + " bytes of ALAC audio data to " + pcmSamplesLength + " PCM samples");
-
- /* Complain if the sender doesn't honour it's commitment */
- if (pcmSamplesLength != m_samplesPerFrame)
- throw new ProtocolException("Frame declared to contain " + m_samplesPerFrame + ", but contained " + pcmSamplesLength);
-
- /* Assemble PCM audio packet from original packet header and decoded data.
- * The ALAC decode emits signed PCM samples as integers. We store them as
- * as unsigned big endian integers in the packet.
- */
-
- RaopRtpPacket.Audio pcmPacket;
- if (alacPacket instanceof RaopRtpPacket.AudioTransmit) {
- pcmPacket = new RaopRtpPacket.AudioTransmit(pcmSamplesLength * 4);
- alacPacket.getBuffer().getBytes(0, pcmPacket.getBuffer(), 0, RaopRtpPacket.AudioTransmit.Length);
- }
- else if (alacPacket instanceof RaopRtpPacket.AudioRetransmit) {
- pcmPacket = new RaopRtpPacket.AudioRetransmit(pcmSamplesLength * 4);
- alacPacket.getBuffer().getBytes(0, pcmPacket.getBuffer(), 0, RaopRtpPacket.AudioRetransmit.Length);
- }
- else
- throw new ProtocolException("Packet type " + alacPacket.getClass() + " is not supported by the ALAC decoder");
-
- for(int i=0; i < pcmSamples.length; ++i) {
- /* Convert sample to big endian unsigned integer PCM */
- final int pcmSampleUnsigned = pcmSamples[i] + 0x8000;
-
- pcmPacket.getPayload().setByte(2*i, (pcmSampleUnsigned & 0xff00) >> 8);
- pcmPacket.getPayload().setByte(2*i + 1, pcmSampleUnsigned & 0x00ff);
- }
-
- return pcmPacket;
- }
-
- @Override
- public AudioFormat getAudioFormat() {
- return AudioOutputFormat;
- }
-
- @Override
- public int getFramesPerPacket() {
- return m_samplesPerFrame;
- }
-
- @Override
- public double getPacketsPerSecond() {
- return getAudioFormat().getSampleRate() / (double)getFramesPerPacket();
- }
-}
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtpRetransmitRequestHandler.java b/src/main/java/org/phlo/AirReceiver/RaopRtpRetransmitRequestHandler.java
index b0f0dda..2ca299c 100644
--- a/src/main/java/org/phlo/AirReceiver/RaopRtpRetransmitRequestHandler.java
+++ b/src/main/java/org/phlo/AirReceiver/RaopRtpRetransmitRequestHandler.java
@@ -20,6 +20,9 @@
import java.util.*;
import java.util.logging.Logger;
+import nz.co.iswe.android.airplay.audio.AudioStreamInformationProvider;
+import nz.co.iswe.android.airplay.network.raop.RaopRtpPacket;
+
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtspChallengeResponseHandler.java b/src/main/java/org/phlo/AirReceiver/RaopRtspChallengeResponseHandler.java
index 1cc47cd..900c311 100644
--- a/src/main/java/org/phlo/AirReceiver/RaopRtspChallengeResponseHandler.java
+++ b/src/main/java/org/phlo/AirReceiver/RaopRtspChallengeResponseHandler.java
@@ -17,13 +17,23 @@
package org.phlo.AirReceiver;
-import java.net.*;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
-import javax.crypto.*;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
-import org.jboss.netty.channel.*;
-import org.jboss.netty.handler.codec.http.*;
+import nz.co.iswe.android.airplay.crypto.AirTunesCryptography;
+
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelHandler;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
/**
* Adds an {@code Apple-Response} header to a response if the request contain
@@ -34,16 +44,32 @@ public class RaopRtspChallengeResponseHandler extends SimpleChannelHandler
private static final String HeaderChallenge = "Apple-Challenge";
private static final String HeaderSignature = "Apple-Response";
+ private static final Logger LOG = Logger.getLogger(RaopRtspChallengeResponseHandler.class.getName());
+
+
private final byte[] m_hwAddress;
- private final Cipher m_rsaPkCS1PaddingCipher = AirTunesCrytography.getCipher("RSA/None/PKCS1Padding");
-
+ //private final Cipher m_rsaPkCS1PaddingCipher = AirTunesCrytography.getCipher("RSA/None/PKCS1Padding");
+ private Cipher rsaPkCS1PaddingCipher;
+
private byte[] m_challenge;
private InetAddress m_localAddress;
public RaopRtspChallengeResponseHandler(final byte[] hwAddress) {
assert hwAddress.length == 6;
-
m_hwAddress = hwAddress;
+
+ String transformation = "RSA/None/PKCS1Padding";
+ try {
+ rsaPkCS1PaddingCipher = Cipher.getInstance(transformation);
+
+ LOG.info("Cipher acquired sucessfully. transformation: " + transformation);
+ }
+ catch (NoSuchAlgorithmException e) {
+ LOG.log(Level.SEVERE, "Error getting the Cipher. transformation: " + transformation, e);
+ }
+ catch (NoSuchPaddingException e) {
+ LOG.log(Level.SEVERE, "Error getting the Cipher. transformation: " + transformation, e);
+ }
}
@Override
@@ -114,8 +140,8 @@ private byte[] getSignature() {
sigData.put((byte)0);
try {
- m_rsaPkCS1PaddingCipher.init(Cipher.ENCRYPT_MODE, AirTunesCrytography.PrivateKey);
- return m_rsaPkCS1PaddingCipher.doFinal(sigData.array());
+ rsaPkCS1PaddingCipher.init(Cipher.ENCRYPT_MODE, AirTunesCryptography.PrivateKey);
+ return rsaPkCS1PaddingCipher.doFinal(sigData.array());
}
catch (final Exception e) {
throw new RuntimeException("Unable to sign response", e);
diff --git a/src/main/java/org/phlo/AirReceiver/RaopRtspPipelineFactory.java b/src/main/java/org/phlo/AirReceiver/RaopRtspPipelineFactory.java
deleted file mode 100644
index 146934c..0000000
--- a/src/main/java/org/phlo/AirReceiver/RaopRtspPipelineFactory.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * This file is part of AirReceiver.
- *
- * AirReceiver 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.
-
- * AirReceiver 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 AirReceiver. If not, see .
- */
-
-package org.phlo.AirReceiver;
-
-import org.jboss.netty.channel.*;
-import org.jboss.netty.handler.codec.rtsp.*;
-
-/**
- * Factory for AirTunes/RAOP RTSP channels
- */
-public class RaopRtspPipelineFactory implements ChannelPipelineFactory {
- @Override
- public ChannelPipeline getPipeline() throws Exception {
- final ChannelPipeline pipeline = Channels.pipeline();
-
- pipeline.addLast("executionHandler", AirReceiver.ChannelExecutionHandler);
- pipeline.addLast("closeOnShutdownHandler", AirReceiver.CloseChannelOnShutdownHandler);
- pipeline.addLast("exceptionLogger", new ExceptionLoggingHandler());
- pipeline.addLast("decoder", new RtspRequestDecoder());
- pipeline.addLast("encoder", new RtspResponseEncoder());
- pipeline.addLast("logger", new RtspLoggingHandler());
- pipeline.addLast("errorResponse", new RtspErrorResponseHandler());
- pipeline.addLast("challengeResponse", new RaopRtspChallengeResponseHandler(AirReceiver.HardwareAddressBytes));
- pipeline.addLast("header", new RaopRtspHeaderHandler());
- pipeline.addLast("options", new RaopRtspOptionsHandler());
- pipeline.addLast("audio", new RaopAudioHandler(AirReceiver.ExecutorService));
- pipeline.addLast("unsupportedResponse", new RtspUnsupportedResponseHandler());
-
- return pipeline;
- }
-
-}
diff --git a/src/main/java/org/phlo/AirReceiver/RunningExponentialAverage.java b/src/main/java/org/phlo/AirReceiver/RunningExponentialAverage.java
index db9c4ea..9825cb8 100644
--- a/src/main/java/org/phlo/AirReceiver/RunningExponentialAverage.java
+++ b/src/main/java/org/phlo/AirReceiver/RunningExponentialAverage.java
@@ -22,20 +22,20 @@
* over a series of values.
*/
public class RunningExponentialAverage {
- public double m_value = Double.NaN;
+ public double value = Double.NaN;
/**
* Create an exponential average without an initial value
*/
public RunningExponentialAverage() {
- m_value = Double.NaN;
+ value = Double.NaN;
}
/**
* Create an exponential average with the given initial value
*/
public RunningExponentialAverage(final double initialValue) {
- m_value = initialValue;
+ value = initialValue;
}
/**
@@ -45,11 +45,11 @@ public RunningExponentialAverage(final double initialValue) {
* @param weight the value's weight between 0 and 1.
*/
public void add(final double value, final double weight) {
- if (Double.isNaN(m_value)) {
- m_value = value;
+ if (Double.isNaN(this.value)) {
+ this.value = value;
}
else {
- m_value = value * weight + m_value * (1.0 - weight);
+ this.value = value * weight + this.value * (1.0 - weight);
}
}
@@ -59,7 +59,7 @@ public void add(final double value, final double weight) {
* Otherwise, always returns false.
*/
public boolean isEmpty() {
- return Double.isNaN(m_value);
+ return Double.isNaN(value);
}
/**
@@ -67,6 +67,6 @@ public boolean isEmpty() {
* @return exponential average
*/
public double get() {
- return m_value;
+ return value;
}
}
diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties
index ae46baf..3ed978b 100644
--- a/src/main/resources/logging.properties
+++ b/src/main/resources/logging.properties
@@ -4,6 +4,7 @@ java.util.logging.ConsoleHandler.level=ALL
.level=WARNING
org.phlo.AirReceiver.level=INFO
+nz.co.iswe.android.airplay.level=INFO
#javax.jmdns.level=ALL
#org.phlo.AirReceiver.RaopRtpTimingHandler.level=FINEST
#org.phlo.AirReceiver.AudioOutputQueue.level=FINE
diff --git a/src/test/java/nz/co/iswe/android/airplay/audio/RaopAudioHandlerTest.java b/src/test/java/nz/co/iswe/android/airplay/audio/RaopAudioHandlerTest.java
new file mode 100644
index 0000000..9d2f59c
--- /dev/null
+++ b/src/test/java/nz/co/iswe/android/airplay/audio/RaopAudioHandlerTest.java
@@ -0,0 +1,33 @@
+package nz.co.iswe.android.airplay.audio;
+
+import java.util.regex.Matcher;
+
+import junit.framework.Assert;
+
+import nz.co.iswe.android.airplay.audio.RaopAudioHandler;
+
+import org.junit.Test;
+
+
+public class RaopAudioHandlerTest {
+
+ @Test
+ public void testPatternSdpLine(){
+
+ String line = "rtpmap:value";
+ Matcher matcher = RaopAudioHandler.s_pattern_sdp_a.matcher(line);
+ Assert.assertTrue(matcher.matches());
+
+
+ line = "min-latency:11400";
+ matcher = RaopAudioHandler.s_pattern_sdp_a.matcher(line);
+ Assert.assertTrue(matcher.matches());
+
+ //line = "min:latency:11400";
+ //matcher = RaopAudioHandler.s_pattern_sdp_a.matcher(line);
+ //Assert.assertFalse(matcher.matches());
+
+ }
+
+
+}