diff --git a/moonlight-common/.classpath b/moonlight-common/.classpath
index 62423bd8..db1e0181 100644
--- a/moonlight-common/.classpath
+++ b/moonlight-common/.classpath
@@ -3,5 +3,6 @@
+
diff --git a/moonlight-common/libs/tinyrtsp.jar b/moonlight-common/libs/tinyrtsp.jar
new file mode 100644
index 00000000..45086d58
Binary files /dev/null and b/moonlight-common/libs/tinyrtsp.jar differ
diff --git a/moonlight-common/src/com/limelight/nvstream/Handshake.java b/moonlight-common/src/com/limelight/nvstream/Handshake.java
deleted file mode 100644
index e3249af9..00000000
--- a/moonlight-common/src/com/limelight/nvstream/Handshake.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package com.limelight.nvstream;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-
-public class Handshake {
- public static final int PORT = 47991;
-
- public static final int HANDSHAKE_TIMEOUT = 5000;
-
- public static final byte[] PLATFORM_HELLO =
- {
- (byte)0x07,
- (byte)0x00,
- (byte)0x00,
- (byte)0x00,
-
- // android in ASCII
- (byte)0x61,
- (byte)0x6e,
- (byte)0x64,
- (byte)0x72,
- (byte)0x6f,
- (byte)0x69,
- (byte)0x64,
-
- (byte)0x03,
- (byte)0x01,
- (byte)0x00,
- (byte)0x00
- };
-
- public static final byte[] PACKET_2 =
- {
- (byte)0x01,
- (byte)0x03,
- (byte)0x02,
- (byte)0x00,
- (byte)0x08,
- (byte)0x00
- };
-
- public static final byte[] PACKET_3 =
- {
- (byte)0x04,
- (byte)0x01,
- (byte)0x00,
- (byte)0x00,
-
- (byte)0x00,
- (byte)0x00,
- (byte)0x00,
- (byte)0x00
- };
-
- public static final byte[] PACKET_4 =
- {
- (byte)0x01,
- (byte)0x01,
- (byte)0x00,
- (byte)0x00
- };
-
- private static boolean waitAndDiscardResponse(InputStream in)
- {
- // Wait for response and discard response
- try {
- in.read();
-
- // Wait for the full response to come in
- Thread.sleep(250);
-
- for (int i = 0; i < in.available(); i++)
- in.read();
-
- } catch (IOException e1) {
- return false;
- } catch (InterruptedException e) {
- return false;
- }
-
- return true;
- }
-
- public static boolean performHandshake(InetAddress host) throws IOException
- {
- Socket s = new Socket();
- s.connect(new InetSocketAddress(host, PORT), HANDSHAKE_TIMEOUT);
- s.setSoTimeout(HANDSHAKE_TIMEOUT);
- OutputStream out = s.getOutputStream();
- InputStream in = s.getInputStream();
-
- // First packet
- out.write(PLATFORM_HELLO);
- out.flush();
-
- if (!waitAndDiscardResponse(in)) {
- s.close();
- return false;
- }
-
- // Second packet
- out.write(PACKET_2);
- out.flush();
-
- if (!waitAndDiscardResponse(in)) {
- s.close();
- return false;
- }
-
- // Third packet
- out.write(PACKET_3);
- out.flush();
-
- if (!waitAndDiscardResponse(in)) {
- s.close();
- return false;
- }
-
- // Fourth packet
- out.write(PACKET_4);
- out.flush();
-
- // Done
- s.close();
-
- return true;
- }
-}
diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/com/limelight/nvstream/NvConnection.java
index e0cac6ca..ee0073a0 100644
--- a/moonlight-common/src/com/limelight/nvstream/NvConnection.java
+++ b/moonlight-common/src/com/limelight/nvstream/NvConnection.java
@@ -22,6 +22,7 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.input.NvController;
+import com.limelight.nvstream.rtsp.RtspConnection;
public class NvConnection {
private String host;
@@ -190,9 +191,16 @@ public class NvConnection {
return true;
}
+ private boolean doRtspHandshake() throws IOException
+ {
+ RtspConnection r = new RtspConnection(hostAddr);
+ r.doRtspHandshake(config);
+ return true;
+ }
+
private boolean startControlStream() throws IOException
{
- controlStream = new ControlStream(hostAddr, listener, config);
+ controlStream = new ControlStream(hostAddr, listener);
controlStream.initialize();
controlStream.start();
return true;
@@ -237,8 +245,8 @@ public class NvConnection {
success = startSteamBigPicture();
break;
- case HANDSHAKE:
- success = Handshake.performHandshake(hostAddr);
+ case RTSP_HANDSHAKE:
+ success = doRtspHandshake();
break;
case CONTROL_START:
diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnectionListener.java b/moonlight-common/src/com/limelight/nvstream/NvConnectionListener.java
index f7b80f82..c96f1d0a 100644
--- a/moonlight-common/src/com/limelight/nvstream/NvConnectionListener.java
+++ b/moonlight-common/src/com/limelight/nvstream/NvConnectionListener.java
@@ -4,7 +4,7 @@ public interface NvConnectionListener {
public enum Stage {
LAUNCH_APP("app"),
- HANDSHAKE("handshake"),
+ RTSP_HANDSHAKE("RTSP handshake"),
CONTROL_START("control connection"),
VIDEO_START("video stream"),
AUDIO_START("audio stream"),
diff --git a/moonlight-common/src/com/limelight/nvstream/control/ByteConfigTuple.java b/moonlight-common/src/com/limelight/nvstream/control/ByteConfigTuple.java
deleted file mode 100644
index 78b6a253..00000000
--- a/moonlight-common/src/com/limelight/nvstream/control/ByteConfigTuple.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.limelight.nvstream.control;
-
-public class ByteConfigTuple extends ConfigTuple {
- public static final short PAYLOAD_LENGTH = 1;
-
- public byte payload;
-
- public ByteConfigTuple(short packetType, byte payload) {
- super(packetType, PAYLOAD_LENGTH);
- this.payload = payload;
- }
-
- @Override
- public byte[] payloadToWire() {
- return new byte[] {payload};
- }
-}
diff --git a/moonlight-common/src/com/limelight/nvstream/control/Config.java b/moonlight-common/src/com/limelight/nvstream/control/Config.java
deleted file mode 100644
index 3339125a..00000000
--- a/moonlight-common/src/com/limelight/nvstream/control/Config.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package com.limelight.nvstream.control;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-
-import com.limelight.nvstream.StreamConfiguration;
-
-public class Config {
-
- public static final ConfigTuple[] CONFIG_720_60 =
- {
- new ByteConfigTuple((short)0x1207, (byte)1), //iFrameOnDemand
- new IntConfigTuple((short)0x120b, 7), //averageBitrate
- new IntConfigTuple((short)0x120c, 7), //peakBitrate
- new IntConfigTuple((short)0x120d, 60), //gopLength
- new IntConfigTuple((short)0x120e, 100), //vbvMultiplier
- new IntConfigTuple((short)0x120f, 5), //rateControlMode
- new IntConfigTuple((short)0x1210, 4), //slicesPerFrame
- new IntConfigTuple((short)0x1202, 1024), //packetSize
- new ByteConfigTuple((short)0x1203, (byte)0), //recordServerStats
- new ByteConfigTuple((short)0x1201, (byte)0), //serverCapture
- new ByteConfigTuple((short)0x1234, (byte)0), //serverNetworkCapture
- new ByteConfigTuple((short)0x1248, (byte)0),
- new ByteConfigTuple((short)0x1208, (byte)1), //refPicInvalidation
- new ByteConfigTuple((short)0x1209, (byte)0), //enableFrameRateCtrl
- new IntConfigTuple((short)0x1212, 3000), //pingBackIntervalMs
- new IntConfigTuple((short)0x1238, 10000), //pingBackTimeoutMs
- new ByteConfigTuple((short)0x1211, (byte)0), //enableSubframeEncoding
- new ByteConfigTuple((short)0x1213, (byte)1), //videoQoSFecEnable
- new IntConfigTuple((short)0x1214, 50), //videoQoSFecNumSrcPackets
- new IntConfigTuple((short)0x1215, 60), //videoQoSFecNumOutPackets
- new IntConfigTuple((short)0x1216, 20), //videoQoSFecRepairPercent
- new IntConfigTuple((short)0x1217, 0), //videoQoSTsEnable
- new IntConfigTuple((short)0x1218, 8), //videoQoSTsAverageBitrate
- new IntConfigTuple((short)0x1219, 10), //videoQoSTsMaximumBitrate
- new IntConfigTuple((short)0x121a, 311), //videoQoSBwFlags
- new IntConfigTuple((short)0x121b, 10000), //videoQoSBwMaximumBitrate
- new IntConfigTuple((short)0x121c, 2000), //videoQoSBwMinimumBitrate
- new IntConfigTuple((short)0x121d, 50), //videoQoSBwStatsTime
- new IntConfigTuple((short)0x121e, 3000), //videoQoSBwZeroLossCount
- new IntConfigTuple((short)0x121f, 2), //videoQoSBwLossThreshold
- new IntConfigTuple((short)0x122a, 5000), //videoQoSBwOwdThreshold
- new IntConfigTuple((short)0x122b, 500), //videoQoSBwOwdReference
- new IntConfigTuple((short)0x1220, 75), //videoQoSBwLossWaitTime
- new IntConfigTuple((short)0x1221, 25), //videoQoSBwRateDropMultiplier
- new IntConfigTuple((short)0x1222, 10), //videoQoSBwRateGainMultiplier
- new IntConfigTuple((short)0x1223, 60), //videoQoSBwMaxFps
- new IntConfigTuple((short)0x1224, 30), //videoQoSBwMinFps
- new IntConfigTuple((short)0x1225, 3), //videoQoSBwFpsThreshold
- new IntConfigTuple((short)0x1226, 1000), //videoQoSBwJitterThreshold
- new IntConfigTuple((short)0x1227, 5000), //videoQoSBwJitterWaitTime
- new IntConfigTuple((short)0x1228, 5000), //videoQoSBwNoJitterWaitTime
- new IntConfigTuple((short)0x124e, 110),
- new IntConfigTuple((short)0x1237, 10), //videoQoSBwEarlyDetectionEnableL1Threshold
- new IntConfigTuple((short)0x1236, 6), //videoQoSBwEarlyDetectionEnableL0Threshold
- new IntConfigTuple((short)0x1235, 4), //videoQoSBwEarlyDetectionDisableThreshold
- new IntConfigTuple((short)0x1242, 20000), //videoQoSBwEarlyDetectionWaitTime
- new IntConfigTuple((short)0x1244, 100),
- new IntConfigTuple((short)0x1245, 1000),
- new IntConfigTuple((short)0x1246, 720),
- new IntConfigTuple((short)0x1247, 480),
- new IntConfigTuple((short)0x1229, 5000), //videoQosVideoQualityScoreUpdateTime
- new ByteConfigTuple((short)0x122e, (byte)7), //videoQosTrafficType
- new IntConfigTuple((short)0x1231, 40), //videoQosBnNotifyUpBoundThreshold
- new IntConfigTuple((short)0x1232, 25), //videoQosBnNotifyLowBoundThreshold
- new IntConfigTuple((short)0x1233, 3000), //videoQosBnNotifyWaitTime
- new IntConfigTuple((short)0x122c, 3), //videoQosInvalidateThreshold
- new IntConfigTuple((short)0x122d, 10), //videoQosInvalidateSkipPercentage
- /*new IntConfigTuple((short)0x123b, 12),
- new IntConfigTuple((short)0x123c, 3),
- new IntConfigTuple((short)0x1249, 0),
- new IntConfigTuple((short)0x124a, 4000),
- new IntConfigTuple((short)0x124b, 5000),
- new IntConfigTuple((short)0x124c, 6000),
- new IntConfigTuple((short)0x124d, 1000),*/
- new IntConfigTuple((short)0x122f, 0), //riSecurityProtocol
- new ShortConfigTuple((short)0x1230, (short)0), //riSecInfoUsePredefinedCert
- new IntConfigTuple((short)0x1239, 0), //videoFrameDropIntervalNumber
- new IntConfigTuple((short)0x123a, 0), //videoFrameDropContinualNumber
- new IntConfigTuple((short)0x123d, 96000), //audioQosBitRate
- new IntConfigTuple((short)0x123e, 5), //audioQosPacketDuration
- new IntConfigTuple((short)0x123f, 1), //audioQosEnablePacketLossPercentage
- new IntConfigTuple((short)0x1243, 100) //audioQosPacketLossPercentageUpdateInterval
- };
-
- public static final ConfigTuple[] CONFIG_1080_30_DIFF =
- {
- new IntConfigTuple((short)0x120b, 10), //averageBitrate
- new IntConfigTuple((short)0x120c, 10), //peakBitrate
-
- // HACK: Streaming 1080p30 without these options causes the encoder
- // to step down to 720p which breaks the CPU decoder
- new IntConfigTuple((short)0x121b, 25000), //videoQoSBwMaximumBitrate
- new IntConfigTuple((short)0x121c, 25000), //videoQoSBwMinimumBitrate
-
- new IntConfigTuple((short)0x1246, 1280),
- new IntConfigTuple((short)0x1247, 720),
- /*new IntConfigTuple((short)0x124a, 5000),
- new IntConfigTuple((short)0x124c, 7000),*/
- };
-
- public static final ConfigTuple[] CONFIG_1080_60_DIFF =
- {
- new IntConfigTuple((short)0x120b, 30), //averageBitrate
- new IntConfigTuple((short)0x120c, 30), //peakBitrate
- new IntConfigTuple((short)0x120f, 4), //rateControlMode
- new IntConfigTuple((short)0x121b, 30000), //videoQoSBwMaximumBitrate
- new IntConfigTuple((short)0x121c, 25000), //videoQoSBwMinimumBitrate
- new IntConfigTuple((short)0x1245, 3000),
- new IntConfigTuple((short)0x1246, 1280),
- new IntConfigTuple((short)0x1247, 720),
- /*new IntConfigTuple((short)0x124a, 5000),
- new IntConfigTuple((short)0x124c, 7000),*/
- };
-
- private StreamConfiguration streamConfig;
-
- public Config(StreamConfiguration streamConfig) {
- this.streamConfig = streamConfig;
- }
-
- private void updateSetWithConfig(ArrayList set, ConfigTuple[] config)
- {
- for (ConfigTuple tuple : config)
- {
- int i;
-
- for (i = 0; i < set.size(); i++) {
- ConfigTuple existingTuple = set.get(i);
- if (existingTuple.packetType == tuple.packetType) {
- set.remove(i);
- set.add(i, tuple);
- break;
- }
- }
-
- if (i == set.size()) {
- set.add(tuple);
- }
- }
- }
-
- private int getConfigOnWireSize(ArrayList tupleSet)
- {
- int size = 0;
-
- for (ConfigTuple t : tupleSet)
- {
- size += ConfigTuple.HEADER_LENGTH + t.payloadLength;
- }
-
- return size;
- }
-
- private ArrayList generateTupleSet() {
- ArrayList tupleSet = new ArrayList();
-
- tupleSet.add(new IntConfigTuple((short)0x1204, streamConfig.getWidth()));
- tupleSet.add(new IntConfigTuple((short)0x1205, streamConfig.getHeight()));
- tupleSet.add(new IntConfigTuple((short)0x1206, 1)); //videoTransferProtocol
- tupleSet.add(new IntConfigTuple((short)0x120A, streamConfig.getRefreshRate()));
-
- // Start with the initial config for 720p60
- updateSetWithConfig(tupleSet, CONFIG_720_60);
-
- if (streamConfig.getWidth() >= 1920 &&
- streamConfig.getHeight() >= 1080)
- {
- if (streamConfig.getRefreshRate() >= 60)
- {
- // Update the initial set with the changed 1080p60 options
- updateSetWithConfig(tupleSet, CONFIG_1080_60_DIFF);
- }
- else
- {
- // Update the initial set with the changed 1080p30 options
- updateSetWithConfig(tupleSet, CONFIG_1080_30_DIFF);
- }
- }
-
- return tupleSet;
- }
-
- public byte[] toWire() {
- ArrayList tupleSet = generateTupleSet();
- ByteBuffer bb = ByteBuffer.allocate(getConfigOnWireSize(tupleSet) + 4).order(ByteOrder.LITTLE_ENDIAN);
-
- for (ConfigTuple t : tupleSet)
- {
- bb.put(t.toWire());
- }
-
- // Config tail
- bb.putShort((short) 0x13fe);
- bb.putShort((short) 0x00);
-
- return bb.array();
- }
-}
diff --git a/moonlight-common/src/com/limelight/nvstream/control/ConfigTuple.java b/moonlight-common/src/com/limelight/nvstream/control/ConfigTuple.java
deleted file mode 100644
index ffd4457a..00000000
--- a/moonlight-common/src/com/limelight/nvstream/control/ConfigTuple.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.limelight.nvstream.control;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-public abstract class ConfigTuple {
- public short packetType;
- public short payloadLength;
-
- public static final short HEADER_LENGTH = 4;
-
- public ConfigTuple(short packetType, short payloadLength)
- {
- this.packetType = packetType;
- this.payloadLength = payloadLength;
- }
-
- public abstract byte[] payloadToWire();
-
- public byte[] toWire()
- {
- byte[] payload = payloadToWire();
- ByteBuffer bb = ByteBuffer.allocate(HEADER_LENGTH + (payload != null ? payload.length : 0))
- .order(ByteOrder.LITTLE_ENDIAN);
-
- bb.putShort(packetType);
- bb.putShort(payloadLength);
-
- if (payload != null) {
- bb.put(payload);
- }
-
- return bb.array();
- }
-
- @Override
- public int hashCode()
- {
- return packetType;
- }
-
- @Override
- public boolean equals(Object o)
- {
- // We only compare the packet types on purpose
- if (o instanceof ConfigTuple) {
- return ((ConfigTuple)o).packetType == packetType;
- }
- else {
- return false;
- }
- }
-}
diff --git a/moonlight-common/src/com/limelight/nvstream/control/IntConfigTuple.java b/moonlight-common/src/com/limelight/nvstream/control/IntConfigTuple.java
deleted file mode 100644
index 9fe43c14..00000000
--- a/moonlight-common/src/com/limelight/nvstream/control/IntConfigTuple.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.limelight.nvstream.control;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-public class IntConfigTuple extends ConfigTuple {
-
- public static final short PAYLOAD_LENGTH = 4;
-
- public int payload;
-
- public IntConfigTuple(short packetType, int payload) {
- super(packetType, PAYLOAD_LENGTH);
- this.payload = payload;
- }
-
- @Override
- public byte[] payloadToWire() {
- ByteBuffer bb = ByteBuffer.allocate(PAYLOAD_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
- bb.putInt(payload);
- return bb.array();
- }
-}
diff --git a/moonlight-common/src/com/limelight/nvstream/control/ShortConfigTuple.java b/moonlight-common/src/com/limelight/nvstream/control/ShortConfigTuple.java
deleted file mode 100644
index 7f684ca0..00000000
--- a/moonlight-common/src/com/limelight/nvstream/control/ShortConfigTuple.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.limelight.nvstream.control;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-public class ShortConfigTuple extends ConfigTuple {
-
- public static final short PAYLOAD_LENGTH = 2;
-
- public short payload;
-
- public ShortConfigTuple(short packetType, short payload) {
- super(packetType, PAYLOAD_LENGTH);
- this.payload = payload;
- }
-
- @Override
- public byte[] payloadToWire() {
- ByteBuffer bb = ByteBuffer.allocate(PAYLOAD_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
- bb.putShort(payload);
- return bb.array();
- }
-}
\ No newline at end of file
diff --git a/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java b/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java
new file mode 100644
index 00000000..fdca46db
--- /dev/null
+++ b/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java
@@ -0,0 +1,144 @@
+package com.limelight.nvstream.rtsp;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.HashMap;
+
+import com.limelight.nvstream.StreamConfiguration;
+import com.tinyrtsp.rtsp.message.RtspMessage;
+import com.tinyrtsp.rtsp.message.RtspRequest;
+import com.tinyrtsp.rtsp.message.RtspResponse;
+import com.tinyrtsp.rtsp.parser.RtspStream;
+
+public class RtspConnection {
+ public static final int PORT = 48010;
+ public static final int RTSP_TIMEOUT = 5000;
+
+ // SHIELD Update 77
+ public static final int CLIENT_VERSION = 9;
+
+ private int sequenceNumber = 1;
+ private int sessionId = 0;
+
+ private String host;
+
+ public RtspConnection(InetAddress host) {
+ if (host instanceof Inet6Address) {
+ // RFC2732-formatted IPv6 address for use in URL
+ this.host = "["+host.getHostAddress()+"]";
+ }
+ else {
+ this.host = host.getHostAddress();
+ }
+ }
+
+ private RtspRequest createRtspRequest(String command, String target) {
+ RtspRequest m = new RtspRequest(command, target, "RTSP/1.0",
+ sequenceNumber++, new HashMap(), null);
+ m.setOption("X-GS-ClientVersion", ""+CLIENT_VERSION);
+ return m;
+ }
+
+ private RtspResponse transactRtspMessage(RtspMessage m) throws IOException {
+ Socket s = new Socket();
+ try {
+ s.setTcpNoDelay(true);
+ s.connect(new InetSocketAddress(host, PORT), RTSP_TIMEOUT);
+
+ RtspStream rtspStream = new RtspStream(s.getInputStream(), s.getOutputStream());
+ try {
+ rtspStream.write(m);
+ return (RtspResponse) rtspStream.read();
+ } finally {
+ rtspStream.close();
+ }
+ } finally {
+ s.close();
+ }
+ }
+
+ private RtspResponse requestOptions() throws IOException {
+ RtspRequest m = createRtspRequest("OPTIONS", "rtsp://"+host);
+ return transactRtspMessage(m);
+ }
+
+ private RtspResponse requestDescribe() throws IOException {
+ RtspRequest m = createRtspRequest("DESCRIBE", "rtsp://"+host);
+ m.setOption("Accept", "application/sdp");
+ m.setOption("If-Modified-Since", "Thu, 01 Jan 1970 00:00:00 GMT");
+ return transactRtspMessage(m);
+ }
+
+ private RtspResponse setupStream(String streamName) throws IOException {
+ RtspRequest m = createRtspRequest("SETUP", "streamid="+streamName);
+ if (sessionId != 0) {
+ m.setOption("Session", ""+sessionId);
+ }
+ m.setOption("Transport", " ");
+ m.setOption("If-Modified-Since", "Thu, 01 Jan 1970 00:00:00 GMT");
+ return transactRtspMessage(m);
+ }
+
+ private RtspResponse playStream(String streamName) throws IOException {
+ RtspRequest m = createRtspRequest("PLAY", "streamid="+streamName);
+ m.setOption("Session", ""+sessionId);
+ return transactRtspMessage(m);
+ }
+
+ private RtspResponse sendVideoAnnounce(StreamConfiguration sc) throws IOException {
+ RtspRequest m = createRtspRequest("ANNOUNCE", "streamid=video");
+ m.setOption("Session", ""+sessionId);
+ m.setOption("Content-type", "application/sdp");
+ // FIXME: IP jank
+ m.setPayload(SdpGenerator.generateSdpFromConfig(InetAddress.getByName(host), sc));
+ m.setOption("Content-length", ""+m.getPayload().length());
+ return transactRtspMessage(m);
+ }
+
+ public void doRtspHandshake(StreamConfiguration sc) throws IOException {
+ RtspResponse r;
+
+ r = requestOptions();
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP OPTIONS request failed: "+r.getStatusCode());
+ }
+
+ r = requestDescribe();
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP DESCRIBE request failed: "+r.getStatusCode());
+ }
+
+ r = setupStream("audio");
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP SETUP request failed: "+r.getStatusCode());
+ }
+
+ try {
+ sessionId = Integer.parseInt(r.getOption("Session"));
+ } catch (NumberFormatException e) {
+ throw new IOException("RTSP SETUP response was malformed");
+ }
+
+ r = setupStream("video");
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP SETUP request failed: "+r.getStatusCode());
+ }
+
+ r = sendVideoAnnounce(sc);
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP ANNOUNCE request failed: "+r.getStatusCode());
+ }
+
+ r = playStream("video");
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP PLAY request failed: "+r.getStatusCode());
+ }
+ r = playStream("audio");
+ if (r.getStatusCode() != 200) {
+ throw new IOException("RTSP PLAY request failed: "+r.getStatusCode());
+ }
+ }
+}
diff --git a/moonlight-common/src/com/limelight/nvstream/rtsp/SdpGenerator.java b/moonlight-common/src/com/limelight/nvstream/rtsp/SdpGenerator.java
new file mode 100644
index 00000000..e1796d85
--- /dev/null
+++ b/moonlight-common/src/com/limelight/nvstream/rtsp/SdpGenerator.java
@@ -0,0 +1,236 @@
+package com.limelight.nvstream.rtsp;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+
+import com.limelight.nvstream.StreamConfiguration;
+
+public class SdpGenerator {
+ private static void addSessionAttributeBytes(StringBuilder config, String attribute, byte[] value) {
+ try {
+ addSessionAttribute(config, attribute, new String(value, "IBM437"));
+ } catch (UnsupportedEncodingException e) {}
+ }
+
+ private static void addSessionAttributeInts(StringBuilder config, String attribute, int[] value) {
+ ByteBuffer b = ByteBuffer.allocate(value.length * 4).order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int val : value) {
+ b.putInt(val);
+ }
+
+ addSessionAttributeBytes(config, attribute, b.array());
+ }
+
+ private static void addSessionAttributeInt(StringBuilder config, String attribute, int value) {
+ ByteBuffer b = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
+ b.putInt(value);
+ addSessionAttributeBytes(config, attribute, b.array());
+ }
+
+ private static void addSessionAttribute(StringBuilder config, String attribute, String value) {
+ config.append("a="+attribute+":"+value+" \r\n");
+ }
+
+ public static String generateSdpFromConfig(InetAddress host, StreamConfiguration sc) {
+ StringBuilder config = new StringBuilder();
+ config.append("v=0").append("\r\n"); // SDP Version 0
+ config.append("o=android 0 9 IN ");
+ if (host instanceof Inet6Address) {
+ config.append("IPv6 ");
+ }
+ else {
+ config.append("IPv4 ");
+ }
+ config.append(host.getHostAddress());
+ config.append("\r\n");
+ config.append("s=NVIDIA Streaming Client").append("\r\n");
+
+ addSessionAttributeBytes(config, "x-nv-callbacks", new byte[] {
+ 0x50, 0x51, 0x49, 0x4a, 0x0d,
+ (byte)0xad, 0x30, 0x4a, (byte)0xf1, (byte)0xbd, 0x30, 0x4a, (byte)0xd5,
+ (byte)0xac, 0x30, 0x4a, 0x21, (byte)0xbc, 0x30, 0x4a, (byte)0xc1,
+ (byte)0xbb, 0x30, 0x4a, 0x7d, (byte)0xbb, 0x30, 0x4a, 0x19,
+ (byte)0xbb, 0x30, 0x4a, 0x00, 0x00, 0x00, 0x00
+ });
+ addSessionAttributeBytes(config, "x-nv-videoDecoder", new byte[] {
+ 0x50, 0x51, 0x49, 0x4a, 0x65, (byte)0xad, 0x30, 0x4a, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte)0xd1, (byte)0xac, 0x30,
+ 0x4a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x4d, (byte)0xad, 0x30, 0x4a
+ });
+ addSessionAttributeBytes(config, "x-nv-audioRenderer", new byte[] {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ });
+
+ addSessionAttribute(config, "x-nv-general.serverAddress", host.getHostAddress());
+
+ addSessionAttributeInts(config, "x-nv-general.serverPorts", new int[] {
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
+
+ 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
+ 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
+ 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff
+ });
+
+ addSessionAttribute(config, "x-nv-general.videoSyncAudioDelayAdjust", "10000");
+ addSessionAttribute(config, "x-nv-general.startTime", "0");
+ addSessionAttributeInt(config, "x-nv-general.featureFlags", 0xffffffff);
+ addSessionAttribute(config, "x-nv-general.userIdleWarningTimeout", "0");
+ addSessionAttribute(config, "x-nv-general.userIdleSessionTimeout", "0");
+ addSessionAttribute(config, "x-nv-general.serverCapture", "0");
+ addSessionAttribute(config, "x-nv-general.clientCapture", "0");
+ addSessionAttribute(config, "x-nv-general.rtpQueueMaxPackets", "16");
+ addSessionAttribute(config, "x-nv-general.rtpQueueMaxDurationMs", "40");
+ addSessionAttribute(config, "x-nv-general.useRtspClient", "257");
+
+ addSessionAttribute(config, "x-nv-video[0].clientViewportWd", ""+sc.getWidth());
+ addSessionAttribute(config, "x-nv-video[0].clientViewportHt", ""+sc.getHeight());
+ addSessionAttribute(config, "x-nv-video[0].adapterNumber", "0");
+ addSessionAttribute(config, "x-nv-video[0].maxFPS", "30");
+ addSessionAttribute(config, "x-nv-video[0].iFrameOnDemand", "1");
+
+ // FIXME: Handle other settings
+ addSessionAttributeInt(config, "x-nv-video[0].transferProtocol", 1);
+ addSessionAttributeInt(config, "x-nv-video[0].rateControlMode", 5);
+ addSessionAttribute(config, "x-nv-video[0].averageBitrate", "7");
+ addSessionAttribute(config, "x-nv-video[0].peakBitrate", "7");
+
+ addSessionAttribute(config, "x-nv-video[0].gopLength", "60");
+ addSessionAttribute(config, "x-nv-video[0].vbvMultiplier", "100");
+ addSessionAttribute(config, "x-nv-video[0].slicesPerFrame", "4");
+ addSessionAttribute(config, "x-nv-video[0].numTemporalLayers", "0");
+ addSessionAttribute(config, "x-nv-video[0].packetSize", "1024");
+ addSessionAttribute(config, "x-nv-video[0].enableSubframeEncoding", "0");
+ addSessionAttribute(config, "x-nv-video[0].refPicInvalidation", "1");
+ addSessionAttribute(config, "x-nv-video[0].pingBackIntervalMs", "3000");
+ addSessionAttribute(config, "x-nv-video[0].pingBackTimeoutMs", "10000");
+ addSessionAttribute(config, "x-nv-video[0].timeoutLengthMs", "7000");
+ addSessionAttribute(config, "x-nv-video[0].fullFrameAssembly", "1");
+ addSessionAttribute(config, "x-nv-video[0].decodeIncompleteFrames", "0");
+ addSessionAttribute(config, "x-nv-video[0].enableIntraRefresh", "0");
+ addSessionAttribute(config, "x-nv-video[0].enableLongTermReferences", "0");
+ addSessionAttribute(config, "x-nv-video[0].enableFrameRateCtrl", "0");
+ addSessionAttribute(config, "x-nv-video[0].rtpDynamicPort", "0");
+ addSessionAttribute(config, "x-nv-video[0].framesWithInvalidRefThreshold", "0");
+ addSessionAttribute(config, "x-nv-video[0].consecutiveFrameLostThreshold", "0");
+
+ addSessionAttribute(config, "x-nv-vqos[0].ts.enable", "0");
+
+ // FIXME: Handle other settings
+ addSessionAttribute(config, "x-nv-vqos[0].ts.averageBitrate", "8");
+ addSessionAttribute(config, "x-nv-vqos[0].ts.maximumBitrate", "10");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.flags", "823");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.maximumBitrate", "10000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "2000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.statsTime", "50");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.zeroLossCount", "3000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.lossThreshold", "2");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.owdThreshold", "5000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.owdReference", "500");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.lossWaitTime", "75");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.rateDropMultiplier", "25");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.rateGainMultiplier", "10");
+
+ // FIXME: Other settings?
+ addSessionAttribute(config, "x-nv-vqos[0].bw.maxFps", "60");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.minFps", "30");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.fpsThreshold", "3");
+
+ addSessionAttribute(config, "x-nv-vqos[0].bw.jitterThreshold", "1000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.jitterWaitTime", "5000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.noJitterWaitTime", "5000");
+
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionEnableBitRatePercentThreshold", "110");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionEnableL1Threshold", "10");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionEnableL0Threshold", "6");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionDisableThreshold", "4");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionDisableWaitTime", "20000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionDisableWaitPercent", "100");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionLowerBoundRate", "1000");
+
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionLowerBoundWidth", "720");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.earlyDetectionLowerBoundHeight", "480");
+
+ addSessionAttribute(config, "x-nv-vqos[0].bw.pf.enableFlags", "3");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.pf.lowBitrate30FpsThreshold", "4000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.pf.lowBitrate60FpsThreshold", "5000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.pf.highBitrateThreshold", "6000");
+ addSessionAttribute(config, "x-nv-vqos[0].bw.pf.bitrateStepSize", "1000");
+ addSessionAttribute(config, "x-nv-vqos[0].bn.notifyUpBoundThreshold", "40");
+ addSessionAttribute(config, "x-nv-vqos[0].bn.notifyLowBoundThreshold", "25");
+ addSessionAttribute(config, "x-nv-vqos[0].bn.notifyWaitTime", "3000");
+ addSessionAttribute(config, "x-nv-vqos[0].fec.enable", "1");
+ addSessionAttribute(config, "x-nv-vqos[0].fec.numSrcPackets", "50");
+ addSessionAttribute(config, "x-nv-vqos[0].fec.numOutPackets", "60");
+ addSessionAttribute(config, "x-nv-vqos[0].fec.repairPercent", "20");
+ addSessionAttribute(config, "x-nv-vqos[0].pictureRefreshIntervalMs", "0");
+ addSessionAttribute(config, "x-nv-vqos[0].videoQualityScoreUpdateTime", "5000");
+ addSessionAttribute(config, "x-nv-vqos[0].invalidateThreshold", "3");
+ addSessionAttribute(config, "x-nv-vqos[0].invalidateSkipPercentage", "10");
+ addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "7");
+ addSessionAttribute(config, "x-nv-vqos[0].videoQoSMaxRoundTripLatencyFrames", "12");
+ addSessionAttribute(config, "x-nv-vqos[0].videoQoSMaxConsecutiveDrops", "3");
+ addSessionAttributeInt(config, "x-nv-vqos[0].profile", 0);
+
+ addSessionAttributeInt(config, "x-nv-aqos.mode", 1);
+ addSessionAttribute(config, "x-nv-aqos.enableAudioStats", "1");
+ addSessionAttribute(config, "x-nv-aqos.audioStatsUpdateIntervalMs", "70");
+ addSessionAttribute(config, "x-nv-aqos.enablePacketLossPercentage", "1");
+ addSessionAttribute(config, "x-nv-aqos.bitRate", "96000");
+ addSessionAttribute(config, "x-nv-aqos.packetDuration", "5");
+ addSessionAttribute(config, "x-nv-aqos.packetLossPercentageUpdateIntervalMs", "100");
+ addSessionAttribute(config, "x-nv-aqos.qosTrafficType", "4");
+
+ addSessionAttribute(config, "x-nv-runtime.recordClientStats", "8");
+ addSessionAttribute(config, "x-nv-runtime.recordServerStats", "0");
+ addSessionAttribute(config, "x-nv-runtime.clientNetworkCapture", "0");
+ addSessionAttribute(config, "x-nv-runtime.clientTraceCapture", "0");
+ addSessionAttribute(config, "x-nv-runtime.serverNetworkCapture", "0");
+ addSessionAttribute(config, "x-nv-runtime.serverTraceCapture", "0");
+
+ addSessionAttributeInt(config, "x-nv-ri.protocol", 0);
+ addSessionAttribute(config, "x-nv-ri.sendStatus", "0");
+ addSessionAttributeInt(config, "x-nv-ri.securityProtocol", 0);
+ addSessionAttributeBytes(config, "x-nv-ri.secInfo", new byte[0x20a]);
+ addSessionAttribute(config, "x-nv-videoFrameDropIntervalNumber", "0");
+ addSessionAttribute(config, "x-nv-videoFrameDropContinualNumber", "0");
+
+ config.append("t=0 0").append("\r\n");
+
+ config.append("m=video 47996 ").append("\r\n");
+
+ return config.toString();
+ }
+}