Merge branch 'master' into logs

Conflicts:
	src/com/limelight/nvstream/av/audio/AudioDepacketizer.java
	src/com/limelight/nvstream/av/video/VideoDepacketizer.java
	src/com/limelight/nvstream/control/ControlStream.java
This commit is contained in:
Cameron Gutman
2014-02-26 16:22:04 -05:00
16 changed files with 608 additions and 248 deletions

View File

@@ -0,0 +1,17 @@
package com.limelight.nvstream.control;
public class ByteConfigTuple extends ConfigTuple {
public static final short PAYLOAD_LENGTH = 1;
public byte payload;
public ByteConfigTuple(short packetType, byte payload) {
super(packetType, PAYLOAD_LENGTH);
this.payload = payload;
}
@Override
public byte[] payloadToWire() {
return new byte[] {payload};
}
}

View File

@@ -2,141 +2,194 @@ package com.limelight.nvstream.control;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import com.limelight.nvstream.StreamConfiguration;
public class Config {
public static final int[] UNKNOWN_CONFIG =
public static final ConfigTuple[] CONFIG_720_60 =
{
70151,
68291329,
1280,
68291584,
1280,
68291840,
15360,
68292096,
25600,
68292352,
2048,
68292608,
1024,
68289024,
262144,
17957632,
302055424,
134217729,
16777490,
70153,
68293120,
768000,
17961216,
303235072,
335609857,
838861842,
352321536,
1006634002,
369098752,
335545362,
385875968,
1042,
402653184,
134218770,
419430400,
167773202,
436207616,
855638290,
266779,
7000,
266780,
2000,
266781,
50,
266782,
3000,
266783,
2,
266794,
5000,
266795,
500,
266784,
75,
266785,
25,
266786,
10,
266787,
60,
266788,
30,
266789,
3,
266790,
1000,
266791,
5000,
266792,
5000,
266793,
5000,
70190,
68301063,
10240,
68301312,
6400,
68301568,
768000,
68299776,
768,
68300032,
2560,
68300544,
0,
34746368,
(int)0xFE000000
new ByteConfigTuple((short)0x1207, (byte)1), //iFrameOnDemand
new IntConfigTuple((short)0x120b, 7), //averageBitrate
new IntConfigTuple((short)0x120c, 7), //peakBitrate
new IntConfigTuple((short)0x120d, 60), //gopLength
new IntConfigTuple((short)0x120e, 100), //vbvMultiplier
new IntConfigTuple((short)0x120f, 5), //rateControlMode
new IntConfigTuple((short)0x1210, 4), //slicesPerFrame
new IntConfigTuple((short)0x1202, 1024), //packetSize
new ByteConfigTuple((short)0x1203, (byte)0), //recordServerStats
new ByteConfigTuple((short)0x1201, (byte)0), //serverCapture
new ByteConfigTuple((short)0x1234, (byte)0), //serverNetworkCapture
new ByteConfigTuple((short)0x1248, (byte)0),
new ByteConfigTuple((short)0x1208, (byte)1), //refPicInvalidation
new ByteConfigTuple((short)0x1209, (byte)0), //enableFrameRateCtrl
new IntConfigTuple((short)0x1212, 3000), //pingBackIntervalMs
new IntConfigTuple((short)0x1238, 10000), //pingBackTimeoutMs
new ByteConfigTuple((short)0x1211, (byte)0), //enableSubframeEncoding
new ByteConfigTuple((short)0x1213, (byte)1), //videoQoSFecEnable
new IntConfigTuple((short)0x1214, 50), //videoQoSFecNumSrcPackets
new IntConfigTuple((short)0x1215, 60), //videoQoSFecNumOutPackets
new IntConfigTuple((short)0x1216, 20), //videoQoSFecRepairPercent
new IntConfigTuple((short)0x1217, 0), //videoQoSTsEnable
new IntConfigTuple((short)0x1218, 8), //videoQoSTsAverageBitrate
new IntConfigTuple((short)0x1219, 10), //videoQoSTsMaximumBitrate
new IntConfigTuple((short)0x121a, 311), //videoQoSBwFlags
new IntConfigTuple((short)0x121b, 10000), //videoQoSBwMaximumBitrate
new IntConfigTuple((short)0x121c, 2000), //videoQoSBwMinimumBitrate
new IntConfigTuple((short)0x121d, 50), //videoQoSBwStatsTime
new IntConfigTuple((short)0x121e, 3000), //videoQoSBwZeroLossCount
new IntConfigTuple((short)0x121f, 2), //videoQoSBwLossThreshold
new IntConfigTuple((short)0x122a, 5000), //videoQoSBwOwdThreshold
new IntConfigTuple((short)0x122b, 500), //videoQoSBwOwdReference
new IntConfigTuple((short)0x1220, 75), //videoQoSBwLossWaitTime
new IntConfigTuple((short)0x1221, 25), //videoQoSBwRateDropMultiplier
new IntConfigTuple((short)0x1222, 10), //videoQoSBwRateGainMultiplier
new IntConfigTuple((short)0x1223, 60), //videoQoSBwMaxFps
new IntConfigTuple((short)0x1224, 30), //videoQoSBwMinFps
new IntConfigTuple((short)0x1225, 3), //videoQoSBwFpsThreshold
new IntConfigTuple((short)0x1226, 1000), //videoQoSBwJitterThreshold
new IntConfigTuple((short)0x1227, 5000), //videoQoSBwJitterWaitTime
new IntConfigTuple((short)0x1228, 5000), //videoQoSBwNoJitterWaitTime
new IntConfigTuple((short)0x124e, 110),
new IntConfigTuple((short)0x1237, 10), //videoQoSBwEarlyDetectionEnableL1Threshold
new IntConfigTuple((short)0x1236, 6), //videoQoSBwEarlyDetectionEnableL0Threshold
new IntConfigTuple((short)0x1235, 4), //videoQoSBwEarlyDetectionDisableThreshold
new IntConfigTuple((short)0x1242, 20000), //videoQoSBwEarlyDetectionWaitTime
new IntConfigTuple((short)0x1244, 100),
new IntConfigTuple((short)0x1245, 1000),
new IntConfigTuple((short)0x1246, 720),
new IntConfigTuple((short)0x1247, 480),
new IntConfigTuple((short)0x1229, 5000), //videoQosVideoQualityScoreUpdateTime
new ByteConfigTuple((short)0x122e, (byte)7), //videoQosTrafficType
new IntConfigTuple((short)0x1231, 40), //videoQosBnNotifyUpBoundThreshold
new IntConfigTuple((short)0x1232, 25), //videoQosBnNotifyLowBoundThreshold
new IntConfigTuple((short)0x1233, 3000), //videoQosBnNotifyWaitTime
new IntConfigTuple((short)0x122c, 3), //videoQosInvalidateThreshold
new IntConfigTuple((short)0x122d, 10), //videoQosInvalidateSkipPercentage
/*new IntConfigTuple((short)0x123b, 12),
new IntConfigTuple((short)0x123c, 3),
new IntConfigTuple((short)0x1249, 0),
new IntConfigTuple((short)0x124a, 4000),
new IntConfigTuple((short)0x124b, 5000),
new IntConfigTuple((short)0x124c, 6000),
new IntConfigTuple((short)0x124d, 1000),*/
new IntConfigTuple((short)0x122f, 0), //riSecurityProtocol
new ShortConfigTuple((short)0x1230, (short)0), //riSecInfoUsePredefinedCert
new IntConfigTuple((short)0x1239, 0), //videoFrameDropIntervalNumber
new IntConfigTuple((short)0x123a, 0), //videoFrameDropContinualNumber
new IntConfigTuple((short)0x123d, 96000), //audioQosBitRate
new IntConfigTuple((short)0x123e, 5), //audioQosPacketDuration
new IntConfigTuple((short)0x123f, 1), //audioQosEnablePacketLossPercentage
new IntConfigTuple((short)0x1243, 100) //audioQosPacketLossPercentageUpdateInterval
};
public static final int CONFIG_SIZE = ((8 + UNKNOWN_CONFIG.length) * 4) + 3;
public static final ConfigTuple[] CONFIG_1080_30_DIFF =
{
new IntConfigTuple((short)0x120b, 10), //averageBitrate
new IntConfigTuple((short)0x120c, 10), //peakBitrate
new IntConfigTuple((short)0x121c, 4000), //videoQoSBwMinimumBitrate
new IntConfigTuple((short)0x1245, 3000),
new IntConfigTuple((short)0x1246, 1280),
new IntConfigTuple((short)0x1247, 720),
/*new IntConfigTuple((short)0x124a, 5000),
new IntConfigTuple((short)0x124c, 7000),*/
};
public static final ConfigTuple[] CONFIG_1080_60_DIFF =
{
new IntConfigTuple((short)0x120b, 30), //averageBitrate
new IntConfigTuple((short)0x120c, 30), //peakBitrate
new IntConfigTuple((short)0x120f, 4), //rateControlMode
new IntConfigTuple((short)0x121b, 30000), //videoQoSBwMaximumBitrate
new IntConfigTuple((short)0x121c, 25000), //videoQoSBwMinimumBitrate
new IntConfigTuple((short)0x1245, 3000),
new IntConfigTuple((short)0x1246, 1280),
new IntConfigTuple((short)0x1247, 720),
/*new IntConfigTuple((short)0x124a, 5000),
new IntConfigTuple((short)0x124c, 7000),*/
};
private StreamConfiguration streamConfig;
public Config(StreamConfiguration streamConfig) {
this.streamConfig = streamConfig;
}
private void updateSetWithConfig(ArrayList<ConfigTuple> set, ConfigTuple[] config)
{
for (ConfigTuple tuple : config)
{
int i;
for (i = 0; i < set.size(); i++) {
ConfigTuple existingTuple = set.get(i);
if (existingTuple.packetType == tuple.packetType) {
set.remove(i);
set.add(i, tuple);
break;
}
}
if (i == set.size()) {
set.add(tuple);
}
}
}
private int getConfigOnWireSize(ArrayList<ConfigTuple> tupleSet)
{
int size = 0;
for (ConfigTuple t : tupleSet)
{
size += ConfigTuple.HEADER_LENGTH + t.payloadLength;
}
return size;
}
private ArrayList<ConfigTuple> generateTupleSet() {
ArrayList<ConfigTuple> tupleSet = new ArrayList<ConfigTuple>();
tupleSet.add(new IntConfigTuple((short)0x1204, streamConfig.getWidth()));
tupleSet.add(new IntConfigTuple((short)0x1205, streamConfig.getHeight()));
tupleSet.add(new IntConfigTuple((short)0x1206, 1)); //videoTransferProtocol
tupleSet.add(new IntConfigTuple((short)0x120A, streamConfig.getRefreshRate()));
// Start with the initial config for 720p60
updateSetWithConfig(tupleSet, CONFIG_720_60);
if (streamConfig.getWidth() >= 1920 &&
streamConfig.getHeight() >= 1080)
{
if (streamConfig.getRefreshRate() >= 60)
{
// Update the initial set with the changed 1080p60 options
updateSetWithConfig(tupleSet, CONFIG_1080_60_DIFF);
}
else
{
// Update the initial set with the changed 1080p30 options
updateSetWithConfig(tupleSet, CONFIG_1080_30_DIFF);
}
}
return tupleSet;
}
public byte[] toWire() {
ByteBuffer bb = ByteBuffer.allocate(CONFIG_SIZE).order(ByteOrder.LITTLE_ENDIAN);
ArrayList<ConfigTuple> tupleSet = generateTupleSet();
ByteBuffer bb = ByteBuffer.allocate(getConfigOnWireSize(tupleSet) + 4).order(ByteOrder.LITTLE_ENDIAN);
// Width
bb.putShort((short) 0x1204);
bb.putShort((short) 0x0004);
bb.putInt(streamConfig.getWidth());
// Height
bb.putShort((short) 0x1205);
bb.putShort((short) 0x0004);
bb.putInt(streamConfig.getHeight());
// Unknown
bb.putShort((short) 0x1206);
bb.putShort((short) 0x0004);
bb.putInt(1);
// Refresh rate
bb.putShort((short) 0x120A);
bb.putShort((short) 0x0004);
bb.putInt(streamConfig.getRefreshRate());
// The rest are hardcoded
for (int i : UNKNOWN_CONFIG) {
bb.putInt(i);
for (ConfigTuple t : tupleSet)
{
bb.put(t.toWire());
}
// Config tail
bb.putShort((short) 0x0013);
bb.put((byte) 0x00);
bb.putShort((short) 0x13fe);
bb.putShort((short) 0x00);
return bb.array();
}

View File

@@ -0,0 +1,53 @@
package com.limelight.nvstream.control;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public abstract class ConfigTuple {
public short packetType;
public short payloadLength;
public static final short HEADER_LENGTH = 4;
public ConfigTuple(short packetType, short payloadLength)
{
this.packetType = packetType;
this.payloadLength = payloadLength;
}
public abstract byte[] payloadToWire();
public byte[] toWire()
{
byte[] payload = payloadToWire();
ByteBuffer bb = ByteBuffer.allocate(HEADER_LENGTH + (payload != null ? payload.length : 0))
.order(ByteOrder.LITTLE_ENDIAN);
bb.putShort(packetType);
bb.putShort(payloadLength);
if (payload != null) {
bb.put(payload);
}
return bb.array();
}
@Override
public int hashCode()
{
return packetType;
}
@Override
public boolean equals(Object o)
{
// We only compare the packet types on purpose
if (o instanceof ConfigTuple) {
return ((ConfigTuple)o).packetType == packetType;
}
else {
return false;
}
}
}

View File

@@ -8,6 +8,7 @@ import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnectionListener;
@@ -45,6 +46,15 @@ public class ControlStream implements ConnectionStatusListener {
private InetAddress host;
private Config config;
public static final int LOSS_PERIOD_MS = 15000;
public static final int MAX_LOSS_COUNT_IN_PERIOD = 3;
public static final int MAX_SLOW_SINK_COUNT = 3;
public static final int MESSAGE_DELAY_FACTOR = 5;
private long lossTimestamp;
private int lossCount;
private int slowSinkCount;
private Socket s;
private InputStream in;
private OutputStream out;
@@ -52,7 +62,7 @@ public class ControlStream implements ConnectionStatusListener {
private Thread heartbeatThread;
private Thread jitterThread;
private Thread resyncThread;
private Object resyncNeeded = new Object();
private LinkedBlockingQueue<int[]> invalidReferenceFrameTuples = new LinkedBlockingQueue<int[]>();
private boolean aborting = false;
public ControlStream(InetAddress host, NvConnectionListener listener, StreamConfiguration streamConfig)
@@ -132,12 +142,6 @@ public class ControlStream implements ConnectionStatusListener {
}
}
public void requestResync() throws IOException
{
LimeLog.info("CTL: Requesting IDR frame");
sendResync();
}
public void start() throws IOException
{
// Use a finite timeout during the handshake process
@@ -180,18 +184,39 @@ public class ControlStream implements ConnectionStatusListener {
public void run() {
while (!isInterrupted())
{
int[] tuple;
// Wait for a tuple
try {
// Wait for notification of a resync needed
synchronized (resyncNeeded) {
resyncNeeded.wait();
}
tuple = invalidReferenceFrameTuples.take();
} catch (InterruptedException e) {
listener.connectionTerminated(e);
return;
}
// Aggregate all lost frames into one range
int[] lastTuple = null;
for (;;) {
int[] nextTuple = lastTuple = invalidReferenceFrameTuples.poll();
if (nextTuple == null) {
break;
}
lastTuple = nextTuple;
}
// The server expects this to be the firstLostFrame + 1
tuple[0]++;
// Update the end of the range to the latest tuple
if (lastTuple != null) {
tuple[1] = lastTuple[1];
}
try {
requestResync();
LimeLog.warning("Invalidating reference frames from "+tuple[0]+" to "+tuple[1]);
ControlStream.this.sendResync(tuple[0], tuple[1]);
LimeLog.warning("Frames invalidated");
} catch (IOException e) {
listener.connectionTerminated(e);
return;
@@ -235,10 +260,12 @@ public class ControlStream implements ConnectionStatusListener {
return sendAndGetReply(new NvCtlPacket(PTYPE_1405, PPAYLEN_1405));
}
private void sendResync() throws IOException
private void sendResync(int firstLostFrame, int nextSuccessfulFrame) throws IOException
{
ByteBuffer conf = ByteBuffer.wrap(new byte[PPAYLEN_RESYNC]).order(ByteOrder.LITTLE_ENDIAN);
//conf.putLong(firstLostFrame);
//conf.putLong(nextSuccessfulFrame);
conf.putLong(0);
conf.putLong(0xFFFFF);
@@ -405,10 +432,32 @@ public class ControlStream implements ConnectionStatusListener {
abort();
}
public void connectionNeedsResync() {
synchronized (resyncNeeded) {
// Wake up the resync thread
resyncNeeded.notify();
private void resyncConnection(int firstLostFrame, int nextSuccessfulFrame) {
invalidReferenceFrameTuples.add(new int[]{firstLostFrame, nextSuccessfulFrame});
}
public void connectionDetectedFrameLoss(int firstLostFrame, int nextSuccessfulFrame) {
if (System.currentTimeMillis() > LOSS_PERIOD_MS + lossTimestamp) {
lossCount++;
lossTimestamp = System.currentTimeMillis();
}
else {
if (++lossCount == MAX_LOSS_COUNT_IN_PERIOD) {
listener.displayTransientMessage("Detected excessive A/V data loss. Try improving your network connection or lowering stream settings.");
lossCount = -MAX_LOSS_COUNT_IN_PERIOD * MESSAGE_DELAY_FACTOR;
lossTimestamp = 0;
}
}
resyncConnection(firstLostFrame, nextSuccessfulFrame);
}
public void connectionSinkTooSlow(int firstLostFrame, int nextSuccessfulFrame) {
if (++slowSinkCount == MAX_SLOW_SINK_COUNT) {
listener.displayTransientMessage("Your device is processing the A/V data too slowly. Try lowering stream settings.");
slowSinkCount = -MAX_SLOW_SINK_COUNT * MESSAGE_DELAY_FACTOR;
}
resyncConnection(firstLostFrame, nextSuccessfulFrame);
}
}

View File

@@ -0,0 +1,23 @@
package com.limelight.nvstream.control;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class IntConfigTuple extends ConfigTuple {
public static final short PAYLOAD_LENGTH = 4;
public int payload;
public IntConfigTuple(short packetType, int payload) {
super(packetType, PAYLOAD_LENGTH);
this.payload = payload;
}
@Override
public byte[] payloadToWire() {
ByteBuffer bb = ByteBuffer.allocate(PAYLOAD_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(payload);
return bb.array();
}
}

View File

@@ -0,0 +1,23 @@
package com.limelight.nvstream.control;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class ShortConfigTuple extends ConfigTuple {
public static final short PAYLOAD_LENGTH = 2;
public short payload;
public ShortConfigTuple(short packetType, short payload) {
super(packetType, PAYLOAD_LENGTH);
this.payload = payload;
}
@Override
public byte[] payloadToWire() {
ByteBuffer bb = ByteBuffer.allocate(PAYLOAD_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
bb.putShort(payload);
return bb.array();
}
}