From daf759877441df7e706dca4dec8818b8bae4e922 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 25 Jan 2015 17:34:28 -0500 Subject: [PATCH] Use a single context object instead of passing around tons of objects. Start of GFE 2.1.x backwards compatibility. --- .../limelight/nvstream/ConnectionContext.java | 21 +++++ .../com/limelight/nvstream/NvConnection.java | 93 +++++++++++-------- .../nvstream/av/audio/AudioStream.java | 20 ++-- .../nvstream/av/video/VideoStream.java | 63 +++++++++---- .../nvstream/control/ControlStream.java | 25 +++-- .../com/limelight/nvstream/http/NvHTTP.java | 21 ++--- .../nvstream/input/ControllerStream.java | 27 +++--- .../nvstream/rtsp/RtspConnection.java | 41 ++++---- .../limelight/nvstream/rtsp/SdpGenerator.java | 48 +++++----- 9 files changed, 211 insertions(+), 148 deletions(-) create mode 100644 moonlight-common/src/com/limelight/nvstream/ConnectionContext.java diff --git a/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java b/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java new file mode 100644 index 00000000..02611efb --- /dev/null +++ b/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java @@ -0,0 +1,21 @@ +package com.limelight.nvstream; + +import java.net.InetAddress; + +import javax.crypto.SecretKey; + +public class ConnectionContext { + // Gen 3 servers are 2.1.1 - 2.2.1 + public static final int SERVER_GENERATION_3 = 3; + + // Gen 4 servers are 2.2.2+ + public static final int SERVER_GENERATION_4 = 4; + + public InetAddress serverAddress; + public StreamConfiguration streamConfig; + public NvConnectionListener connListener; + public SecretKey riKey; + public int riKeyId; + + public int serverGeneration; +} diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/com/limelight/nvstream/NvConnection.java index 15ebc5f0..2206054d 100644 --- a/moonlight-common/src/com/limelight/nvstream/NvConnection.java +++ b/moonlight-common/src/com/limelight/nvstream/NvConnection.java @@ -26,13 +26,13 @@ import com.limelight.nvstream.input.ControllerStream; import com.limelight.nvstream.rtsp.RtspConnection; public class NvConnection { + // Context parameters private String host; - private NvConnectionListener listener; - private StreamConfiguration config; private LimelightCryptoProvider cryptoProvider; private String uniqueId; + private ConnectionContext context; - private InetAddress hostAddr; + // Stream objects private ControlStream controlStream; private ControllerStream inputStream; private VideoStream videoStream; @@ -43,27 +43,25 @@ public class NvConnection { private Object videoRenderTarget; private VideoDecoderRenderer videoDecoderRenderer; private AudioRenderer audioRenderer; - private String localDeviceName; - private SecretKey riKey; - private int riKeyId; public NvConnection(String host, String uniqueId, NvConnectionListener listener, StreamConfiguration config, LimelightCryptoProvider cryptoProvider) - { + { this.host = host; - this.listener = listener; - this.config = config; this.cryptoProvider = cryptoProvider; this.uniqueId = uniqueId; + this.context = new ConnectionContext(); + this.context.connListener = listener; + this.context.streamConfig = config; try { // This is unique per connection - this.riKey = generateRiAesKey(); + this.context.riKey = generateRiAesKey(); } catch (NoSuchAlgorithmException e) { // Should never happen e.printStackTrace(); } - this.riKeyId = generateRiKeyId(); + this.context.riKeyId = generateRiKeyId(); } private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException { @@ -100,23 +98,41 @@ public class NvConnection { private boolean startApp() throws XmlPullParserException, IOException { - NvHTTP h = new NvHTTP(hostAddr, uniqueId, localDeviceName, cryptoProvider); + NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, null, cryptoProvider); String serverInfo = h.getServerInfo(uniqueId); String serverVersion = h.getServerVersion(serverInfo); - if (!serverVersion.startsWith("4.")) { - listener.displayMessage("Limelight now requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again."); + if (serverVersion == null || serverVersion.indexOf('.') < 0) { + context.connListener.displayMessage("Server major version not present"); + return false; + } + + try { + int majorVersion = Integer.parseInt(serverVersion.substring(0, serverVersion.indexOf('.'))); + if (majorVersion < 3) { + // Even though we support major version 3 (2.1.x), GFE 2.2.2 is preferred. + context.connListener.displayMessage("Limelight now requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again."); + return false; + } + else if (majorVersion > 4) { + // Warn the user but allow them to continue + context.connListener.displayTransientMessage("This version of GFE is not currently supported. You may experience issues until Limelight is updated"); + } + + LimeLog.info("Server major version: "+majorVersion); + } catch (NumberFormatException e) { + context.connListener.displayMessage("Server version malformed: "+serverVersion); return false; } if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) { - listener.displayMessage("Device not paired with computer"); + context.connListener.displayMessage("Device not paired with computer"); return false; } - NvApp app = h.getApp(config.getApp()); + NvApp app = h.getApp(context.streamConfig.getApp()); if (app == null) { - listener.displayMessage("The app " + config.getApp() + " is not in GFE app list"); + context.connListener.displayMessage("The app " + context.streamConfig.getApp() + " is not in GFE app list"); return false; } @@ -124,8 +140,8 @@ public class NvConnection { if (h.getCurrentGame(serverInfo) != 0) { try { if (h.getCurrentGame(serverInfo) == app.getAppId()) { - if (!h.resumeApp(riKey, riKeyId)) { - listener.displayMessage("Failed to resume existing session"); + if (!h.resumeApp(context)) { + context.connListener.displayMessage("Failed to resume existing session"); return false; } } else if (h.getCurrentGame(serverInfo) != app.getAppId()) { @@ -135,13 +151,13 @@ public class NvConnection { if (e.getErrorCode() == 470) { // This is the error you get when you try to resume a session that's not yours. // Because this is fairly common, we'll display a more detailed message. - listener.displayMessage("This session wasn't started by this device," + + context.connListener.displayMessage("This session wasn't started by this device," + " so it cannot be resumed. End streaming on the original " + "device or the PC itself and try again. (Error code: "+e.getErrorCode()+")"); return false; } else if (e.getErrorCode() == 525) { - listener.displayMessage("The application is minimized. Resume it on the PC manually or " + + context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " + "quit the session and start streaming again."); return false; } else { @@ -160,7 +176,7 @@ public class NvConnection { protected boolean quitAndLaunch(NvHTTP h, NvApp app) throws IOException, XmlPullParserException { if (!h.quitApp()) { - listener.displayMessage("Failed to quit previous session! You must quit it manually"); + context.connListener.displayMessage("Failed to quit previous session! You must quit it manually"); return false; } else { return launchNotRunningApp(h, app); @@ -170,9 +186,9 @@ public class NvConnection { private boolean launchNotRunningApp(NvHTTP h, NvApp app) throws IOException, XmlPullParserException { // Launch the app since it's not running - int gameSessionId = h.launchApp(app.getAppId(), riKey, riKeyId, config); + int gameSessionId = h.launchApp(context, app.getAppId()); if (gameSessionId == 0) { - listener.displayMessage("Failed to launch application"); + context.connListener.displayMessage("Failed to launch application"); return false; } @@ -183,14 +199,14 @@ public class NvConnection { private boolean doRtspHandshake() throws IOException { - RtspConnection r = new RtspConnection(hostAddr); - r.doRtspHandshake(config); + RtspConnection r = new RtspConnection(context); + r.doRtspHandshake(); return true; } private boolean startControlStream() throws IOException { - controlStream = new ControlStream(hostAddr, listener); + controlStream = new ControlStream(context); controlStream.initialize(); controlStream.start(); return true; @@ -198,13 +214,13 @@ public class NvConnection { private boolean startVideoStream() throws IOException { - videoStream = new VideoStream(hostAddr, listener, controlStream, config); + videoStream = new VideoStream(context, controlStream); return videoStream.startVideoStream(videoDecoderRenderer, videoRenderTarget, drFlags); } private boolean startAudioStream() throws IOException { - audioStream = new AudioStream(hostAddr, listener, audioRenderer); + audioStream = new AudioStream(context, audioRenderer); return audioStream.startAudioStream(); } @@ -214,7 +230,7 @@ public class NvConnection { // it to the instance variable once the object is properly initialized. // This avoids the race where inputStream != null but inputStream.initialize() // has not returned yet. - ControllerStream tempController = new ControllerStream(hostAddr, riKey, riKeyId, listener); + ControllerStream tempController = new ControllerStream(context); tempController.initialize(); tempController.start(); inputStream = tempController; @@ -228,10 +244,10 @@ public class NvConnection { if (currentStage == NvConnectionListener.Stage.LAUNCH_APP) { // Display the app name instead of the stage name - currentStage.setName(config.getApp()); + currentStage.setName(context.streamConfig.getApp()); } - listener.stageStarting(currentStage); + context.connListener.stageStarting(currentStage); try { switch (currentStage) { @@ -261,25 +277,24 @@ public class NvConnection { } } catch (Exception e) { e.printStackTrace(); - listener.displayMessage(e.getMessage()); + context.connListener.displayMessage(e.getMessage()); success = false; } if (success) { - listener.stageComplete(currentStage); + context.connListener.stageComplete(currentStage); } else { - listener.stageFailed(currentStage); + context.connListener.stageFailed(currentStage); return; } } - listener.connectionStarted(); + context.connListener.connectionStarted(); } public void start(String localDeviceName, Object videoRenderTarget, int drFlags, AudioRenderer audioRenderer, VideoDecoderRenderer videoDecoderRenderer) { - this.localDeviceName = localDeviceName; this.drFlags = drFlags; this.audioRenderer = audioRenderer; this.videoRenderTarget = videoRenderTarget; @@ -288,9 +303,9 @@ public class NvConnection { new Thread(new Runnable() { public void run() { try { - hostAddr = InetAddress.getByName(host); + context.serverAddress = InetAddress.getByName(host); } catch (UnknownHostException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } diff --git a/moonlight-common/src/com/limelight/nvstream/av/audio/AudioStream.java b/moonlight-common/src/com/limelight/nvstream/av/audio/AudioStream.java index 5ac236b4..c5be8d22 100644 --- a/moonlight-common/src/com/limelight/nvstream/av/audio/AudioStream.java +++ b/moonlight-common/src/com/limelight/nvstream/av/audio/AudioStream.java @@ -8,7 +8,7 @@ import java.net.InetSocketAddress; import java.net.SocketException; import java.util.LinkedList; -import com.limelight.nvstream.NvConnectionListener; +import com.limelight.nvstream.ConnectionContext; import com.limelight.nvstream.av.ByteBufferDescriptor; import com.limelight.nvstream.av.RtpPacket; import com.limelight.nvstream.av.RtpReorderQueue; @@ -28,14 +28,12 @@ public class AudioStream { private boolean aborting = false; - private InetAddress host; - private NvConnectionListener connListener; + private ConnectionContext context; private AudioRenderer streamListener; - public AudioStream(InetAddress host, NvConnectionListener connListener, AudioRenderer streamListener) + public AudioStream(ConnectionContext context, AudioRenderer streamListener) { - this.host = host; - this.connListener = connListener; + this.context = context; this.streamListener = streamListener; } @@ -131,7 +129,7 @@ public class AudioStream { try { samples = depacketizer.getNextDecodedData(); } catch (InterruptedException e) { - connListener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } @@ -198,7 +196,7 @@ public class AudioStream { } } } catch (IOException e) { - connListener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } @@ -219,7 +217,7 @@ public class AudioStream { // PING in ASCII final byte[] pingPacketData = new byte[] {0x50, 0x49, 0x4E, 0x47}; DatagramPacket pingPacket = new DatagramPacket(pingPacketData, pingPacketData.length); - pingPacket.setSocketAddress(new InetSocketAddress(host, RTP_PORT)); + pingPacket.setSocketAddress(new InetSocketAddress(context.serverAddress, RTP_PORT)); // Send PING every 500 ms while (!isInterrupted()) @@ -227,14 +225,14 @@ public class AudioStream { try { rtp.send(pingPacket); } catch (IOException e) { - connListener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } try { Thread.sleep(500); } catch (InterruptedException e) { - connListener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } diff --git a/moonlight-common/src/com/limelight/nvstream/av/video/VideoStream.java b/moonlight-common/src/com/limelight/nvstream/av/video/VideoStream.java index d89aa2b6..88473930 100644 --- a/moonlight-common/src/com/limelight/nvstream/av/video/VideoStream.java +++ b/moonlight-common/src/com/limelight/nvstream/av/video/VideoStream.java @@ -1,6 +1,7 @@ package com.limelight.nvstream.av.video; import java.io.IOException; +import java.io.InputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; @@ -10,8 +11,7 @@ import java.net.SocketException; import java.util.LinkedList; import com.limelight.LimeLog; -import com.limelight.nvstream.NvConnectionListener; -import com.limelight.nvstream.StreamConfiguration; +import com.limelight.nvstream.ConnectionContext; import com.limelight.nvstream.av.ConnectionStatusListener; import com.limelight.nvstream.av.RtpPacket; import com.limelight.nvstream.av.RtpReorderQueue; @@ -19,6 +19,7 @@ import com.limelight.nvstream.av.RtpReorderQueue; public class VideoStream { public static final int RTP_PORT = 47998; public static final int RTCP_PORT = 47999; + public static final int FIRST_FRAME_PORT = 47996; public static final int FIRST_FRAME_TIMEOUT = 5000; public static final int RTP_RECV_BUFFER = 256 * 1024; @@ -33,28 +34,24 @@ public class VideoStream { // presentable frame public static final int VIDEO_RING_SIZE = 384; - private InetAddress host; private DatagramSocket rtp; private Socket firstFrameSocket; private LinkedList threads = new LinkedList(); - private NvConnectionListener listener; + private ConnectionContext context; private ConnectionStatusListener avConnListener; private VideoDepacketizer depacketizer; - private StreamConfiguration streamConfig; private VideoDecoderRenderer decRend; private boolean startedRendering; private boolean aborting = false; - public VideoStream(InetAddress host, NvConnectionListener listener, ConnectionStatusListener avConnListener, StreamConfiguration streamConfig) + public VideoStream(ConnectionContext context, ConnectionStatusListener avConnListener) { - this.host = host; - this.listener = listener; + this.context = context; this.avConnListener = avConnListener; - this.streamConfig = streamConfig; } public void abort() @@ -98,6 +95,40 @@ public class VideoStream { threads.clear(); } + private void connectFirstFrame() throws IOException + { + firstFrameSocket = new Socket(); + firstFrameSocket.setSoTimeout(FIRST_FRAME_TIMEOUT); + firstFrameSocket.connect(new InetSocketAddress(context.serverAddress, FIRST_FRAME_PORT), FIRST_FRAME_TIMEOUT); + } + + private void readFirstFrame() throws IOException + { + byte[] firstFrame = new byte[context.streamConfig.getMaxPacketSize()]; + + try { + InputStream firstFrameStream = firstFrameSocket.getInputStream(); + + int offset = 0; + for (;;) + { + int bytesRead = firstFrameStream.read(firstFrame, offset, firstFrame.length-offset); + + if (bytesRead == -1) + break; + + offset += bytesRead; + } + + // We can actually ignore this data. It's the act of reading it that matters. + // If this changes, we'll need to move this call before startReceiveThread() + // to avoid state corruption in the depacketizer + } finally { + firstFrameSocket.close(); + firstFrameSocket = null; + } + } + public void setupRtpSession() throws SocketException { rtp = new DatagramSocket(); @@ -107,11 +138,11 @@ public class VideoStream { public boolean setupDecoderRenderer(VideoDecoderRenderer decRend, Object renderTarget, int drFlags) { this.decRend = decRend; - depacketizer = new VideoDepacketizer(avConnListener, streamConfig.getMaxPacketSize()); + depacketizer = new VideoDepacketizer(avConnListener, context.streamConfig.getMaxPacketSize()); if (decRend != null) { try { - if (!decRend.setup(streamConfig.getWidth(), streamConfig.getHeight(), + if (!decRend.setup(context.streamConfig.getWidth(), context.streamConfig.getHeight(), 60, renderTarget, drFlags)) { return false; } @@ -168,7 +199,7 @@ public class VideoStream { RtpReorderQueue.RtpQueueStatus queueStatus; // Preinitialize the ring buffer - int requiredBufferSize = streamConfig.getMaxPacketSize() + RtpPacket.MAX_HEADER_SIZE; + int requiredBufferSize = context.streamConfig.getMaxPacketSize() + RtpPacket.MAX_HEADER_SIZE; for (int i = 0; i < VIDEO_RING_SIZE; i++) { ring[i] = new VideoPacket(new byte[requiredBufferSize]); } @@ -215,7 +246,7 @@ public class VideoStream { } } while (ring[ringIndex].decodeUnitRefCount.get() != 0); } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } @@ -236,7 +267,7 @@ public class VideoStream { // PING in ASCII final byte[] pingPacketData = new byte[] {0x50, 0x49, 0x4E, 0x47}; DatagramPacket pingPacket = new DatagramPacket(pingPacketData, pingPacketData.length); - pingPacket.setSocketAddress(new InetSocketAddress(host, RTP_PORT)); + pingPacket.setSocketAddress(new InetSocketAddress(context.serverAddress, RTP_PORT)); // Send PING every 500 ms while (!isInterrupted()) @@ -244,14 +275,14 @@ public class VideoStream { try { rtp.send(pingPacket); } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } try { Thread.sleep(500); } catch (InterruptedException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } diff --git a/moonlight-common/src/com/limelight/nvstream/control/ControlStream.java b/moonlight-common/src/com/limelight/nvstream/control/ControlStream.java index 28003d09..8615fd0b 100644 --- a/moonlight-common/src/com/limelight/nvstream/control/ControlStream.java +++ b/moonlight-common/src/com/limelight/nvstream/control/ControlStream.java @@ -3,7 +3,6 @@ package com.limelight.nvstream.control; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; @@ -11,7 +10,7 @@ import java.nio.ByteOrder; import java.util.concurrent.LinkedBlockingQueue; import com.limelight.LimeLog; -import com.limelight.nvstream.NvConnectionListener; +import com.limelight.nvstream.ConnectionContext; import com.limelight.nvstream.av.ConnectionStatusListener; public class ControlStream implements ConnectionStatusListener { @@ -43,8 +42,7 @@ public class ControlStream implements ConnectionStatusListener { private int currentFrame; private int lossCountSinceLastReport; - private NvConnectionListener listener; - private InetAddress host; + private ConnectionContext context; public static final int LOSS_PERIOD_MS = 15000; public static final int MAX_LOSS_COUNT_IN_PERIOD = 5; @@ -64,17 +62,16 @@ public class ControlStream implements ConnectionStatusListener { private LinkedBlockingQueue invalidReferenceFrameTuples = new LinkedBlockingQueue(); private boolean aborting = false; - public ControlStream(InetAddress host, NvConnectionListener listener) + public ControlStream(ConnectionContext context) { - this.listener = listener; - this.host = host; + this.context = context; } public void initialize() throws IOException { s = new Socket(); s.setTcpNoDelay(true); - s.connect(new InetSocketAddress(host, PORT), CONTROL_TIMEOUT); + s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROL_TIMEOUT); in = s.getInputStream(); out = s.getOutputStream(); } @@ -159,14 +156,14 @@ public class ControlStream implements ConnectionStatusListener { sendLossStats(bb); lossCountSinceLastReport = 0; } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } try { Thread.sleep(LOSS_REPORT_INTERVAL_MS); } catch (InterruptedException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } @@ -187,7 +184,7 @@ public class ControlStream implements ConnectionStatusListener { try { tuple = invalidReferenceFrameTuples.take(); } catch (InterruptedException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } @@ -215,7 +212,7 @@ public class ControlStream implements ConnectionStatusListener { ControlStream.this.sendResync(tuple[0], tuple[1]); LimeLog.warning("Frames invalidated"); } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } @@ -422,7 +419,7 @@ public class ControlStream implements ConnectionStatusListener { } else { if (++lossCount == MAX_LOSS_COUNT_IN_PERIOD) { - listener.displayTransientMessage("Detected high amounts of network packet loss"); + context.connListener.displayTransientMessage("Detected high amounts of network packet loss"); lossCount = -MAX_LOSS_COUNT_IN_PERIOD * MESSAGE_DELAY_FACTOR; lossTimestamp = 0; } @@ -439,7 +436,7 @@ public class ControlStream implements ConnectionStatusListener { } if (++slowSinkCount == MAX_SLOW_SINK_COUNT) { - listener.displayTransientMessage("Your device is processing the A/V data too slowly. Try lowering stream resolution and/or frame rate."); + context.connListener.displayTransientMessage("Your device is processing the A/V data too slowly. Try lowering stream resolution and/or frame rate."); slowSinkCount = -MAX_SLOW_SINK_COUNT * MESSAGE_DELAY_FACTOR; } } diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index 8f3bfd89..a8d3979b 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -20,7 +20,6 @@ import java.util.Stack; import java.util.UUID; import java.util.concurrent.TimeUnit; -import javax.crypto.SecretKey; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; @@ -33,7 +32,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; -import com.limelight.nvstream.StreamConfiguration; +import com.limelight.nvstream.ConnectionContext; import com.limelight.nvstream.http.PairingManager.PairState; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; @@ -364,23 +363,23 @@ public class NvHTTP { return new String(hexChars); } - public int launchApp(int appId, SecretKey inputKey, int riKeyId, StreamConfiguration config) throws IOException, XmlPullParserException { + public int launchApp(ConnectionContext context, int appId) throws IOException, XmlPullParserException { String xmlStr = openHttpConnectionToString(baseUrl + "/launch?uniqueid=" + uniqueId + "&appid=" + appId + - "&mode=" + config.getWidth() + "x" + config.getHeight() + "x" + config.getRefreshRate() + - "&additionalStates=1&sops=" + (config.getSops() ? 1 : 0) + - "&rikey="+bytesToHex(inputKey.getEncoded()) + - "&rikeyid="+riKeyId + - "&localAudioPlayMode=" + (config.getPlayLocalAudio() ? 1 : 0), false); + "&mode=" + context.streamConfig.getWidth() + "x" + context.streamConfig.getHeight() + "x" + context.streamConfig.getRefreshRate() + + "&additionalStates=1&sops=" + (context.streamConfig.getSops() ? 1 : 0) + + "&rikey="+bytesToHex(context.riKey.getEncoded()) + + "&rikeyid="+context.riKeyId + + "&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0), false); String gameSession = getXmlString(xmlStr, "gamesession"); return Integer.parseInt(gameSession); } - public boolean resumeApp(SecretKey inputKey, int riKeyId) throws IOException, XmlPullParserException { + public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException { String xmlStr = openHttpConnectionToString(baseUrl + "/resume?uniqueid=" + uniqueId + - "&rikey="+bytesToHex(inputKey.getEncoded()) + - "&rikeyid="+riKeyId, false); + "&rikey="+bytesToHex(context.riKey.getEncoded()) + + "&rikeyid="+context.riKeyId, false); String resume = getXmlString(xmlStr, "resume"); return Integer.parseInt(resume) != 0; } diff --git a/moonlight-common/src/com/limelight/nvstream/input/ControllerStream.java b/moonlight-common/src/com/limelight/nvstream/input/ControllerStream.java index 93c3702f..1053a3cb 100644 --- a/moonlight-common/src/com/limelight/nvstream/input/ControllerStream.java +++ b/moonlight-common/src/com/limelight/nvstream/input/ControllerStream.java @@ -2,7 +2,6 @@ package com.limelight.nvstream.input; import java.io.IOException; import java.io.OutputStream; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; @@ -15,10 +14,9 @@ import java.util.concurrent.LinkedBlockingQueue; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; -import com.limelight.nvstream.NvConnectionListener; +import com.limelight.nvstream.ConnectionContext; public class ControllerStream { @@ -26,11 +24,11 @@ public class ControllerStream { public final static int CONTROLLER_TIMEOUT = 3000; - private InetAddress host; + private ConnectionContext context; + private Socket s; private OutputStream out; private Cipher riCipher; - private NvConnectionListener listener; private Thread inputThread; private LinkedBlockingQueue inputQueue = new LinkedBlockingQueue(); @@ -38,18 +36,17 @@ public class ControllerStream { private ByteBuffer stagingBuffer = ByteBuffer.allocate(128); private ByteBuffer sendBuffer = ByteBuffer.allocate(128).order(ByteOrder.BIG_ENDIAN); - public ControllerStream(InetAddress host, SecretKey riKey, int riKeyId, NvConnectionListener listener) + public ControllerStream(ConnectionContext context) { - this.host = host; - this.listener = listener; + this.context = context; try { // This cipher is guaranteed to be supported this.riCipher = Cipher.getInstance("AES/CBC/NoPadding"); ByteBuffer bb = ByteBuffer.allocate(16); - bb.putInt(riKeyId); + bb.putInt(context.riKeyId); - this.riCipher.init(Cipher.ENCRYPT_MODE, riKey, new IvParameterSpec(bb.array())); + this.riCipher.init(Cipher.ENCRYPT_MODE, context.riKey, new IvParameterSpec(bb.array())); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { @@ -64,7 +61,7 @@ public class ControllerStream { public void initialize() throws IOException { s = new Socket(); - s.connect(new InetSocketAddress(host, PORT), CONTROLLER_TIMEOUT); + s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROLLER_TIMEOUT); s.setTcpNoDelay(true); out = s.getOutputStream(); } @@ -80,7 +77,7 @@ public class ControllerStream { try { packet = inputQueue.take(); } catch (InterruptedException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } @@ -123,7 +120,7 @@ public class ControllerStream { try { sendPacket(initialMouseMove); } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } @@ -169,7 +166,7 @@ public class ControllerStream { try { sendPacket(packet); } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } @@ -178,7 +175,7 @@ public class ControllerStream { try { sendPacket(packet); } catch (IOException e) { - listener.connectionTerminated(e); + context.connListener.connectionTerminated(e); return; } } diff --git a/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java b/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java index 01ada0ab..e420c264 100644 --- a/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java +++ b/moonlight-common/src/com/limelight/nvstream/rtsp/RtspConnection.java @@ -1,13 +1,12 @@ 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.limelight.nvstream.ConnectionContext; import com.tinyrtsp.rtsp.message.RtspMessage; import com.tinyrtsp.rtsp.message.RtspRequest; import com.tinyrtsp.rtsp.message.RtspResponse; @@ -17,30 +16,38 @@ public class RtspConnection { public static final int PORT = 48010; public static final int RTSP_TIMEOUT = 5000; - // GFE 2.2.2+ - public static final int CLIENT_VERSION = 11; - private int sequenceNumber = 1; private int sessionId = 0; - private InetAddress host; + private ConnectionContext context; private String hostStr; - public RtspConnection(InetAddress host) { - this.host = host; - if (host instanceof Inet6Address) { + public RtspConnection(ConnectionContext context) { + this.context = context; + if (context.serverAddress instanceof Inet6Address) { // RFC2732-formatted IPv6 address for use in URL - this.hostStr = "["+host.getHostAddress()+"]"; + this.hostStr = "["+context.serverAddress.getHostAddress()+"]"; } else { - this.hostStr = host.getHostAddress(); + this.hostStr = context.serverAddress.getHostAddress(); + } + } + + public static int getRtspVersionFromContext(ConnectionContext context) { + switch (context.serverGeneration) + { + case ConnectionContext.SERVER_GENERATION_3: + return 10; + case ConnectionContext.SERVER_GENERATION_4: + default: + return 11; } } 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); + m.setOption("X-GS-ClientVersion", ""+getRtspVersionFromContext(context)); return m; } @@ -48,7 +55,7 @@ public class RtspConnection { Socket s = new Socket(); try { s.setTcpNoDelay(true); - s.connect(new InetSocketAddress(host, PORT), RTSP_TIMEOUT); + s.connect(new InetSocketAddress(context.serverAddress, PORT), RTSP_TIMEOUT); RtspStream rtspStream = new RtspStream(s.getInputStream(), s.getOutputStream()); try { @@ -90,16 +97,16 @@ public class RtspConnection { return transactRtspMessage(m); } - private RtspResponse sendVideoAnnounce(StreamConfiguration sc) throws IOException { + private RtspResponse sendVideoAnnounce() throws IOException { RtspRequest m = createRtspRequest("ANNOUNCE", "streamid=video"); m.setOption("Session", ""+sessionId); m.setOption("Content-type", "application/sdp"); - m.setPayload(SdpGenerator.generateSdpFromConfig(host, sc)); + m.setPayload(SdpGenerator.generateSdpFromContext(context)); m.setOption("Content-length", ""+m.getPayload().length()); return transactRtspMessage(m); } - public void doRtspHandshake(StreamConfiguration sc) throws IOException { + public void doRtspHandshake() throws IOException { RtspResponse r; r = requestOptions(); @@ -128,7 +135,7 @@ public class RtspConnection { throw new IOException("RTSP SETUP request failed: "+r.getStatusCode()); } - r = sendVideoAnnounce(sc); + r = sendVideoAnnounce(); if (r.getStatusCode() != 200) { throw new IOException("RTSP ANNOUNCE 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 index dfb05875..c65cbdcc 100644 --- a/moonlight-common/src/com/limelight/nvstream/rtsp/SdpGenerator.java +++ b/moonlight-common/src/com/limelight/nvstream/rtsp/SdpGenerator.java @@ -1,45 +1,43 @@ package com.limelight.nvstream.rtsp; -import java.net.InetAddress; import java.net.Inet6Address; - -import com.limelight.nvstream.StreamConfiguration; +import com.limelight.nvstream.ConnectionContext; public class SdpGenerator { 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) { + public static String generateSdpFromContext(ConnectionContext context) { StringBuilder config = new StringBuilder(); config.append("v=0").append("\r\n"); // SDP Version 0 - config.append("o=android 0 "+RtspConnection.CLIENT_VERSION+" IN "); - if (host instanceof Inet6Address) { + config.append("o=android 0 "+RtspConnection.getRtspVersionFromContext(context)+" IN "); + if (context.serverAddress instanceof Inet6Address) { config.append("IPv6 "); } else { config.append("IPv4 "); } - config.append(host.getHostAddress()); + config.append(context.serverAddress.getHostAddress()); config.append("\r\n"); config.append("s=NVIDIA Streaming Client").append("\r\n"); - addSessionAttribute(config, "x-nv-general.serverAddress", "rtsp://"+host.getHostAddress()+":48010"); + addSessionAttribute(config, "x-nv-general.serverAddress", "rtsp://"+context.serverAddress.getHostAddress()+":48010"); - addSessionAttribute(config, "x-nv-video[0].clientViewportWd", ""+sc.getWidth()); - addSessionAttribute(config, "x-nv-video[0].clientViewportHt", ""+sc.getHeight()); - addSessionAttribute(config, "x-nv-video[0].maxFPS", ""+sc.getRefreshRate()); + addSessionAttribute(config, "x-nv-video[0].clientViewportWd", ""+context.streamConfig.getWidth()); + addSessionAttribute(config, "x-nv-video[0].clientViewportHt", ""+context.streamConfig.getHeight()); + addSessionAttribute(config, "x-nv-video[0].maxFPS", ""+context.streamConfig.getRefreshRate()); - addSessionAttribute(config, "x-nv-video[0].packetSize", ""+sc.getMaxPacketSize()); + addSessionAttribute(config, "x-nv-video[0].packetSize", ""+context.streamConfig.getMaxPacketSize()); addSessionAttribute(config, "x-nv-video[0].rateControlMode", "4"); - if (sc.getRemote()) { + if (context.streamConfig.getRemote()) { addSessionAttribute(config, "x-nv-video[0].averageBitrate", "4"); addSessionAttribute(config, "x-nv-video[0].peakBitrate", "4"); } - else if (sc.getBitrate() <= 13000) { + else if (context.streamConfig.getBitrate() <= 13000) { addSessionAttribute(config, "x-nv-video[0].averageBitrate", "9"); addSessionAttribute(config, "x-nv-video[0].peakBitrate", "9"); } @@ -50,32 +48,32 @@ public class SdpGenerator { addSessionAttribute(config, "x-nv-vqos[0].bw.flags", "51"); // Lock the bitrate if we're not scaling resolution so the picture doesn't get too bad - if (sc.getHeight() >= 1080 && sc.getRefreshRate() >= 60) { - if (sc.getBitrate() < 10000) { - addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+sc.getBitrate()); + if (context.streamConfig.getHeight() >= 1080 && context.streamConfig.getRefreshRate() >= 60) { + if (context.streamConfig.getBitrate() < 10000) { + addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+context.streamConfig.getBitrate()); } else { addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "10000"); } } - else if (sc.getHeight() >= 1080 || sc.getRefreshRate() >= 60) { - if (sc.getBitrate() < 7000) { - addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+sc.getBitrate()); + else if (context.streamConfig.getHeight() >= 1080 || context.streamConfig.getRefreshRate() >= 60) { + if (context.streamConfig.getBitrate() < 7000) { + addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+context.streamConfig.getBitrate()); } else { addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "7000"); } } else { - if (sc.getBitrate() < 3000) { - addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+sc.getBitrate()); + if (context.streamConfig.getBitrate() < 3000) { + addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+context.streamConfig.getBitrate()); } else { addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "3000"); } } - addSessionAttribute(config, "x-nv-vqos[0].bw.maximumBitrate", ""+sc.getBitrate()); + addSessionAttribute(config, "x-nv-vqos[0].bw.maximumBitrate", ""+context.streamConfig.getBitrate()); // Using FEC turns padding on which makes us have to take the slow path // in the depacketizer, not to mention exposing some ambiguous cases with @@ -85,14 +83,14 @@ public class SdpGenerator { addSessionAttribute(config, "x-nv-vqos[0].videoQualityScoreUpdateTime", "5000"); - if (sc.getRemote()) { + if (context.streamConfig.getRemote()) { addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "0"); } else { addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "5"); } - if (sc.getRemote()) { + if (context.streamConfig.getRemote()) { addSessionAttribute(config, "x-nv-aqos.qosTrafficType", "0"); } else {