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

View File

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

View File

@ -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<Thread> threads = new LinkedList<Thread>();
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;
}
}

View File

@ -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<int[]> invalidReferenceFrameTuples = new LinkedBlockingQueue<int[]>();
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;
}
}

View File

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

View File

@ -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<InputPacket> inputQueue = new LinkedBlockingQueue<InputPacket>();
@ -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;
}
}

View File

@ -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<String, String>(), 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());
}

View File

@ -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 {