mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-19 11:03:01 +00:00
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:
parent
6de5cf8925
commit
daf7598774
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user