/* * 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 <http://www.gnu.org/licenses/>. */ package com.raventech.airplayserver.network.raop; import java.util.logging.Logger; import com.raventech.airplayserver.network.rtp.RtpPacket; import org.jboss.netty.buffer.ChannelBuffer; import com.raventech.airplayserver.network.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 * @param index the start index * @return the integer, as long to preserve the original sign */ public static long getBeUInt(final ChannelBuffer buffer, final int index) { return ( ((buffer.getByte(index+0) & 0xffL) << 24) | ((buffer.getByte(index+1) & 0xffL) << 16) | ((buffer.getByte(index+2) & 0xffL) << 8) | ((buffer.getByte(index+3) & 0xffL) << 0) ); } /** * Writes an 32-bit unsigned integer to a channel buffer * @param buffer the channel buffer * @param index the start index * @param value the integer, as long to preserve the original sign */ public static void setBeUInt(final ChannelBuffer buffer, final int index, final long value) { assert (value & ~0xffffffffL) == 0; buffer.setByte(index+0, (int)((value & 0xff000000L) >> 24)); buffer.setByte(index+1, (int)((value & 0x00ff0000L) >> 16)); buffer.setByte(index+2, (int)((value & 0x0000ff00L) >> 8)); buffer.setByte(index+3, (int)((value & 0x000000ffL) >> 0)); } /** * Reads an 16-bit unsigned integer from a channel buffer * @param buffer the channel buffer * @param index the start index * @return the short, as int to preserve the original sign */ public static int getBeUInt16(final ChannelBuffer buffer, final int index) { return (int)( ((buffer.getByte(index+0) & 0xffL) << 8) | ((buffer.getByte(index+1) & 0xffL) << 0) ); } /** * Writes an 16-bit unsigned integer to a channel buffer * @param buffer the channel buffer * @param index the start index * @param value the short, as int to preserve the original sign */ public static void setBeUInt16(final ChannelBuffer buffer, final int index, final int value) { assert (value & ~0xffffL) == 0; buffer.setByte(index+0, (int)((value & 0xff00L) >> 8)); buffer.setByte(index+1, (int)((value & 0x00ffL) >> 0)); } /** * Represents an NTP time stamp, i.e. an amount of seconds * since 1900-01-01 00:00:00.000. * * The value is internally represented as a 64-bit fixed * point number with 32 fractional bits. */ public static final class NtpTime { public static final int LENGTH = 8; private final ChannelBuffer buffer; protected NtpTime(final ChannelBuffer buffer) { assert buffer.capacity() == LENGTH; this.buffer = buffer; } public long getSeconds() { return getBeUInt(buffer, 0); } public void setSeconds(final long seconds) { setBeUInt(buffer, 0, seconds); } public long getFraction() { return getBeUInt(buffer, 4); } public void setFraction(final long fraction) { setBeUInt(buffer, 4, fraction); } public double getDouble() { return getSeconds() + (double)getFraction() / 0x100000000L; } public void setDouble(final double v) { setSeconds((long)v); setFraction((long)(0x100000000L * (v - Math.floor(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; protected Timing() { super(LENGTH); setMarker(true); setSequence(7); } protected Timing(final ChannelBuffer buffer, final int minimumSize) throws ProtocolException { super(buffer, minimumSize); } /** * The time at which the {@link TimingRequest} was send. Copied into * {@link TimingResponse} when iTunes/iOS responds to a {@link TimingRequest} * @return */ public NtpTime getReferenceTime() { return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 4, 8)); } /** * The time at which a {@link TimingRequest} was received. Filled out * by iTunes/iOS. * @return */ public NtpTime getReceivedTime() { return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 12, 8)); } /** * The time at which a {@link TimingResponse} was sent as an response * to a {@link TimingRequest} * @return */ public NtpTime getSendTime() { return new NtpTime(getBuffer().slice(RaopRtpPacket.LENGTH + 20, 8)); } @Override public String toString() { final StringBuilder s = new StringBuilder(); s.append(super.toString()); s.append(" "); s.append("ref="); s.append(getReferenceTime().getDouble()); s.append(" "); s.append("recv="); s.append(getReceivedTime().getDouble()); s.append(" "); s.append("send="); s.append(getSendTime().getDouble()); return s.toString(); } } /** * Time synchronization request. * <p> * Sent by the target (AirPort Express/AirReceiver) on the timing channel. * Used to synchronize to the source's clock. *<p> * The sequence number must always be 7, otherwise * at least iOS ignores the packet. */ public static final class TimingRequest extends Timing { public static final byte PAYLOAD_TYPE = 0x52; public TimingRequest() { setPayloadType(PAYLOAD_TYPE); } protected TimingRequest(final ChannelBuffer buffer) throws ProtocolException { super(buffer, LENGTH); } } /** * Time synchronization response. * <p> * Sent by the source (iTunes/iOS) on the timing channel. * Used to synchronize to the source's clock. * <p> * The sequence should match the request's * sequence, which is always 7. */ public static final class TimingResponse extends Timing { public static final byte PAYLOAD_TYPE = 0x53; public TimingResponse() { setPayloadType(PAYLOAD_TYPE); } protected TimingResponse(final ChannelBuffer buffer) throws ProtocolException { super(buffer, LENGTH); } } /** * Synchronization Requests. * <p> * Sent by the source (iTunes/iOs) on the control channel. * Used to translate RTP time stamps (frame time) into the source's time (seconds time) * and from there into the target's time (provided that the two are synchronized using * {@link TimingRequest} and * {@link TimingResponse}. * */ public static final class Sync extends RaopRtpPacket { public static final byte PAYLOAD_TYPE = 0x54; public static final int LENGTH = RaopRtpPacket.LENGTH + 4 + 8 + 4; public Sync() { super(LENGTH); setPayloadType(PAYLOAD_TYPE); } protected Sync(final ChannelBuffer buffer) throws ProtocolException { super(buffer, LENGTH); } /** * Gets the source's RTP time at which the sync packet was send * with the latency taken into account. (i.e. the RTP time stamp * of the packet supposed to be played back at {@link #getTime()}). * @return the source's RTP time corresponding to {@link #getTime()} minus the latency */ public long getTimeStampMinusLatency() { return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH); } /** * Sets the source's RTP time at which the sync packet was send * with the latency taken into account. (i.e. the RTP time stamp * of the packet supposed to be played back at {@link #getTime()}). * @return the source's RTP time corresponding to {@link #getTime()} minus the latency */ public void setTimeStampMinusLatency(final long value) { setBeUInt(getBuffer(), RaopRtpPacket.LENGTH, value); } /** * The source's NTP time at which the sync packet was send * @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)); } /** * Gets the current RTP time (frame time) at which the sync packet was sent, * <b>disregarding</b> the latency. (i.e. approximately the RTP time stamp * of the packet sent at {@link #getTime()}) * @return the source's RTP time corresponding to {@link #getTime()} */ public long getTimeStamp() { return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 8); } /** * Sets the current RTP time (frame time) at which the sync packet was sent, * <b>disregarding</b> the latency. (i.e. approximately the RTP time stamp * of the packet sent at {@link #getTime()}) * @param value the source's RTP time corresponding to {@link #getTime()} */ public void setTimeStamp(final long value) { setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 8, value); } @Override public String toString() { final StringBuilder s = new StringBuilder(); s.append(super.toString()); s.append(" "); s.append("ts-lat="); s.append(getTimeStampMinusLatency()); s.append(" "); s.append("ts="); s.append(getTimeStamp()); s.append(" "); s.append("time="); s.append(getTime().getDouble()); return s.toString(); } } /** * Retransmit request. * <p> * Sent by the target (Airport Express/AirReceiver) on the control channel. Used to let the * source know about missing packets. * <p> * The source is supposed to respond by re-sending the packets with sequence numbers * {@link #getSequenceFirst()} to {@link #getSequenceFirst()} + {@link #getSequenceCount()}. * <p> * The retransmit responses are sent on the <b>control</b> channel, and use packet format * {@link AudioRetransmit} instead of {@link AudioTransmit}. * */ public static final class RetransmitRequest extends RaopRtpPacket { public static final byte PAYLOAD_TYPE = 0x55; public static final int LENGTH = RaopRtpPacket.LENGTH + 4; public RetransmitRequest() { super(LENGTH); setPayloadType(PAYLOAD_TYPE); setMarker(true); setSequence(1); } protected RetransmitRequest(final ChannelBuffer buffer) throws ProtocolException { super(buffer, LENGTH); } /** * Gets the sequence number of the first missing packet * @return sequence number */ public int getSequenceFirst() { return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH); } /** * Sets the sequence number of the first missing packet * @param value sequence number */ public void setSequenceFirst(final int value) { setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH, value); } /** * Gets the number of missing packets * @return number of missing packets */ public int getSequenceCount() { return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2); } /** * Sets the number of missing packets * @param value number of missing packets */ public void setSequenceCount(final int value) { setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2, value); } @Override public String toString() { final StringBuilder s = new StringBuilder(); s.append(super.toString()); s.append(" "); s.append("first="); s.append(getSequenceFirst()); s.append(" "); s.append("count="); s.append(getSequenceCount()); return s.toString(); } } /** * Base class for {@link AudioTransmit} and {@link AudioRetransmit}. */ public static abstract class Audio extends RaopRtpPacket { public Audio(final int length) { super(length); } protected Audio(final ChannelBuffer buffer, final int minimumSize) throws ProtocolException { super(buffer, minimumSize); } /** * Gets the packet's RTP time stamp (frame time) * @return RTP timestamp in frames */ abstract public long getTimeStamp(); /** * Sets the packet's RTP time stamp (frame time) * @param timeStamp RTP timestamp in frames */ abstract public void setTimeStamp(long timeStamp); /** * Unknown, seems to be always zero */ abstract public long getSSrc(); /** * Unknown, seems to be always zero */ abstract public void setSSrc(long sSrc); /** * ChannelBuffer containing the audio data * @return channel buffer containing audio data */ abstract public ChannelBuffer getPayload(); } /** * Audio data transmission. * <p> * Sent by the source (iTunes/iOS) on the audio channel. */ public static final class AudioTransmit extends Audio { public static final byte PAYLOAD_TYPE = 0x60; public static final int LENGTH = RaopRtpPacket.LENGTH + 4 + 4; public AudioTransmit(final int payloadLength) { super(LENGTH + payloadLength); assert payloadLength >= 0; setPayloadType(PAYLOAD_TYPE); } protected AudioTransmit(final ChannelBuffer buffer) throws ProtocolException { super(buffer, LENGTH); } @Override public long getTimeStamp() { return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH); } @Override public void setTimeStamp(final long timeStamp) { setBeUInt(getBuffer(), RaopRtpPacket.LENGTH, timeStamp); } @Override public long getSSrc() { return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4); } @Override public void setSSrc(final long sSrc) { setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4, sSrc); } @Override public ChannelBuffer getPayload() { return getBuffer().slice(LENGTH, getLength() - LENGTH); } @Override public String toString() { final StringBuilder s = new StringBuilder(); s.append(super.toString()); s.append(" "); s.append("ts="); s.append(getTimeStamp()); s.append(" "); s.append("ssrc="); s.append(getSSrc()); s.append(" "); s.append("<"); s.append(getPayload().capacity()); s.append(" bytes payload>"); return s.toString(); } } /** * Audio data re-transmission. * <p> * Sent by the source (iTunes/iOS) on the control channel in response * to {@link RetransmitRequest}. */ public static final class AudioRetransmit extends Audio { 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); assert payloadLength >= 0; setPayloadType(PAYLOAD_TYPE); } protected AudioRetransmit(final ChannelBuffer buffer) throws ProtocolException { super(buffer, LENGTH); } /** * First two bytes after RTP header */ public int getUnknown2Bytes() { return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH); } /** * First two bytes after RTP header */ public void setUnknown2Bytes(final int b) { setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH, b); } /** * This is to be the sequence of the original * packet (i.e., the sequence we requested to be * retransmitted). */ public int getOriginalSequence() { return getBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2); } /** * This seems is the sequence of the original * packet (i.e., the sequence we requested to be * retransmitted). */ public void setOriginalSequence(final int seq) { setBeUInt16(getBuffer(), RaopRtpPacket.LENGTH + 2, seq); } @Override public long getTimeStamp() { return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4); } @Override public void setTimeStamp(final long timeStamp) { setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4, timeStamp); } @Override public long getSSrc() { return getBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 4); } @Override public void setSSrc(final long sSrc) { setBeUInt(getBuffer(), RaopRtpPacket.LENGTH + 4 + 4, sSrc); } @Override public ChannelBuffer getPayload() { return getBuffer().slice(LENGTH, getLength() - LENGTH); } @Override public String toString() { final StringBuilder s = new StringBuilder(); s.append(super.toString()); s.append(" "); s.append("?="); s.append(getUnknown2Bytes()); s.append(" "); s.append("oseq="); s.append(getOriginalSequence()); s.append(" "); s.append("ts="); s.append(getTimeStamp()); s.append(" "); s.append("ssrc="); s.append(getSSrc()); s.append(" "); s.append("<"); s.append(getPayload().capacity()); s.append(" bytes payload>"); return s.toString(); } } /** * Creates an RTP packet from a {@link ChannelBuffer}, using the * sub-class of {@link RaopRtpPacket} indicated by the packet's * {@link #getPayloadType()} * * @param buffer ChannelBuffer containing the packet * @return Instance of one of the sub-classes of {@link RaopRtpPacket} * @throws ProtocolException if the packet is invalid. */ public static RaopRtpPacket decode(final ChannelBuffer buffer) throws ProtocolException { 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.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()); } } protected RaopRtpPacket(final int length) { super(length); setVersion((byte)2); } protected RaopRtpPacket(final ChannelBuffer buffer, final int minimumSize) throws ProtocolException { super(buffer, minimumSize); } protected RaopRtpPacket(final ChannelBuffer buffer) throws ProtocolException { super(buffer); } }