mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-20 19:42:45 +00:00
Add support for Generation 5 servers (GFE 2.10.2+)
This commit is contained in:
parent
5718c47be7
commit
d9cb5eacf8
@ -14,6 +14,9 @@ public class ConnectionContext {
|
|||||||
// Gen 4 servers are 2.2.2+
|
// Gen 4 servers are 2.2.2+
|
||||||
public static final int SERVER_GENERATION_4 = 4;
|
public static final int SERVER_GENERATION_4 = 4;
|
||||||
|
|
||||||
|
// Gen 5 servers are 2.10.2+
|
||||||
|
public static final int SERVER_GENERATION_5 = 5;
|
||||||
|
|
||||||
public InetAddress serverAddress;
|
public InetAddress serverAddress;
|
||||||
public StreamConfiguration streamConfig;
|
public StreamConfiguration streamConfig;
|
||||||
public VideoDecoderRenderer videoDecoderRenderer;
|
public VideoDecoderRenderer videoDecoderRenderer;
|
||||||
|
@ -116,7 +116,7 @@ public class NvConnection {
|
|||||||
context.connListener.displayMessage("This app requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again.");
|
context.connListener.displayMessage("This app requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (majorVersion > 4) {
|
else if (majorVersion > 5) {
|
||||||
// Warn the user but allow them to continue
|
// Warn the user but allow them to continue
|
||||||
context.connListener.displayTransientMessage("This version of GFE is not currently supported. You may experience issues until this app is updated.");
|
context.connListener.displayTransientMessage("This version of GFE is not currently supported. You may experience issues until this app is updated.");
|
||||||
}
|
}
|
||||||
@ -126,9 +126,12 @@ public class NvConnection {
|
|||||||
context.serverGeneration = ConnectionContext.SERVER_GENERATION_3;
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_3;
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
default:
|
|
||||||
context.serverGeneration = ConnectionContext.SERVER_GENERATION_4;
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_4;
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
default:
|
||||||
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_5;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Server major version: "+majorVersion);
|
LimeLog.info("Server major version: "+majorVersion);
|
||||||
|
@ -7,7 +7,9 @@ public interface ConnectionStatusListener {
|
|||||||
|
|
||||||
public void connectionSinkTooSlow(int firstLostFrame, int nextSuccessfulFrame);
|
public void connectionSinkTooSlow(int firstLostFrame, int nextSuccessfulFrame);
|
||||||
|
|
||||||
public void connectionReceivedFrame(int frameIndex);
|
public void connectionReceivedCompleteFrame(int frameIndex);
|
||||||
|
|
||||||
|
public void connectionSawFrame(int frameIndex);
|
||||||
|
|
||||||
public void connectionLostPackets(int lastReceivedPacket, int nextReceivedPacket);
|
public void connectionLostPackets(int lastReceivedPacket, int nextReceivedPacket);
|
||||||
}
|
}
|
||||||
|
@ -43,11 +43,22 @@ public class VideoDepacketizer {
|
|||||||
private static final int DU_LIMIT = 15;
|
private static final int DU_LIMIT = 15;
|
||||||
private AbstractPopulatedBufferList<DecodeUnit> decodedUnits;
|
private AbstractPopulatedBufferList<DecodeUnit> decodedUnits;
|
||||||
|
|
||||||
|
private final int frameHeaderOffset;
|
||||||
|
|
||||||
public VideoDepacketizer(ConnectionContext context, ConnectionStatusListener controlListener, int nominalPacketSize)
|
public VideoDepacketizer(ConnectionContext context, ConnectionStatusListener controlListener, int nominalPacketSize)
|
||||||
{
|
{
|
||||||
this.controlListener = controlListener;
|
this.controlListener = controlListener;
|
||||||
this.nominalPacketDataLength = nominalPacketSize - VideoPacket.HEADER_SIZE;
|
this.nominalPacketDataLength = nominalPacketSize - VideoPacket.HEADER_SIZE;
|
||||||
|
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
// Gen 5 servers have an 8 byte header in the data portion of the first
|
||||||
|
// packet of each frame
|
||||||
|
frameHeaderOffset = 8;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frameHeaderOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
boolean unsynchronized;
|
boolean unsynchronized;
|
||||||
if (context.videoDecoderRenderer != null) {
|
if (context.videoDecoderRenderer != null) {
|
||||||
int videoCaps = context.videoDecoderRenderer.getCapabilities();
|
int videoCaps = context.videoDecoderRenderer.getCapabilities();
|
||||||
@ -177,7 +188,7 @@ public class VideoDepacketizer {
|
|||||||
// Packets now owned by the DU
|
// Packets now owned by the DU
|
||||||
backingPacketTail = backingPacketHead = null;
|
backingPacketTail = backingPacketHead = null;
|
||||||
|
|
||||||
controlListener.connectionReceivedFrame(frameNumber);
|
controlListener.connectionReceivedCompleteFrame(frameNumber);
|
||||||
|
|
||||||
// Submit the DU to the consumer
|
// Submit the DU to the consumer
|
||||||
decodedUnits.addPopulatedObject(du);
|
decodedUnits.addPopulatedObject(du);
|
||||||
@ -367,6 +378,9 @@ public class VideoDepacketizer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify the listener of the latest frame we've seen from the PC
|
||||||
|
controlListener.connectionSawFrame(frameIndex);
|
||||||
|
|
||||||
// Look for a frame start before receiving a frame end
|
// Look for a frame start before receiving a frame end
|
||||||
if (firstPacket && decodingFrame)
|
if (firstPacket && decodingFrame)
|
||||||
{
|
{
|
||||||
@ -446,6 +460,12 @@ public class VideoDepacketizer {
|
|||||||
}
|
}
|
||||||
lastPacketInStream = streamPacketIndex;
|
lastPacketInStream = streamPacketIndex;
|
||||||
|
|
||||||
|
// If this is the first packet, skip the frame header (if one exists)
|
||||||
|
if (firstPacket) {
|
||||||
|
cachedReassemblyDesc.offset += frameHeaderOffset;
|
||||||
|
cachedReassemblyDesc.length -= frameHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
if (firstPacket && isIdrFrameStart(cachedReassemblyDesc))
|
if (firstPacket && isIdrFrameStart(cachedReassemblyDesc))
|
||||||
{
|
{
|
||||||
// The slow path doesn't update the frame start time by itself
|
// The slow path doesn't update the frame start time by itself
|
||||||
|
@ -41,6 +41,13 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
0x060a, // Loss Stats
|
0x060a, // Loss Stats
|
||||||
0x0611, // Frame Stats (unused)
|
0x0611, // Frame Stats (unused)
|
||||||
};
|
};
|
||||||
|
private static final short packetTypesGen5[] = {
|
||||||
|
0x0305, // Start A
|
||||||
|
0x0307, // Start B
|
||||||
|
0x0301, // Invalidate reference frames
|
||||||
|
0x0201, // Loss Stats
|
||||||
|
0x0204, // Frame Stats (unused)
|
||||||
|
};
|
||||||
|
|
||||||
private static final short payloadLengthsGen3[] = {
|
private static final short payloadLengthsGen3[] = {
|
||||||
-1, // Start A
|
-1, // Start A
|
||||||
@ -56,6 +63,13 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
32, // Loss Stats
|
32, // Loss Stats
|
||||||
64, // Frame Stats
|
64, // Frame Stats
|
||||||
};
|
};
|
||||||
|
private static final short payloadLengthsGen5[] = {
|
||||||
|
-1, // Start A
|
||||||
|
16, // Start B
|
||||||
|
24, // Invalidate reference frames
|
||||||
|
32, // Loss Stats
|
||||||
|
80, // Frame Stats
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] precontructedPayloadsGen3[] = {
|
private static final byte[] precontructedPayloadsGen3[] = {
|
||||||
new byte[]{0}, // Start A
|
new byte[]{0}, // Start A
|
||||||
@ -71,10 +85,18 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
null, // Loss Stats
|
null, // Loss Stats
|
||||||
null, // Frame Stats
|
null, // Frame Stats
|
||||||
};
|
};
|
||||||
|
private static final byte[] precontructedPayloadsGen5[] = {
|
||||||
|
new byte[]{0, 0}, // Start A
|
||||||
|
null, // Start B
|
||||||
|
null, // Invalidate reference frames
|
||||||
|
null, // Loss Stats
|
||||||
|
null, // Frame Stats
|
||||||
|
};
|
||||||
|
|
||||||
public static final int LOSS_REPORT_INTERVAL_MS = 50;
|
public static final int LOSS_REPORT_INTERVAL_MS = 50;
|
||||||
|
|
||||||
private int currentFrame;
|
private int lastGoodFrame;
|
||||||
|
private int lastSeenFrame;
|
||||||
private int lossCountSinceLastReport;
|
private int lossCountSinceLastReport;
|
||||||
|
|
||||||
private ConnectionContext context;
|
private ConnectionContext context;
|
||||||
@ -121,11 +143,16 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
preconstructedPayloads = precontructedPayloadsGen3;
|
preconstructedPayloads = precontructedPayloadsGen3;
|
||||||
break;
|
break;
|
||||||
case ConnectionContext.SERVER_GENERATION_4:
|
case ConnectionContext.SERVER_GENERATION_4:
|
||||||
default:
|
|
||||||
packetTypes = packetTypesGen4;
|
packetTypes = packetTypesGen4;
|
||||||
payloadLengths = payloadLengthsGen4;
|
payloadLengths = payloadLengthsGen4;
|
||||||
preconstructedPayloads = precontructedPayloadsGen4;
|
preconstructedPayloads = precontructedPayloadsGen4;
|
||||||
break;
|
break;
|
||||||
|
case ConnectionContext.SERVER_GENERATION_5:
|
||||||
|
default:
|
||||||
|
packetTypes = packetTypesGen5;
|
||||||
|
payloadLengths = payloadLengthsGen5;
|
||||||
|
preconstructedPayloads = precontructedPayloadsGen5;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.videoDecoderRenderer != null) {
|
if (context.videoDecoderRenderer != null) {
|
||||||
@ -164,7 +191,7 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
bb.putInt(lossCountSinceLastReport); // Packet loss count
|
bb.putInt(lossCountSinceLastReport); // Packet loss count
|
||||||
bb.putInt(LOSS_REPORT_INTERVAL_MS); // Time since last report in milliseconds
|
bb.putInt(LOSS_REPORT_INTERVAL_MS); // Time since last report in milliseconds
|
||||||
bb.putInt(1000);
|
bb.putInt(1000);
|
||||||
bb.putLong(currentFrame); // Last successfully received frame
|
bb.putLong(lastGoodFrame); // Last successfully received frame
|
||||||
bb.putInt(0);
|
bb.putInt(0);
|
||||||
bb.putInt(0);
|
bb.putInt(0);
|
||||||
bb.putInt(0x14);
|
bb.putInt(0x14);
|
||||||
@ -315,7 +342,8 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
|
|
||||||
private ControlStream.NvCtlResponse doStartB() throws IOException
|
private ControlStream.NvCtlResponse doStartB() throws IOException
|
||||||
{
|
{
|
||||||
if (context.serverGeneration == ConnectionContext.SERVER_GENERATION_3) {
|
// Gen 3 and 5 both use a packet of this form
|
||||||
|
if (context.serverGeneration != ConnectionContext.SERVER_GENERATION_4) {
|
||||||
ByteBuffer payload = ByteBuffer.wrap(new byte[payloadLengths[IDX_START_B]]).order(ByteOrder.LITTLE_ENDIAN);
|
ByteBuffer payload = ByteBuffer.wrap(new byte[payloadLengths[IDX_START_B]]).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
payload.putInt(0);
|
payload.putInt(0);
|
||||||
@ -334,16 +362,28 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void requestIdrFrame() throws IOException {
|
private void requestIdrFrame() throws IOException {
|
||||||
// On Gen 3, we use the invalidate reference frames trick which works for about 5 hours of streaming at 60 FPS
|
// On Gen 3, we use the invalidate reference frames trick.
|
||||||
// On Gen 4+, we use the known IDR frame request packet
|
// On Gen 4+, we use the known IDR frame request packet
|
||||||
|
// On Gen 5, we're currently using the invalidate reference frames trick again.
|
||||||
|
|
||||||
if (context.serverGeneration == ConnectionContext.SERVER_GENERATION_3) {
|
if (context.serverGeneration != ConnectionContext.SERVER_GENERATION_4) {
|
||||||
ByteBuffer conf = ByteBuffer.wrap(new byte[payloadLengths[IDX_INVALIDATE_REF_FRAMES]]).order(ByteOrder.LITTLE_ENDIAN);
|
ByteBuffer conf = ByteBuffer.wrap(new byte[payloadLengths[IDX_INVALIDATE_REF_FRAMES]]).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
//conf.putLong(firstLostFrame);
|
//conf.putLong(firstLostFrame);
|
||||||
//conf.putLong(nextSuccessfulFrame);
|
//conf.putLong(nextSuccessfulFrame);
|
||||||
conf.putLong(0);
|
|
||||||
conf.putLong(0xFFFFF);
|
// Early on, we'll use a special IDR sequence. Otherwise,
|
||||||
|
// we'll just say we lost the last 32 frames. This is larger
|
||||||
|
// than the number of buffered frames in the encoder (16) so
|
||||||
|
// it should trigger an IDR frame.
|
||||||
|
if (lastSeenFrame < 0x20) {
|
||||||
|
conf.putLong(0);
|
||||||
|
conf.putLong(0x20);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conf.putLong(lastSeenFrame - 0x20);
|
||||||
|
conf.putLong(lastSeenFrame);
|
||||||
|
}
|
||||||
conf.putLong(0);
|
conf.putLong(0);
|
||||||
|
|
||||||
sendAndGetReply(new NvCtlPacket(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
sendAndGetReply(new NvCtlPacket(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
||||||
@ -534,7 +574,7 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
|
|
||||||
// Suppress connection warnings for the first 150 frames to allow the connection
|
// Suppress connection warnings for the first 150 frames to allow the connection
|
||||||
// to stabilize
|
// to stabilize
|
||||||
if (currentFrame < 150) {
|
if (lastGoodFrame < 150) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,7 +609,7 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
|
|
||||||
// Suppress connection warnings for the first 150 frames to allow the connection
|
// Suppress connection warnings for the first 150 frames to allow the connection
|
||||||
// to stabilize
|
// to stabilize
|
||||||
if (currentFrame < 150) {
|
if (lastGoodFrame < 150) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,8 +619,12 @@ public class ControlStream implements ConnectionStatusListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connectionReceivedFrame(int frameIndex) {
|
public void connectionReceivedCompleteFrame(int frameIndex) {
|
||||||
currentFrame = frameIndex;
|
lastGoodFrame = frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connectionSawFrame(int frameIndex) {
|
||||||
|
lastSeenFrame = frameIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connectionLostPackets(int lastReceivedPacket, int nextReceivedPacket) {
|
public void connectionLostPackets(int lastReceivedPacket, int nextReceivedPacket) {
|
||||||
|
@ -271,7 +271,7 @@ public class ControllerStream {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Use multi-controller packets for generation 4 and above
|
// Use multi-controller packets for generation 4 and above
|
||||||
queuePacket(new MultiControllerPacket((short) 0, buttonFlags, leftTrigger,
|
queuePacket(new MultiControllerPacket(context, (short) 0, buttonFlags, leftTrigger,
|
||||||
rightTrigger, leftStickX, leftStickY,
|
rightTrigger, leftStickX, leftStickY,
|
||||||
rightStickX, rightStickY));
|
rightStickX, rightStickY));
|
||||||
}
|
}
|
||||||
@ -288,7 +288,7 @@ public class ControllerStream {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Use multi-controller packets for generation 4 and above
|
// Use multi-controller packets for generation 4 and above
|
||||||
queuePacket(new MultiControllerPacket(controllerNumber, buttonFlags, leftTrigger,
|
queuePacket(new MultiControllerPacket(context, controllerNumber, buttonFlags, leftTrigger,
|
||||||
rightTrigger, leftStickX, leftStickY,
|
rightTrigger, leftStickX, leftStickY,
|
||||||
rightStickX, rightStickY));
|
rightStickX, rightStickY));
|
||||||
}
|
}
|
||||||
@ -296,17 +296,17 @@ public class ControllerStream {
|
|||||||
|
|
||||||
public void sendMouseButtonDown(byte mouseButton)
|
public void sendMouseButtonDown(byte mouseButton)
|
||||||
{
|
{
|
||||||
queuePacket(new MouseButtonPacket(true, mouseButton));
|
queuePacket(new MouseButtonPacket(context, true, mouseButton));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMouseButtonUp(byte mouseButton)
|
public void sendMouseButtonUp(byte mouseButton)
|
||||||
{
|
{
|
||||||
queuePacket(new MouseButtonPacket(false, mouseButton));
|
queuePacket(new MouseButtonPacket(context, false, mouseButton));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMouseMove(short deltaX, short deltaY)
|
public void sendMouseMove(short deltaX, short deltaY)
|
||||||
{
|
{
|
||||||
queuePacket(new MouseMovePacket(deltaX, deltaY));
|
queuePacket(new MouseMovePacket(context, deltaX, deltaY));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier)
|
public void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier)
|
||||||
@ -316,6 +316,6 @@ public class ControllerStream {
|
|||||||
|
|
||||||
public void sendMouseScroll(byte scrollClicks)
|
public void sendMouseScroll(byte scrollClicks)
|
||||||
{
|
{
|
||||||
queuePacket(new MouseScrollPacket(scrollClicks));
|
queuePacket(new MouseScrollPacket(context, scrollClicks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@ public class KeyboardPacket extends InputPacket {
|
|||||||
public static final byte MODIFIER_CTRL = 0x02;
|
public static final byte MODIFIER_CTRL = 0x02;
|
||||||
public static final byte MODIFIER_ALT = 0x04;
|
public static final byte MODIFIER_ALT = 0x04;
|
||||||
|
|
||||||
short keyCode;
|
private short keyCode;
|
||||||
byte keyDirection;
|
private byte keyDirection;
|
||||||
byte modifier;
|
private byte modifier;
|
||||||
|
|
||||||
public KeyboardPacket(short keyCode, byte keyDirection, byte modifier) {
|
public KeyboardPacket(short keyCode, byte keyDirection, byte modifier) {
|
||||||
super(PACKET_TYPE);
|
super(PACKET_TYPE);
|
||||||
|
@ -3,6 +3,8 @@ package com.limelight.nvstream.input;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
|
|
||||||
public class MouseButtonPacket extends InputPacket {
|
public class MouseButtonPacket extends InputPacket {
|
||||||
|
|
||||||
byte buttonEventType;
|
byte buttonEventType;
|
||||||
@ -20,7 +22,7 @@ public class MouseButtonPacket extends InputPacket {
|
|||||||
public static final byte BUTTON_MIDDLE = 0x02;
|
public static final byte BUTTON_MIDDLE = 0x02;
|
||||||
public static final byte BUTTON_RIGHT = 0x03;
|
public static final byte BUTTON_RIGHT = 0x03;
|
||||||
|
|
||||||
public MouseButtonPacket(boolean buttonDown, byte mouseButton)
|
public MouseButtonPacket(ConnectionContext context, boolean buttonDown, byte mouseButton)
|
||||||
{
|
{
|
||||||
super(PACKET_TYPE);
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
@ -28,6 +30,11 @@ public class MouseButtonPacket extends InputPacket {
|
|||||||
|
|
||||||
buttonEventType = buttonDown ?
|
buttonEventType = buttonDown ?
|
||||||
PRESS_EVENT : RELEASE_EVENT;
|
PRESS_EVENT : RELEASE_EVENT;
|
||||||
|
|
||||||
|
// On Gen 5 servers, the button event codes are incremented by one
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
buttonEventType++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -3,36 +3,40 @@ package com.limelight.nvstream.input;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
|
|
||||||
public class MouseMovePacket extends InputPacket {
|
public class MouseMovePacket extends InputPacket {
|
||||||
|
private static final int HEADER_CODE = 0x06;
|
||||||
private static final byte[] HEADER =
|
private static final int PACKET_TYPE = 0x8;
|
||||||
{
|
private static final int PAYLOAD_LENGTH = 8;
|
||||||
0x06,
|
private static final int PACKET_LENGTH = PAYLOAD_LENGTH +
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final int PACKET_TYPE = 0x8;
|
|
||||||
public static final int PAYLOAD_LENGTH = 8;
|
|
||||||
public static final int PACKET_LENGTH = PAYLOAD_LENGTH +
|
|
||||||
InputPacket.HEADER_LENGTH;
|
InputPacket.HEADER_LENGTH;
|
||||||
|
|
||||||
|
private int headerCode;
|
||||||
|
|
||||||
|
// Accessed in ControllerStream for batching
|
||||||
short deltaX;
|
short deltaX;
|
||||||
short deltaY;
|
short deltaY;
|
||||||
|
|
||||||
public MouseMovePacket(short deltaX, short deltaY)
|
public MouseMovePacket(ConnectionContext context, short deltaX, short deltaY)
|
||||||
{
|
{
|
||||||
super(PACKET_TYPE);
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
|
this.headerCode = HEADER_CODE;
|
||||||
|
|
||||||
|
// On Gen 5 servers, the header code is incremented by one
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
headerCode++;
|
||||||
|
}
|
||||||
|
|
||||||
this.deltaX = deltaX;
|
this.deltaX = deltaX;
|
||||||
this.deltaY = deltaY;
|
this.deltaY = deltaY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void toWirePayload(ByteBuffer bb) {
|
public void toWirePayload(ByteBuffer bb) {
|
||||||
|
bb.order(ByteOrder.LITTLE_ENDIAN).putInt(headerCode);
|
||||||
bb.order(ByteOrder.BIG_ENDIAN);
|
bb.order(ByteOrder.BIG_ENDIAN);
|
||||||
bb.put(HEADER);
|
|
||||||
bb.putShort(deltaX);
|
bb.putShort(deltaX);
|
||||||
bb.putShort(deltaY);
|
bb.putShort(deltaY);
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,38 @@ package com.limelight.nvstream.input;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
|
|
||||||
public class MouseScrollPacket extends InputPacket {
|
public class MouseScrollPacket extends InputPacket {
|
||||||
|
private static final int HEADER_CODE = 0x09;
|
||||||
private static final int PACKET_TYPE = 0xa;
|
private static final int PACKET_TYPE = 0xa;
|
||||||
private static final int PAYLOAD_LENGTH = 10;
|
private static final int PAYLOAD_LENGTH = 10;
|
||||||
private static final int PACKET_LENGTH = PAYLOAD_LENGTH +
|
private static final int PACKET_LENGTH = PAYLOAD_LENGTH +
|
||||||
InputPacket.HEADER_LENGTH;
|
InputPacket.HEADER_LENGTH;
|
||||||
|
|
||||||
short scroll;
|
|
||||||
|
|
||||||
public MouseScrollPacket(byte scrollClicks)
|
private int headerCode;
|
||||||
|
private short scroll;
|
||||||
|
|
||||||
|
public MouseScrollPacket(ConnectionContext context, byte scrollClicks)
|
||||||
{
|
{
|
||||||
super(PACKET_TYPE);
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
|
this.headerCode = HEADER_CODE;
|
||||||
|
|
||||||
|
// On Gen 5 servers, the header code is incremented by one
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
headerCode++;
|
||||||
|
}
|
||||||
|
|
||||||
this.scroll = (short)(scrollClicks * 120);
|
this.scroll = (short)(scrollClicks * 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void toWirePayload(ByteBuffer bb) {
|
public void toWirePayload(ByteBuffer bb) {
|
||||||
bb.order(ByteOrder.BIG_ENDIAN);
|
bb.order(ByteOrder.LITTLE_ENDIAN).putInt(headerCode);
|
||||||
|
|
||||||
bb.put((byte) 0x09);
|
bb.order(ByteOrder.BIG_ENDIAN);
|
||||||
bb.put((byte) 0);
|
|
||||||
bb.put((byte) 0);
|
|
||||||
bb.put((byte) 0);
|
|
||||||
|
|
||||||
bb.putShort(scroll);
|
bb.putShort(scroll);
|
||||||
bb.putShort(scroll);
|
bb.putShort(scroll);
|
||||||
|
@ -3,6 +3,8 @@ package com.limelight.nvstream.input;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
|
|
||||||
public class MultiControllerPacket extends InputPacket {
|
public class MultiControllerPacket extends InputPacket {
|
||||||
private static final byte[] TAIL =
|
private static final byte[] TAIL =
|
||||||
{
|
{
|
||||||
@ -14,6 +16,7 @@ public class MultiControllerPacket extends InputPacket {
|
|||||||
0x00
|
0x00
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static final int HEADER_CODE = 0x0d;
|
||||||
private static final int PACKET_TYPE = 0x1e;
|
private static final int PACKET_TYPE = 0x1e;
|
||||||
|
|
||||||
private static final short PAYLOAD_LENGTH = 30;
|
private static final short PAYLOAD_LENGTH = 30;
|
||||||
@ -29,12 +32,22 @@ public class MultiControllerPacket extends InputPacket {
|
|||||||
short rightStickX;
|
short rightStickX;
|
||||||
short rightStickY;
|
short rightStickY;
|
||||||
|
|
||||||
public MultiControllerPacket(short controllerNumber, short buttonFlags, byte leftTrigger, byte rightTrigger,
|
private int headerCode;
|
||||||
|
|
||||||
|
public MultiControllerPacket(ConnectionContext context,
|
||||||
|
short controllerNumber, short buttonFlags, byte leftTrigger, byte rightTrigger,
|
||||||
short leftStickX, short leftStickY,
|
short leftStickX, short leftStickY,
|
||||||
short rightStickX, short rightStickY)
|
short rightStickX, short rightStickY)
|
||||||
{
|
{
|
||||||
super(PACKET_TYPE);
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
|
this.headerCode = HEADER_CODE;
|
||||||
|
|
||||||
|
// On Gen 5 servers, the header code is decremented by one
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
|
headerCode--;
|
||||||
|
}
|
||||||
|
|
||||||
this.controllerNumber = controllerNumber;
|
this.controllerNumber = controllerNumber;
|
||||||
|
|
||||||
this.buttonFlags = buttonFlags;
|
this.buttonFlags = buttonFlags;
|
||||||
@ -72,7 +85,7 @@ public class MultiControllerPacket extends InputPacket {
|
|||||||
@Override
|
@Override
|
||||||
public void toWirePayload(ByteBuffer bb) {
|
public void toWirePayload(ByteBuffer bb) {
|
||||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
bb.putInt(0xd);
|
bb.putInt(headerCode);
|
||||||
bb.putShort((short) 0x1a);
|
bb.putShort((short) 0x1a);
|
||||||
bb.putShort(controllerNumber);
|
bb.putShort(controllerNumber);
|
||||||
bb.putShort((short) 0x0f); // Active controller flags
|
bb.putShort((short) 0x0f); // Active controller flags
|
||||||
|
@ -40,8 +40,10 @@ public class RtspConnection {
|
|||||||
case ConnectionContext.SERVER_GENERATION_3:
|
case ConnectionContext.SERVER_GENERATION_3:
|
||||||
return 10;
|
return 10;
|
||||||
case ConnectionContext.SERVER_GENERATION_4:
|
case ConnectionContext.SERVER_GENERATION_4:
|
||||||
default:
|
|
||||||
return 11;
|
return 11;
|
||||||
|
case ConnectionContext.SERVER_GENERATION_5:
|
||||||
|
default:
|
||||||
|
return 12;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,36 +54,15 @@ public class SdpGenerator {
|
|||||||
private static void addGen4Attributes(StringBuilder config, ConnectionContext context) {
|
private static void addGen4Attributes(StringBuilder config, ConnectionContext context) {
|
||||||
addSessionAttribute(config, "x-nv-general.serverAddress", "rtsp://"+context.serverAddress.getHostAddress()+":48010");
|
addSessionAttribute(config, "x-nv-general.serverAddress", "rtsp://"+context.serverAddress.getHostAddress()+":48010");
|
||||||
|
|
||||||
// If client and server are able, request HEVC
|
|
||||||
if (context.negotiatedVideoFormat == VideoFormat.H265) {
|
|
||||||
addSessionAttribute(config, "x-nv-clientSupportHevc", "1");
|
|
||||||
addSessionAttribute(config, "x-nv-vqos[0].bitStreamFormat", "1");
|
|
||||||
|
|
||||||
// Disable slicing on HEVC
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].videoEncoderSlicesPerFrame", "1");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Otherwise, use AVC
|
|
||||||
addSessionAttribute(config, "x-nv-clientSupportHevc", "0");
|
|
||||||
addSessionAttribute(config, "x-nv-vqos[0].bitStreamFormat", "0");
|
|
||||||
|
|
||||||
// Use slicing for increased performance on some decoders
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].videoEncoderSlicesPerFrame", "4");
|
|
||||||
}
|
|
||||||
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].rateControlMode", "4");
|
addSessionAttribute(config, "x-nv-video[0].rateControlMode", "4");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
addSessionAttribute(config, "x-nv-general.useReliableUdp", "0");
|
||||||
|
addSessionAttribute(config, "x-nv-ri.useControlChannel", "0");
|
||||||
|
|
||||||
|
addSessionAttribute(config, "x-nv-vqos[0].enableQec", "0");
|
||||||
// Enable surround sound if configured for it
|
|
||||||
addSessionAttribute(config, "x-nv-audio.surround.numChannels", ""+context.streamConfig.getAudioChannelCount());
|
|
||||||
addSessionAttribute(config, "x-nv-audio.surround.channelMask", ""+context.streamConfig.getAudioChannelMask());
|
|
||||||
if (context.streamConfig.getAudioChannelCount() > 2) {
|
|
||||||
addSessionAttribute(config, "x-nv-audio.surround.enable", "1");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
addSessionAttribute(config, "x-nv-audio.surround.enable", "0");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String generateSdpFromContext(ConnectionContext context) {
|
public static String generateSdpFromContext(ConnectionContext context) {
|
||||||
@ -116,11 +95,6 @@ public class SdpGenerator {
|
|||||||
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].packetSize", ""+context.streamConfig.getMaxPacketSize());
|
addSessionAttribute(config, "x-nv-video[0].packetSize", ""+context.streamConfig.getMaxPacketSize());
|
||||||
|
|
||||||
if (context.streamConfig.getRemote()) {
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].averageBitrate", "4");
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].peakBitrate", "4");
|
|
||||||
}
|
|
||||||
|
|
||||||
addSessionAttribute(config, "x-nv-video[0].timeoutLengthMs", "7000");
|
addSessionAttribute(config, "x-nv-video[0].timeoutLengthMs", "7000");
|
||||||
addSessionAttribute(config, "x-nv-video[0].framesWithInvalidRefThreshold", "0");
|
addSessionAttribute(config, "x-nv-video[0].framesWithInvalidRefThreshold", "0");
|
||||||
|
|
||||||
@ -135,11 +109,22 @@ public class SdpGenerator {
|
|||||||
bitrate = context.streamConfig.getBitrate();
|
bitrate = context.streamConfig.getBitrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't support dynamic bitrate scaling properly (it tends to bounce between min and max and never
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_5) {
|
||||||
// settle on the optimal bitrate if it's somewhere in the middle), so we'll just latch the bitrate
|
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrateKbps", ""+bitrate);
|
||||||
// to the requested value.
|
addSessionAttribute(config, "x-nv-vqos[0].bw.maximumBitrateKbps", ""+bitrate);
|
||||||
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+bitrate);
|
}
|
||||||
addSessionAttribute(config, "x-nv-vqos[0].bw.maximumBitrate", ""+bitrate);
|
else {
|
||||||
|
if (context.streamConfig.getRemote()) {
|
||||||
|
addSessionAttribute(config, "x-nv-video[0].averageBitrate", "4");
|
||||||
|
addSessionAttribute(config, "x-nv-video[0].peakBitrate", "4");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't support dynamic bitrate scaling properly (it tends to bounce between min and max and never
|
||||||
|
// settle on the optimal bitrate if it's somewhere in the middle), so we'll just latch the bitrate
|
||||||
|
// to the requested value.
|
||||||
|
addSessionAttribute(config, "x-nv-vqos[0].bw.minimumBitrate", ""+bitrate);
|
||||||
|
addSessionAttribute(config, "x-nv-vqos[0].bw.maximumBitrate", ""+bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
// Using FEC turns padding on which makes us have to take the slow path
|
// Using FEC turns padding on which makes us have to take the slow path
|
||||||
// in the depacketizer, not to mention exposing some ambiguous cases with
|
// in the depacketizer, not to mention exposing some ambiguous cases with
|
||||||
@ -170,9 +155,42 @@ public class SdpGenerator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ConnectionContext.SERVER_GENERATION_4:
|
case ConnectionContext.SERVER_GENERATION_4:
|
||||||
default:
|
|
||||||
addGen4Attributes(config, context);
|
addGen4Attributes(config, context);
|
||||||
break;
|
break;
|
||||||
|
case ConnectionContext.SERVER_GENERATION_5:
|
||||||
|
default:
|
||||||
|
addGen5Attributes(config, context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gen 4+ supports H.265 and surround sound
|
||||||
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_4) {
|
||||||
|
// If client and server are able, request HEVC
|
||||||
|
if (context.negotiatedVideoFormat == VideoFormat.H265) {
|
||||||
|
addSessionAttribute(config, "x-nv-clientSupportHevc", "1");
|
||||||
|
addSessionAttribute(config, "x-nv-vqos[0].bitStreamFormat", "1");
|
||||||
|
|
||||||
|
// Disable slicing on HEVC
|
||||||
|
addSessionAttribute(config, "x-nv-video[0].videoEncoderSlicesPerFrame", "1");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise, use AVC
|
||||||
|
addSessionAttribute(config, "x-nv-clientSupportHevc", "0");
|
||||||
|
addSessionAttribute(config, "x-nv-vqos[0].bitStreamFormat", "0");
|
||||||
|
|
||||||
|
// Use slicing for increased performance on some decoders
|
||||||
|
addSessionAttribute(config, "x-nv-video[0].videoEncoderSlicesPerFrame", "4");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable surround sound if configured for it
|
||||||
|
addSessionAttribute(config, "x-nv-audio.surround.numChannels", ""+context.streamConfig.getAudioChannelCount());
|
||||||
|
addSessionAttribute(config, "x-nv-audio.surround.channelMask", ""+context.streamConfig.getAudioChannelMask());
|
||||||
|
if (context.streamConfig.getAudioChannelCount() > 2) {
|
||||||
|
addSessionAttribute(config, "x-nv-audio.surround.enable", "1");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
addSessionAttribute(config, "x-nv-audio.surround.enable", "0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.append("t=0 0").append("\r\n");
|
config.append("t=0 0").append("\r\n");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user