Use a single context object instead of passing around tons of objects. Start of GFE 2.1.x backwards compatibility.

This commit is contained in:
Cameron Gutman 2015-01-25 17:34:28 -05:00
parent 6de5cf8925
commit daf7598774
9 changed files with 211 additions and 148 deletions

View File

@ -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;
}

View File

@ -26,13 +26,13 @@ import com.limelight.nvstream.input.ControllerStream;
import com.limelight.nvstream.rtsp.RtspConnection; import com.limelight.nvstream.rtsp.RtspConnection;
public class NvConnection { public class NvConnection {
// Context parameters
private String host; private String host;
private NvConnectionListener listener;
private StreamConfiguration config;
private LimelightCryptoProvider cryptoProvider; private LimelightCryptoProvider cryptoProvider;
private String uniqueId; private String uniqueId;
private ConnectionContext context;
private InetAddress hostAddr; // Stream objects
private ControlStream controlStream; private ControlStream controlStream;
private ControllerStream inputStream; private ControllerStream inputStream;
private VideoStream videoStream; private VideoStream videoStream;
@ -43,27 +43,25 @@ public class NvConnection {
private Object videoRenderTarget; private Object videoRenderTarget;
private VideoDecoderRenderer videoDecoderRenderer; private VideoDecoderRenderer videoDecoderRenderer;
private AudioRenderer audioRenderer; private AudioRenderer audioRenderer;
private String localDeviceName;
private SecretKey riKey;
private int riKeyId;
public NvConnection(String host, String uniqueId, NvConnectionListener listener, StreamConfiguration config, LimelightCryptoProvider cryptoProvider) public NvConnection(String host, String uniqueId, NvConnectionListener listener, StreamConfiguration config, LimelightCryptoProvider cryptoProvider)
{ {
this.host = host; this.host = host;
this.listener = listener;
this.config = config;
this.cryptoProvider = cryptoProvider; this.cryptoProvider = cryptoProvider;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
this.context = new ConnectionContext();
this.context.connListener = listener;
this.context.streamConfig = config;
try { try {
// This is unique per connection // This is unique per connection
this.riKey = generateRiAesKey(); this.context.riKey = generateRiAesKey();
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
} }
this.riKeyId = generateRiKeyId(); this.context.riKeyId = generateRiKeyId();
} }
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException { private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
@ -100,23 +98,41 @@ public class NvConnection {
private boolean startApp() throws XmlPullParserException, IOException 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 serverInfo = h.getServerInfo(uniqueId);
String serverVersion = h.getServerVersion(serverInfo); String serverVersion = h.getServerVersion(serverInfo);
if (!serverVersion.startsWith("4.")) { if (serverVersion == null || serverVersion.indexOf('.') < 0) {
listener.displayMessage("Limelight now requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again."); 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; return false;
} }
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) { if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
listener.displayMessage("Device not paired with computer"); context.connListener.displayMessage("Device not paired with computer");
return false; return false;
} }
NvApp app = h.getApp(config.getApp()); NvApp app = h.getApp(context.streamConfig.getApp());
if (app == null) { 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; return false;
} }
@ -124,8 +140,8 @@ public class NvConnection {
if (h.getCurrentGame(serverInfo) != 0) { if (h.getCurrentGame(serverInfo) != 0) {
try { try {
if (h.getCurrentGame(serverInfo) == app.getAppId()) { if (h.getCurrentGame(serverInfo) == app.getAppId()) {
if (!h.resumeApp(riKey, riKeyId)) { if (!h.resumeApp(context)) {
listener.displayMessage("Failed to resume existing session"); context.connListener.displayMessage("Failed to resume existing session");
return false; return false;
} }
} else if (h.getCurrentGame(serverInfo) != app.getAppId()) { } else if (h.getCurrentGame(serverInfo) != app.getAppId()) {
@ -135,13 +151,13 @@ public class NvConnection {
if (e.getErrorCode() == 470) { if (e.getErrorCode() == 470) {
// This is the error you get when you try to resume a session that's not yours. // 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. // 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 " + " so it cannot be resumed. End streaming on the original " +
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")"); "device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
return false; return false;
} }
else if (e.getErrorCode() == 525) { 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."); "quit the session and start streaming again.");
return false; return false;
} else { } else {
@ -160,7 +176,7 @@ public class NvConnection {
protected boolean quitAndLaunch(NvHTTP h, NvApp app) throws IOException, protected boolean quitAndLaunch(NvHTTP h, NvApp app) throws IOException,
XmlPullParserException { XmlPullParserException {
if (!h.quitApp()) { 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; return false;
} else { } else {
return launchNotRunningApp(h, app); return launchNotRunningApp(h, app);
@ -170,9 +186,9 @@ public class NvConnection {
private boolean launchNotRunningApp(NvHTTP h, NvApp app) private boolean launchNotRunningApp(NvHTTP h, NvApp app)
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
// Launch the app since it's not running // 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) { if (gameSessionId == 0) {
listener.displayMessage("Failed to launch application"); context.connListener.displayMessage("Failed to launch application");
return false; return false;
} }
@ -183,14 +199,14 @@ public class NvConnection {
private boolean doRtspHandshake() throws IOException private boolean doRtspHandshake() throws IOException
{ {
RtspConnection r = new RtspConnection(hostAddr); RtspConnection r = new RtspConnection(context);
r.doRtspHandshake(config); r.doRtspHandshake();
return true; return true;
} }
private boolean startControlStream() throws IOException private boolean startControlStream() throws IOException
{ {
controlStream = new ControlStream(hostAddr, listener); controlStream = new ControlStream(context);
controlStream.initialize(); controlStream.initialize();
controlStream.start(); controlStream.start();
return true; return true;
@ -198,13 +214,13 @@ public class NvConnection {
private boolean startVideoStream() throws IOException private boolean startVideoStream() throws IOException
{ {
videoStream = new VideoStream(hostAddr, listener, controlStream, config); videoStream = new VideoStream(context, controlStream);
return videoStream.startVideoStream(videoDecoderRenderer, videoRenderTarget, drFlags); return videoStream.startVideoStream(videoDecoderRenderer, videoRenderTarget, drFlags);
} }
private boolean startAudioStream() throws IOException private boolean startAudioStream() throws IOException
{ {
audioStream = new AudioStream(hostAddr, listener, audioRenderer); audioStream = new AudioStream(context, audioRenderer);
return audioStream.startAudioStream(); return audioStream.startAudioStream();
} }
@ -214,7 +230,7 @@ public class NvConnection {
// it to the instance variable once the object is properly initialized. // it to the instance variable once the object is properly initialized.
// This avoids the race where inputStream != null but inputStream.initialize() // This avoids the race where inputStream != null but inputStream.initialize()
// has not returned yet. // has not returned yet.
ControllerStream tempController = new ControllerStream(hostAddr, riKey, riKeyId, listener); ControllerStream tempController = new ControllerStream(context);
tempController.initialize(); tempController.initialize();
tempController.start(); tempController.start();
inputStream = tempController; inputStream = tempController;
@ -228,10 +244,10 @@ public class NvConnection {
if (currentStage == NvConnectionListener.Stage.LAUNCH_APP) { if (currentStage == NvConnectionListener.Stage.LAUNCH_APP) {
// Display the app name instead of the stage name // 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 { try {
switch (currentStage) switch (currentStage)
{ {
@ -261,25 +277,24 @@ public class NvConnection {
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
listener.displayMessage(e.getMessage()); context.connListener.displayMessage(e.getMessage());
success = false; success = false;
} }
if (success) { if (success) {
listener.stageComplete(currentStage); context.connListener.stageComplete(currentStage);
} }
else { else {
listener.stageFailed(currentStage); context.connListener.stageFailed(currentStage);
return; return;
} }
} }
listener.connectionStarted(); context.connListener.connectionStarted();
} }
public void start(String localDeviceName, Object videoRenderTarget, int drFlags, AudioRenderer audioRenderer, VideoDecoderRenderer videoDecoderRenderer) public void start(String localDeviceName, Object videoRenderTarget, int drFlags, AudioRenderer audioRenderer, VideoDecoderRenderer videoDecoderRenderer)
{ {
this.localDeviceName = localDeviceName;
this.drFlags = drFlags; this.drFlags = drFlags;
this.audioRenderer = audioRenderer; this.audioRenderer = audioRenderer;
this.videoRenderTarget = videoRenderTarget; this.videoRenderTarget = videoRenderTarget;
@ -288,9 +303,9 @@ public class NvConnection {
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
try { try {
hostAddr = InetAddress.getByName(host); context.serverAddress = InetAddress.getByName(host);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }

View File

@ -8,7 +8,7 @@ import java.net.InetSocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.util.LinkedList; 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.ByteBufferDescriptor;
import com.limelight.nvstream.av.RtpPacket; import com.limelight.nvstream.av.RtpPacket;
import com.limelight.nvstream.av.RtpReorderQueue; import com.limelight.nvstream.av.RtpReorderQueue;
@ -28,14 +28,12 @@ public class AudioStream {
private boolean aborting = false; private boolean aborting = false;
private InetAddress host; private ConnectionContext context;
private NvConnectionListener connListener;
private AudioRenderer streamListener; private AudioRenderer streamListener;
public AudioStream(InetAddress host, NvConnectionListener connListener, AudioRenderer streamListener) public AudioStream(ConnectionContext context, AudioRenderer streamListener)
{ {
this.host = host; this.context = context;
this.connListener = connListener;
this.streamListener = streamListener; this.streamListener = streamListener;
} }
@ -131,7 +129,7 @@ public class AudioStream {
try { try {
samples = depacketizer.getNextDecodedData(); samples = depacketizer.getNextDecodedData();
} catch (InterruptedException e) { } catch (InterruptedException e) {
connListener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
@ -198,7 +196,7 @@ public class AudioStream {
} }
} }
} catch (IOException e) { } catch (IOException e) {
connListener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }
@ -219,7 +217,7 @@ public class AudioStream {
// PING in ASCII // PING in ASCII
final byte[] pingPacketData = new byte[] {0x50, 0x49, 0x4E, 0x47}; final byte[] pingPacketData = new byte[] {0x50, 0x49, 0x4E, 0x47};
DatagramPacket pingPacket = new DatagramPacket(pingPacketData, pingPacketData.length); 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 // Send PING every 500 ms
while (!isInterrupted()) while (!isInterrupted())
@ -227,14 +225,14 @@ public class AudioStream {
try { try {
rtp.send(pingPacket); rtp.send(pingPacket);
} catch (IOException e) { } catch (IOException e) {
connListener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
try { try {
Thread.sleep(500); Thread.sleep(500);
} catch (InterruptedException e) { } catch (InterruptedException e) {
connListener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }

View File

@ -1,6 +1,7 @@
package com.limelight.nvstream.av.video; package com.limelight.nvstream.av.video;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
@ -10,8 +11,7 @@ import java.net.SocketException;
import java.util.LinkedList; import java.util.LinkedList;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.ConnectionStatusListener; import com.limelight.nvstream.av.ConnectionStatusListener;
import com.limelight.nvstream.av.RtpPacket; import com.limelight.nvstream.av.RtpPacket;
import com.limelight.nvstream.av.RtpReorderQueue; import com.limelight.nvstream.av.RtpReorderQueue;
@ -19,6 +19,7 @@ import com.limelight.nvstream.av.RtpReorderQueue;
public class VideoStream { public class VideoStream {
public static final int RTP_PORT = 47998; public static final int RTP_PORT = 47998;
public static final int RTCP_PORT = 47999; 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 FIRST_FRAME_TIMEOUT = 5000;
public static final int RTP_RECV_BUFFER = 256 * 1024; public static final int RTP_RECV_BUFFER = 256 * 1024;
@ -33,28 +34,24 @@ public class VideoStream {
// presentable frame // presentable frame
public static final int VIDEO_RING_SIZE = 384; public static final int VIDEO_RING_SIZE = 384;
private InetAddress host;
private DatagramSocket rtp; private DatagramSocket rtp;
private Socket firstFrameSocket; private Socket firstFrameSocket;
private LinkedList<Thread> threads = new LinkedList<Thread>(); private LinkedList<Thread> threads = new LinkedList<Thread>();
private NvConnectionListener listener; private ConnectionContext context;
private ConnectionStatusListener avConnListener; private ConnectionStatusListener avConnListener;
private VideoDepacketizer depacketizer; private VideoDepacketizer depacketizer;
private StreamConfiguration streamConfig;
private VideoDecoderRenderer decRend; private VideoDecoderRenderer decRend;
private boolean startedRendering; private boolean startedRendering;
private boolean aborting = false; private boolean aborting = false;
public VideoStream(InetAddress host, NvConnectionListener listener, ConnectionStatusListener avConnListener, StreamConfiguration streamConfig) public VideoStream(ConnectionContext context, ConnectionStatusListener avConnListener)
{ {
this.host = host; this.context = context;
this.listener = listener;
this.avConnListener = avConnListener; this.avConnListener = avConnListener;
this.streamConfig = streamConfig;
} }
public void abort() public void abort()
@ -98,6 +95,40 @@ public class VideoStream {
threads.clear(); 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 public void setupRtpSession() throws SocketException
{ {
rtp = new DatagramSocket(); rtp = new DatagramSocket();
@ -107,11 +138,11 @@ public class VideoStream {
public boolean setupDecoderRenderer(VideoDecoderRenderer decRend, Object renderTarget, int drFlags) { public boolean setupDecoderRenderer(VideoDecoderRenderer decRend, Object renderTarget, int drFlags) {
this.decRend = decRend; this.decRend = decRend;
depacketizer = new VideoDepacketizer(avConnListener, streamConfig.getMaxPacketSize()); depacketizer = new VideoDepacketizer(avConnListener, context.streamConfig.getMaxPacketSize());
if (decRend != null) { if (decRend != null) {
try { try {
if (!decRend.setup(streamConfig.getWidth(), streamConfig.getHeight(), if (!decRend.setup(context.streamConfig.getWidth(), context.streamConfig.getHeight(),
60, renderTarget, drFlags)) { 60, renderTarget, drFlags)) {
return false; return false;
} }
@ -168,7 +199,7 @@ public class VideoStream {
RtpReorderQueue.RtpQueueStatus queueStatus; RtpReorderQueue.RtpQueueStatus queueStatus;
// Preinitialize the ring buffer // 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++) { for (int i = 0; i < VIDEO_RING_SIZE; i++) {
ring[i] = new VideoPacket(new byte[requiredBufferSize]); ring[i] = new VideoPacket(new byte[requiredBufferSize]);
} }
@ -215,7 +246,7 @@ public class VideoStream {
} }
} while (ring[ringIndex].decodeUnitRefCount.get() != 0); } while (ring[ringIndex].decodeUnitRefCount.get() != 0);
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }
@ -236,7 +267,7 @@ public class VideoStream {
// PING in ASCII // PING in ASCII
final byte[] pingPacketData = new byte[] {0x50, 0x49, 0x4E, 0x47}; final byte[] pingPacketData = new byte[] {0x50, 0x49, 0x4E, 0x47};
DatagramPacket pingPacket = new DatagramPacket(pingPacketData, pingPacketData.length); 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 // Send PING every 500 ms
while (!isInterrupted()) while (!isInterrupted())
@ -244,14 +275,14 @@ public class VideoStream {
try { try {
rtp.send(pingPacket); rtp.send(pingPacket);
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
try { try {
Thread.sleep(500); Thread.sleep(500);
} catch (InterruptedException e) { } catch (InterruptedException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }

View File

@ -3,7 +3,6 @@ package com.limelight.nvstream.control;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -11,7 +10,7 @@ import java.nio.ByteOrder;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.av.ConnectionStatusListener; import com.limelight.nvstream.av.ConnectionStatusListener;
public class ControlStream implements ConnectionStatusListener { public class ControlStream implements ConnectionStatusListener {
@ -43,8 +42,7 @@ public class ControlStream implements ConnectionStatusListener {
private int currentFrame; private int currentFrame;
private int lossCountSinceLastReport; private int lossCountSinceLastReport;
private NvConnectionListener listener; private ConnectionContext context;
private InetAddress host;
public static final int LOSS_PERIOD_MS = 15000; public static final int LOSS_PERIOD_MS = 15000;
public static final int MAX_LOSS_COUNT_IN_PERIOD = 5; public static final int MAX_LOSS_COUNT_IN_PERIOD = 5;
@ -64,17 +62,16 @@ public class ControlStream implements ConnectionStatusListener {
private LinkedBlockingQueue<int[]> invalidReferenceFrameTuples = new LinkedBlockingQueue<int[]>(); private LinkedBlockingQueue<int[]> invalidReferenceFrameTuples = new LinkedBlockingQueue<int[]>();
private boolean aborting = false; private boolean aborting = false;
public ControlStream(InetAddress host, NvConnectionListener listener) public ControlStream(ConnectionContext context)
{ {
this.listener = listener; this.context = context;
this.host = host;
} }
public void initialize() throws IOException public void initialize() throws IOException
{ {
s = new Socket(); s = new Socket();
s.setTcpNoDelay(true); s.setTcpNoDelay(true);
s.connect(new InetSocketAddress(host, PORT), CONTROL_TIMEOUT); s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROL_TIMEOUT);
in = s.getInputStream(); in = s.getInputStream();
out = s.getOutputStream(); out = s.getOutputStream();
} }
@ -159,14 +156,14 @@ public class ControlStream implements ConnectionStatusListener {
sendLossStats(bb); sendLossStats(bb);
lossCountSinceLastReport = 0; lossCountSinceLastReport = 0;
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
try { try {
Thread.sleep(LOSS_REPORT_INTERVAL_MS); Thread.sleep(LOSS_REPORT_INTERVAL_MS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }
@ -187,7 +184,7 @@ public class ControlStream implements ConnectionStatusListener {
try { try {
tuple = invalidReferenceFrameTuples.take(); tuple = invalidReferenceFrameTuples.take();
} catch (InterruptedException e) { } catch (InterruptedException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
@ -215,7 +212,7 @@ public class ControlStream implements ConnectionStatusListener {
ControlStream.this.sendResync(tuple[0], tuple[1]); ControlStream.this.sendResync(tuple[0], tuple[1]);
LimeLog.warning("Frames invalidated"); LimeLog.warning("Frames invalidated");
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }
@ -422,7 +419,7 @@ public class ControlStream implements ConnectionStatusListener {
} }
else { else {
if (++lossCount == MAX_LOSS_COUNT_IN_PERIOD) { 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; lossCount = -MAX_LOSS_COUNT_IN_PERIOD * MESSAGE_DELAY_FACTOR;
lossTimestamp = 0; lossTimestamp = 0;
} }
@ -439,7 +436,7 @@ public class ControlStream implements ConnectionStatusListener {
} }
if (++slowSinkCount == MAX_SLOW_SINK_COUNT) { 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; slowSinkCount = -MAX_SLOW_SINK_COUNT * MESSAGE_DELAY_FACTOR;
} }
} }

View File

@ -20,7 +20,6 @@ import java.util.Stack;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.crypto.SecretKey;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -33,7 +32,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
import com.limelight.nvstream.StreamConfiguration; import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.http.PairingManager.PairState; import com.limelight.nvstream.http.PairingManager.PairState;
import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request; import com.squareup.okhttp.Request;
@ -364,23 +363,23 @@ public class NvHTTP {
return new String(hexChars); 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 + String xmlStr = openHttpConnectionToString(baseUrl +
"/launch?uniqueid=" + uniqueId + "/launch?uniqueid=" + uniqueId +
"&appid=" + appId + "&appid=" + appId +
"&mode=" + config.getWidth() + "x" + config.getHeight() + "x" + config.getRefreshRate() + "&mode=" + context.streamConfig.getWidth() + "x" + context.streamConfig.getHeight() + "x" + context.streamConfig.getRefreshRate() +
"&additionalStates=1&sops=" + (config.getSops() ? 1 : 0) + "&additionalStates=1&sops=" + (context.streamConfig.getSops() ? 1 : 0) +
"&rikey="+bytesToHex(inputKey.getEncoded()) + "&rikey="+bytesToHex(context.riKey.getEncoded()) +
"&rikeyid="+riKeyId + "&rikeyid="+context.riKeyId +
"&localAudioPlayMode=" + (config.getPlayLocalAudio() ? 1 : 0), false); "&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0), false);
String gameSession = getXmlString(xmlStr, "gamesession"); String gameSession = getXmlString(xmlStr, "gamesession");
return Integer.parseInt(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 + String xmlStr = openHttpConnectionToString(baseUrl + "/resume?uniqueid=" + uniqueId +
"&rikey="+bytesToHex(inputKey.getEncoded()) + "&rikey="+bytesToHex(context.riKey.getEncoded()) +
"&rikeyid="+riKeyId, false); "&rikeyid="+context.riKeyId, false);
String resume = getXmlString(xmlStr, "resume"); String resume = getXmlString(xmlStr, "resume");
return Integer.parseInt(resume) != 0; return Integer.parseInt(resume) != 0;
} }

View File

@ -2,7 +2,6 @@ package com.limelight.nvstream.input;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -15,10 +14,9 @@ import java.util.concurrent.LinkedBlockingQueue;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.ConnectionContext;
public class ControllerStream { public class ControllerStream {
@ -26,11 +24,11 @@ public class ControllerStream {
public final static int CONTROLLER_TIMEOUT = 3000; public final static int CONTROLLER_TIMEOUT = 3000;
private InetAddress host; private ConnectionContext context;
private Socket s; private Socket s;
private OutputStream out; private OutputStream out;
private Cipher riCipher; private Cipher riCipher;
private NvConnectionListener listener;
private Thread inputThread; private Thread inputThread;
private LinkedBlockingQueue<InputPacket> inputQueue = new LinkedBlockingQueue<InputPacket>(); private LinkedBlockingQueue<InputPacket> inputQueue = new LinkedBlockingQueue<InputPacket>();
@ -38,18 +36,17 @@ public class ControllerStream {
private ByteBuffer stagingBuffer = ByteBuffer.allocate(128); private ByteBuffer stagingBuffer = ByteBuffer.allocate(128);
private ByteBuffer sendBuffer = ByteBuffer.allocate(128).order(ByteOrder.BIG_ENDIAN); 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.context = context;
this.listener = listener;
try { try {
// This cipher is guaranteed to be supported // This cipher is guaranteed to be supported
this.riCipher = Cipher.getInstance("AES/CBC/NoPadding"); this.riCipher = Cipher.getInstance("AES/CBC/NoPadding");
ByteBuffer bb = ByteBuffer.allocate(16); 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) { } catch (NoSuchAlgorithmException e) {
e.printStackTrace(); e.printStackTrace();
} catch (NoSuchPaddingException e) { } catch (NoSuchPaddingException e) {
@ -64,7 +61,7 @@ public class ControllerStream {
public void initialize() throws IOException public void initialize() throws IOException
{ {
s = new Socket(); s = new Socket();
s.connect(new InetSocketAddress(host, PORT), CONTROLLER_TIMEOUT); s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROLLER_TIMEOUT);
s.setTcpNoDelay(true); s.setTcpNoDelay(true);
out = s.getOutputStream(); out = s.getOutputStream();
} }
@ -80,7 +77,7 @@ public class ControllerStream {
try { try {
packet = inputQueue.take(); packet = inputQueue.take();
} catch (InterruptedException e) { } catch (InterruptedException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
@ -123,7 +120,7 @@ public class ControllerStream {
try { try {
sendPacket(initialMouseMove); sendPacket(initialMouseMove);
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
@ -169,7 +166,7 @@ public class ControllerStream {
try { try {
sendPacket(packet); sendPacket(packet);
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }
@ -178,7 +175,7 @@ public class ControllerStream {
try { try {
sendPacket(packet); sendPacket(packet);
} catch (IOException e) { } catch (IOException e) {
listener.connectionTerminated(e); context.connListener.connectionTerminated(e);
return; return;
} }
} }

View File

@ -1,13 +1,12 @@
package com.limelight.nvstream.rtsp; package com.limelight.nvstream.rtsp;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.util.HashMap; 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.RtspMessage;
import com.tinyrtsp.rtsp.message.RtspRequest; import com.tinyrtsp.rtsp.message.RtspRequest;
import com.tinyrtsp.rtsp.message.RtspResponse; import com.tinyrtsp.rtsp.message.RtspResponse;
@ -17,30 +16,38 @@ public class RtspConnection {
public static final int PORT = 48010; public static final int PORT = 48010;
public static final int RTSP_TIMEOUT = 5000; public static final int RTSP_TIMEOUT = 5000;
// GFE 2.2.2+
public static final int CLIENT_VERSION = 11;
private int sequenceNumber = 1; private int sequenceNumber = 1;
private int sessionId = 0; private int sessionId = 0;
private InetAddress host; private ConnectionContext context;
private String hostStr; private String hostStr;
public RtspConnection(InetAddress host) { public RtspConnection(ConnectionContext context) {
this.host = host; this.context = context;
if (host instanceof Inet6Address) { if (context.serverAddress instanceof Inet6Address) {
// RFC2732-formatted IPv6 address for use in URL // RFC2732-formatted IPv6 address for use in URL
this.hostStr = "["+host.getHostAddress()+"]"; this.hostStr = "["+context.serverAddress.getHostAddress()+"]";
} }
else { 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) { private RtspRequest createRtspRequest(String command, String target) {
RtspRequest m = new RtspRequest(command, target, "RTSP/1.0", RtspRequest m = new RtspRequest(command, target, "RTSP/1.0",
sequenceNumber++, new HashMap<String, String>(), null); sequenceNumber++, new HashMap<String, String>(), null);
m.setOption("X-GS-ClientVersion", ""+CLIENT_VERSION); m.setOption("X-GS-ClientVersion", ""+getRtspVersionFromContext(context));
return m; return m;
} }
@ -48,7 +55,7 @@ public class RtspConnection {
Socket s = new Socket(); Socket s = new Socket();
try { try {
s.setTcpNoDelay(true); 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()); RtspStream rtspStream = new RtspStream(s.getInputStream(), s.getOutputStream());
try { try {
@ -90,16 +97,16 @@ public class RtspConnection {
return transactRtspMessage(m); return transactRtspMessage(m);
} }
private RtspResponse sendVideoAnnounce(StreamConfiguration sc) throws IOException { private RtspResponse sendVideoAnnounce() throws IOException {
RtspRequest m = createRtspRequest("ANNOUNCE", "streamid=video"); RtspRequest m = createRtspRequest("ANNOUNCE", "streamid=video");
m.setOption("Session", ""+sessionId); m.setOption("Session", ""+sessionId);
m.setOption("Content-type", "application/sdp"); m.setOption("Content-type", "application/sdp");
m.setPayload(SdpGenerator.generateSdpFromConfig(host, sc)); m.setPayload(SdpGenerator.generateSdpFromContext(context));
m.setOption("Content-length", ""+m.getPayload().length()); m.setOption("Content-length", ""+m.getPayload().length());
return transactRtspMessage(m); return transactRtspMessage(m);
} }
public void doRtspHandshake(StreamConfiguration sc) throws IOException { public void doRtspHandshake() throws IOException {
RtspResponse r; RtspResponse r;
r = requestOptions(); r = requestOptions();
@ -128,7 +135,7 @@ public class RtspConnection {
throw new IOException("RTSP SETUP request failed: "+r.getStatusCode()); throw new IOException("RTSP SETUP request failed: "+r.getStatusCode());
} }
r = sendVideoAnnounce(sc); r = sendVideoAnnounce();
if (r.getStatusCode() != 200) { if (r.getStatusCode() != 200) {
throw new IOException("RTSP ANNOUNCE request failed: "+r.getStatusCode()); throw new IOException("RTSP ANNOUNCE request failed: "+r.getStatusCode());
} }

View File

@ -1,45 +1,43 @@
package com.limelight.nvstream.rtsp; package com.limelight.nvstream.rtsp;
import java.net.InetAddress;
import java.net.Inet6Address; import java.net.Inet6Address;
import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.StreamConfiguration;
public class SdpGenerator { public class SdpGenerator {
private static void addSessionAttribute(StringBuilder config, String attribute, String value) { private static void addSessionAttribute(StringBuilder config, String attribute, String value) {
config.append("a="+attribute+":"+value+" \r\n"); 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(); StringBuilder config = new StringBuilder();
config.append("v=0").append("\r\n"); // SDP Version 0 config.append("v=0").append("\r\n"); // SDP Version 0
config.append("o=android 0 "+RtspConnection.CLIENT_VERSION+" IN "); config.append("o=android 0 "+RtspConnection.getRtspVersionFromContext(context)+" IN ");
if (host instanceof Inet6Address) { if (context.serverAddress instanceof Inet6Address) {
config.append("IPv6 "); config.append("IPv6 ");
} }
else { else {
config.append("IPv4 "); config.append("IPv4 ");
} }
config.append(host.getHostAddress()); config.append(context.serverAddress.getHostAddress());
config.append("\r\n"); config.append("\r\n");
config.append("s=NVIDIA Streaming Client").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].clientViewportWd", ""+context.streamConfig.getWidth());
addSessionAttribute(config, "x-nv-video[0].clientViewportHt", ""+sc.getHeight()); addSessionAttribute(config, "x-nv-video[0].clientViewportHt", ""+context.streamConfig.getHeight());
addSessionAttribute(config, "x-nv-video[0].maxFPS", ""+sc.getRefreshRate()); 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"); 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].averageBitrate", "4");
addSessionAttribute(config, "x-nv-video[0].peakBitrate", "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].averageBitrate", "9");
addSessionAttribute(config, "x-nv-video[0].peakBitrate", "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"); 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 // 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 (context.streamConfig.getHeight() >= 1080 && context.streamConfig.getRefreshRate() >= 60) {
if (sc.getBitrate() < 10000) { if (context.streamConfig.getBitrate() < 10000) {
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+sc.getBitrate()); addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+context.streamConfig.getBitrate());
} }
else { else {
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "10000"); addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "10000");
} }
} }
else if (sc.getHeight() >= 1080 || sc.getRefreshRate() >= 60) { else if (context.streamConfig.getHeight() >= 1080 || context.streamConfig.getRefreshRate() >= 60) {
if (sc.getBitrate() < 7000) { if (context.streamConfig.getBitrate() < 7000) {
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+sc.getBitrate()); addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+context.streamConfig.getBitrate());
} }
else { else {
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "7000"); addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "7000");
} }
} }
else { else {
if (sc.getBitrate() < 3000) { if (context.streamConfig.getBitrate() < 3000) {
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+sc.getBitrate()); addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+context.streamConfig.getBitrate());
} }
else { else {
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", "3000"); 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 // 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 // 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"); addSessionAttribute(config, "x-nv-vqos[0].videoQualityScoreUpdateTime", "5000");
if (sc.getRemote()) { if (context.streamConfig.getRemote()) {
addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "0"); addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "0");
} }
else { else {
addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "5"); addSessionAttribute(config, "x-nv-vqos[0].qosTrafficType", "5");
} }
if (sc.getRemote()) { if (context.streamConfig.getRemote()) {
addSessionAttribute(config, "x-nv-aqos.qosTrafficType", "0"); addSessionAttribute(config, "x-nv-aqos.qosTrafficType", "0");
} }
else { else {