mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-21 12:03:02 +00:00
Implement ENet for control and input streams
This commit is contained in:
parent
306c2d143b
commit
5fd105c9a9
@ -292,7 +292,7 @@ public class NvConnection {
|
|||||||
// 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(context);
|
ControllerStream tempController = new ControllerStream(context);
|
||||||
tempController.initialize();
|
tempController.initialize(controlStream);
|
||||||
tempController.start();
|
tempController.start();
|
||||||
inputStream = tempController;
|
inputStream = tempController;
|
||||||
return true;
|
return true;
|
||||||
|
@ -13,11 +13,13 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.nvstream.ConnectionContext;
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
import com.limelight.nvstream.av.ConnectionStatusListener;
|
import com.limelight.nvstream.av.ConnectionStatusListener;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.enet.EnetConnection;
|
||||||
import com.limelight.utils.TimeHelper;
|
import com.limelight.utils.TimeHelper;
|
||||||
|
|
||||||
public class ControlStream implements ConnectionStatusListener {
|
public class ControlStream implements ConnectionStatusListener, InputPacketSender {
|
||||||
|
|
||||||
private static final int PORT = 47995;
|
private static final int TCP_PORT = 47995;
|
||||||
|
private static final int UDP_PORT = 47999;
|
||||||
|
|
||||||
private static final int CONTROL_TIMEOUT = 10000;
|
private static final int CONTROL_TIMEOUT = 10000;
|
||||||
|
|
||||||
@ -117,6 +119,10 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
|
|
||||||
private int slowSinkCount;
|
private int slowSinkCount;
|
||||||
|
|
||||||
|
// Used on Gen 5 servers and above
|
||||||
|
private EnetConnection enetConnection;
|
||||||
|
|
||||||
|
// Used on Gen 4 servers and below
|
||||||
private Socket s;
|
private Socket s;
|
||||||
private InputStream in;
|
private InputStream in;
|
||||||
private OutputStream out;
|
private OutputStream out;
|
||||||
@ -163,26 +169,43 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
|
|
||||||
public void initialize() throws IOException
|
public void initialize() throws IOException
|
||||||
{
|
{
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
enetConnection = EnetConnection.connect(context.serverAddress.getHostAddress(), UDP_PORT, CONTROL_TIMEOUT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
s = new Socket();
|
s = new Socket();
|
||||||
s.setTcpNoDelay(true);
|
s.setTcpNoDelay(true);
|
||||||
s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROL_TIMEOUT);
|
s.connect(new InetSocketAddress(context.serverAddress, TCP_PORT), CONTROL_TIMEOUT);
|
||||||
in = s.getInputStream();
|
in = s.getInputStream();
|
||||||
out = s.getOutputStream();
|
out = s.getOutputStream();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void sendPacket(NvCtlPacket packet) throws IOException
|
private void sendPacket(NvCtlPacket packet) throws IOException
|
||||||
{
|
{
|
||||||
// Prevent multiple clients from writing to the stream at the same time
|
// Prevent multiple clients from writing to the stream at the same time
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
packet.write(enetConnection);
|
||||||
|
}
|
||||||
|
else {
|
||||||
packet.write(out);
|
packet.write(out);
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ControlStream.NvCtlResponse sendAndGetReply(NvCtlPacket packet) throws IOException
|
private void sendAndDiscardReply(NvCtlPacket packet) throws IOException
|
||||||
{
|
{
|
||||||
|
synchronized (this) {
|
||||||
sendPacket(packet);
|
sendPacket(packet);
|
||||||
return new NvCtlResponse(in);
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
enetConnection.readPacket(CONTROL_TIMEOUT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new NvCtlResponse(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendLossStats(ByteBuffer bb) throws IOException
|
private void sendLossStats(ByteBuffer bb) throws IOException
|
||||||
@ -200,6 +223,10 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
payloadLengths[IDX_LOSS_STATS], bb.array()));
|
payloadLengths[IDX_LOSS_STATS], bb.array()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendInputPacket(byte[] data, short length) throws IOException {
|
||||||
|
sendPacket(new NvCtlPacket((short) 0x0207, length, data));
|
||||||
|
}
|
||||||
|
|
||||||
public void abort()
|
public void abort()
|
||||||
{
|
{
|
||||||
if (aborting) {
|
if (aborting) {
|
||||||
@ -208,9 +235,17 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
|
|
||||||
aborting = true;
|
aborting = true;
|
||||||
|
|
||||||
|
if (s != null) {
|
||||||
try {
|
try {
|
||||||
s.close();
|
s.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enetConnection != null) {
|
||||||
|
try {
|
||||||
|
enetConnection.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (lossStatsThread != null) {
|
if (lossStatsThread != null) {
|
||||||
lossStatsThread.interrupt();
|
lossStatsThread.interrupt();
|
||||||
@ -232,13 +267,17 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
public void start() throws IOException
|
public void start() throws IOException
|
||||||
{
|
{
|
||||||
// Use a finite timeout during the handshake process
|
// Use a finite timeout during the handshake process
|
||||||
|
if (s != null) {
|
||||||
s.setSoTimeout(CONTROL_TIMEOUT);
|
s.setSoTimeout(CONTROL_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
doStartA();
|
doStartA();
|
||||||
doStartB();
|
doStartB();
|
||||||
|
|
||||||
// Return to an infinte read timeout after the initial control handshake
|
// Return to an infinte read timeout after the initial control handshake
|
||||||
|
if (s != null) {
|
||||||
s.setSoTimeout(0);
|
s.setSoTimeout(0);
|
||||||
|
}
|
||||||
|
|
||||||
lossStatsThread = new Thread() {
|
lossStatsThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
@ -333,14 +372,14 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
resyncThread.start();
|
resyncThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlStream.NvCtlResponse doStartA() throws IOException
|
private void doStartA() throws IOException
|
||||||
{
|
{
|
||||||
return sendAndGetReply(new NvCtlPacket(packetTypes[IDX_START_A],
|
sendAndDiscardReply(new NvCtlPacket(packetTypes[IDX_START_A],
|
||||||
(short) preconstructedPayloads[IDX_START_A].length,
|
(short) preconstructedPayloads[IDX_START_A].length,
|
||||||
preconstructedPayloads[IDX_START_A]));
|
preconstructedPayloads[IDX_START_A]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlStream.NvCtlResponse doStartB() throws IOException
|
private void doStartB() throws IOException
|
||||||
{
|
{
|
||||||
// Gen 3 and 5 both use a packet of this form
|
// Gen 3 and 5 both use a packet of this form
|
||||||
if (context.serverGeneration != ConnectionContext.SERVER_GENERATION_4) {
|
if (context.serverGeneration != ConnectionContext.SERVER_GENERATION_4) {
|
||||||
@ -351,11 +390,11 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
payload.putInt(0);
|
payload.putInt(0);
|
||||||
payload.putInt(0xa);
|
payload.putInt(0xa);
|
||||||
|
|
||||||
return sendAndGetReply(new NvCtlPacket(packetTypes[IDX_START_B],
|
sendAndDiscardReply(new NvCtlPacket(packetTypes[IDX_START_B],
|
||||||
payloadLengths[IDX_START_B], payload.array()));
|
payloadLengths[IDX_START_B], payload.array()));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return sendAndGetReply(new NvCtlPacket(packetTypes[IDX_START_B],
|
sendAndDiscardReply(new NvCtlPacket(packetTypes[IDX_START_B],
|
||||||
(short) preconstructedPayloads[IDX_START_B].length,
|
(short) preconstructedPayloads[IDX_START_B].length,
|
||||||
preconstructedPayloads[IDX_START_B]));
|
preconstructedPayloads[IDX_START_B]));
|
||||||
}
|
}
|
||||||
@ -386,11 +425,11 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
}
|
}
|
||||||
conf.putLong(0);
|
conf.putLong(0);
|
||||||
|
|
||||||
sendAndGetReply(new NvCtlPacket(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
sendAndDiscardReply(new NvCtlPacket(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
||||||
payloadLengths[IDX_INVALIDATE_REF_FRAMES], conf.array()));
|
payloadLengths[IDX_INVALIDATE_REF_FRAMES], conf.array()));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendAndGetReply(new NvCtlPacket(packetTypes[IDX_REQUEST_IDR_FRAME],
|
sendAndDiscardReply(new NvCtlPacket(packetTypes[IDX_REQUEST_IDR_FRAME],
|
||||||
(short) preconstructedPayloads[IDX_REQUEST_IDR_FRAME].length,
|
(short) preconstructedPayloads[IDX_REQUEST_IDR_FRAME].length,
|
||||||
preconstructedPayloads[IDX_REQUEST_IDR_FRAME]));
|
preconstructedPayloads[IDX_REQUEST_IDR_FRAME]));
|
||||||
}
|
}
|
||||||
@ -407,7 +446,7 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
conf.putLong(nextSuccessfulFrame);
|
conf.putLong(nextSuccessfulFrame);
|
||||||
conf.putLong(0);
|
conf.putLong(0);
|
||||||
|
|
||||||
sendAndGetReply(new NvCtlPacket(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
sendAndDiscardReply(new NvCtlPacket(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
||||||
payloadLengths[IDX_INVALIDATE_REF_FRAMES], conf.array()));
|
payloadLengths[IDX_INVALIDATE_REF_FRAMES], conf.array()));
|
||||||
|
|
||||||
LimeLog.warning("Reference frame invalidation sent");
|
LimeLog.warning("Reference frame invalidation sent");
|
||||||
@ -419,7 +458,7 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
public byte[] payload;
|
public byte[] payload;
|
||||||
|
|
||||||
private static final ByteBuffer headerBuffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
|
private static final ByteBuffer headerBuffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
private static final ByteBuffer serializationBuffer = ByteBuffer.allocate(128).order(ByteOrder.LITTLE_ENDIAN);
|
private static final ByteBuffer serializationBuffer = ByteBuffer.allocate(256).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
public NvCtlPacket(InputStream in) throws IOException
|
public NvCtlPacket(InputStream in) throws IOException
|
||||||
{
|
{
|
||||||
@ -529,6 +568,18 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
out.write(serializationBuffer.array(), 0, serializationBuffer.position());
|
out.write(serializationBuffer.array(), 0, serializationBuffer.position());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void write(EnetConnection conn) throws IOException
|
||||||
|
{
|
||||||
|
// Use the class's serialization buffer to construct the wireform to send
|
||||||
|
synchronized (serializationBuffer) {
|
||||||
|
serializationBuffer.rewind();
|
||||||
|
serializationBuffer.putShort(type);
|
||||||
|
serializationBuffer.put(payload);
|
||||||
|
|
||||||
|
conn.writePacket(serializationBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NvCtlResponse extends NvCtlPacket {
|
class NvCtlResponse extends NvCtlPacket {
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.nvstream.control;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface InputPacketSender {
|
||||||
|
void sendInputPacket(byte[] data, short length) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package com.limelight.nvstream.enet;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class EnetConnection implements Closeable {
|
||||||
|
private long enetPeer;
|
||||||
|
private long enetClient;
|
||||||
|
|
||||||
|
private static final int ENET_PACKET_FLAG_RELIABLE = 1;
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("jnienet");
|
||||||
|
|
||||||
|
initializeEnet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnetConnection() {}
|
||||||
|
|
||||||
|
public static EnetConnection connect(String host, int port, int timeout) throws IOException {
|
||||||
|
EnetConnection conn = new EnetConnection();
|
||||||
|
|
||||||
|
conn.enetClient = createClient();
|
||||||
|
if (conn.enetClient == 0) {
|
||||||
|
throw new IOException("Unable to create ENet client");
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.enetPeer = connectToPeer(conn.enetClient, host, port, timeout);
|
||||||
|
if (conn.enetPeer == 0) {
|
||||||
|
try {
|
||||||
|
conn.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
throw new IOException("Unable to connect to UDP port "+port);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer readPacket(int timeout) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(128);
|
||||||
|
|
||||||
|
int readLength = readPacket(enetClient, buffer.array(), buffer.limit(), timeout);
|
||||||
|
if (readLength <= 0) {
|
||||||
|
throw new IOException("Failed to receive ENet packet");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.limit(readLength);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writePacket(ByteBuffer buffer) throws IOException {
|
||||||
|
if (!writePacket(enetClient, enetPeer, buffer.array(), buffer.position(), ENET_PACKET_FLAG_RELIABLE)) {
|
||||||
|
throw new IOException("Failed to send ENet packet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (enetPeer != 0) {
|
||||||
|
disconnectPeer(enetPeer);
|
||||||
|
enetPeer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enetClient != 0) {
|
||||||
|
destroyClient(enetClient);
|
||||||
|
enetClient = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native int initializeEnet();
|
||||||
|
private static native long createClient();
|
||||||
|
private static native long connectToPeer(long client, String host, int port, int timeout);
|
||||||
|
private static native int readPacket(long client, byte[] data, int length, int timeout);
|
||||||
|
private static native boolean writePacket(long client, long peer, byte[] data, int length, int packetFlags);
|
||||||
|
private static native void destroyClient(long client);
|
||||||
|
private static native void disconnectPeer(long peer);
|
||||||
|
}
|
@ -17,6 +17,7 @@ import javax.crypto.NoSuchPaddingException;
|
|||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
|
||||||
import com.limelight.nvstream.ConnectionContext;
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
|
import com.limelight.nvstream.control.InputPacketSender;
|
||||||
|
|
||||||
public class ControllerStream {
|
public class ControllerStream {
|
||||||
|
|
||||||
@ -26,8 +27,13 @@ public class ControllerStream {
|
|||||||
|
|
||||||
private ConnectionContext context;
|
private ConnectionContext context;
|
||||||
|
|
||||||
|
// Only used on Gen 4 or below servers
|
||||||
private Socket s;
|
private Socket s;
|
||||||
private OutputStream out;
|
private OutputStream out;
|
||||||
|
|
||||||
|
// Used on Gen 5+ servers
|
||||||
|
private InputPacketSender controlSender;
|
||||||
|
|
||||||
private Cipher riCipher;
|
private Cipher riCipher;
|
||||||
|
|
||||||
private Thread inputThread;
|
private Thread inputThread;
|
||||||
@ -58,13 +64,20 @@ public class ControllerStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() throws IOException
|
public void initialize(InputPacketSender controlSender) throws IOException
|
||||||
{
|
{
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
// Gen 5 sends input over the control stream
|
||||||
|
this.controlSender = controlSender;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Gen 4 and below uses a separate TCP connection for input
|
||||||
s = new Socket();
|
s = new Socket();
|
||||||
s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROLLER_TIMEOUT);
|
s.connect(new InetSocketAddress(context.serverAddress, PORT), CONTROLLER_TIMEOUT);
|
||||||
s.setTcpNoDelay(true);
|
s.setTcpNoDelay(true);
|
||||||
out = s.getOutputStream();
|
out = s.getOutputStream();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void start()
|
public void start()
|
||||||
{
|
{
|
||||||
@ -197,10 +210,12 @@ public class ControllerStream {
|
|||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s != null) {
|
||||||
try {
|
try {
|
||||||
s.close();
|
s.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException e) {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int getPaddedSize(int length) {
|
private static int getPaddedSize(int length) {
|
||||||
return ((length + 15) / 16) * 16;
|
return ((length + 15) / 16) * 16;
|
||||||
@ -249,10 +264,16 @@ public class ControllerStream {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the packet
|
// Send the packet over the control stream on Gen 5+
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
controlSender.sendInputPacket(sendBuffer.array(), (short) (paddedLength + 4));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Send the packet over the TCP connection on Gen 4 and below
|
||||||
out.write(sendBuffer.array(), 0, paddedLength + 4);
|
out.write(sendBuffer.array(), 0, paddedLength + 4);
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void queuePacket(InputPacket packet) {
|
private void queuePacket(InputPacket packet) {
|
||||||
synchronized (inputQueue) {
|
synchronized (inputQueue) {
|
||||||
|
@ -58,10 +58,11 @@ public class SdpGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void addGen5Attributes(StringBuilder config, ConnectionContext context) {
|
private static void addGen5Attributes(StringBuilder config, ConnectionContext context) {
|
||||||
// We want to use the legacy TCP connections for control and input rather than the new UDP stuff
|
// We want to use the new ENet connections for control and input
|
||||||
addSessionAttribute(config, "x-nv-general.useReliableUdp", "0");
|
addSessionAttribute(config, "x-nv-general.useReliableUdp", "1");
|
||||||
addSessionAttribute(config, "x-nv-ri.useControlChannel", "0");
|
addSessionAttribute(config, "x-nv-ri.useControlChannel", "1");
|
||||||
|
|
||||||
|
// Disable dynamic resolution switching
|
||||||
addSessionAttribute(config, "x-nv-vqos[0].drc.enable", "0");
|
addSessionAttribute(config, "x-nv-vqos[0].drc.enable", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user