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(); + } +}