mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-20 19:42:45 +00:00
Commit of common limelight core with bindings based on HEAD of RenderScript-Renderer
This commit is contained in:
parent
41d2f6b0e2
commit
ce1494895e
7
moonlight-common/.classpath
Normal file
7
moonlight-common/.classpath
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" path="src"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||||
|
<classpathentry kind="lib" path="libs/xpp3-1.1.4c.jar"/>
|
||||||
|
<classpathentry kind="output" path="bin"/>
|
||||||
|
</classpath>
|
17
moonlight-common/.project
Normal file
17
moonlight-common/.project
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>limelight-common</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
11
moonlight-common/.settings/org.eclipse.jdt.core.prefs
Normal file
11
moonlight-common/.settings/org.eclipse.jdt.core.prefs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
eclipse.preferences.version=1
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||||
|
org.eclipse.jdt.core.compiler.compliance=1.6
|
||||||
|
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||||
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.source=1.6
|
BIN
moonlight-common/libs/xpp3-1.1.4c.jar
Normal file
BIN
moonlight-common/libs/xpp3-1.1.4c.jar
Normal file
Binary file not shown.
505
moonlight-common/src/com/limelight/nvstream/ControlStream.java
Normal file
505
moonlight-common/src/com/limelight/nvstream/ControlStream.java
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.ConnectionStatusListener;
|
||||||
|
|
||||||
|
public class ControlStream implements ConnectionStatusListener {
|
||||||
|
|
||||||
|
public static final int PORT = 47995;
|
||||||
|
|
||||||
|
public static final int CONTROL_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
public static final short PTYPE_HELLO = 0x1204;
|
||||||
|
public static final short PPAYLEN_HELLO = 0x0004;
|
||||||
|
public static final byte[] PPAYLOAD_HELLO =
|
||||||
|
{
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x05,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final short PTYPE_KEEPALIVE = 0x13ff;
|
||||||
|
public static final short PPAYLEN_KEEPALIVE = 0x0000;
|
||||||
|
|
||||||
|
public static final short PTYPE_HEARTBEAT = 0x1401;
|
||||||
|
public static final short PPAYLEN_HEARTBEAT = 0x0000;
|
||||||
|
|
||||||
|
public static final short PTYPE_1405 = 0x1405;
|
||||||
|
public static final short PPAYLEN_1405 = 0x0000;
|
||||||
|
|
||||||
|
public static final short PTYPE_RESYNC = 0x1404;
|
||||||
|
public static final short PPAYLEN_RESYNC = 16;
|
||||||
|
|
||||||
|
public static final short PTYPE_CONFIG = 0x1205;
|
||||||
|
public static final short PPAYLEN_CONFIG = 0x0004;
|
||||||
|
public static final int[] PPAYLOAD_CONFIG =
|
||||||
|
{
|
||||||
|
720,
|
||||||
|
266758,
|
||||||
|
1,
|
||||||
|
266762,
|
||||||
|
30,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public static final short PTYPE_JITTER = 0x140c;
|
||||||
|
public static final short PPAYLEN_JITTER = 0x10;
|
||||||
|
|
||||||
|
private int seqNum;
|
||||||
|
|
||||||
|
private NvConnectionListener listener;
|
||||||
|
private InetAddress host;
|
||||||
|
|
||||||
|
private Socket s;
|
||||||
|
private InputStream in;
|
||||||
|
private OutputStream out;
|
||||||
|
|
||||||
|
private Thread heartbeatThread;
|
||||||
|
private Thread jitterThread;
|
||||||
|
private Thread resyncThread;
|
||||||
|
private Object resyncNeeded = new Object();
|
||||||
|
private boolean aborting = false;
|
||||||
|
|
||||||
|
public ControlStream(InetAddress host, NvConnectionListener listener)
|
||||||
|
{
|
||||||
|
this.listener = listener;
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize() throws IOException
|
||||||
|
{
|
||||||
|
s = new Socket();
|
||||||
|
s.setSoTimeout(CONTROL_TIMEOUT);
|
||||||
|
s.setTcpNoDelay(true);
|
||||||
|
s.connect(new InetSocketAddress(host, PORT), CONTROL_TIMEOUT);
|
||||||
|
in = s.getInputStream();
|
||||||
|
out = s.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPacket(NvCtlPacket packet) throws IOException
|
||||||
|
{
|
||||||
|
out.write(packet.toWire());
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControlStream.NvCtlResponse sendAndGetReply(NvCtlPacket packet) throws IOException
|
||||||
|
{
|
||||||
|
sendPacket(packet);
|
||||||
|
return new NvCtlResponse(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendJitter() throws IOException
|
||||||
|
{
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
bb.putInt(0);
|
||||||
|
bb.putInt(77);
|
||||||
|
bb.putInt(888);
|
||||||
|
bb.putInt(seqNum += 2);
|
||||||
|
|
||||||
|
sendPacket(new NvCtlPacket(PTYPE_JITTER, PPAYLEN_JITTER, bb.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abort()
|
||||||
|
{
|
||||||
|
if (aborting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aborting = true;
|
||||||
|
|
||||||
|
if (jitterThread != null) {
|
||||||
|
jitterThread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatThread != null) {
|
||||||
|
heartbeatThread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
s.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestResync() throws IOException
|
||||||
|
{
|
||||||
|
System.out.println("CTL: Requesting IDR frame");
|
||||||
|
sendResync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() throws IOException
|
||||||
|
{
|
||||||
|
sendHello();
|
||||||
|
sendConfig();
|
||||||
|
pingPong();
|
||||||
|
send1405AndGetResponse();
|
||||||
|
|
||||||
|
heartbeatThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
sendHeartbeat();
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(3000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
heartbeatThread.start();
|
||||||
|
|
||||||
|
resyncThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Wait for notification of a resync needed
|
||||||
|
synchronized (resyncNeeded) {
|
||||||
|
resyncNeeded.wait();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestResync();
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
resyncThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startJitterPackets()
|
||||||
|
{
|
||||||
|
jitterThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
sendJitter();
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jitterThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControlStream.NvCtlResponse send1405AndGetResponse() throws IOException
|
||||||
|
{
|
||||||
|
return sendAndGetReply(new NvCtlPacket(PTYPE_1405, PPAYLEN_1405));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHello() throws IOException
|
||||||
|
{
|
||||||
|
sendPacket(new NvCtlPacket(PTYPE_HELLO, PPAYLEN_HELLO, PPAYLOAD_HELLO));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendResync() throws IOException
|
||||||
|
{
|
||||||
|
ByteBuffer conf = ByteBuffer.wrap(new byte[PPAYLEN_RESYNC]).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
conf.putLong(0);
|
||||||
|
conf.putLong(0xFFFF);
|
||||||
|
|
||||||
|
sendAndGetReply(new NvCtlPacket(PTYPE_RESYNC, PPAYLEN_RESYNC, conf.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendConfig() throws IOException
|
||||||
|
{
|
||||||
|
ByteBuffer conf = ByteBuffer.wrap(new byte[PPAYLOAD_CONFIG.length * 4 + 3]).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
for (int i : PPAYLOAD_CONFIG)
|
||||||
|
conf.putInt(i);
|
||||||
|
|
||||||
|
conf.putShort((short)0x0013);
|
||||||
|
conf.put((byte) 0x00);
|
||||||
|
|
||||||
|
sendPacket(new NvCtlPacket(PTYPE_CONFIG, PPAYLEN_CONFIG, conf.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHeartbeat() throws IOException
|
||||||
|
{
|
||||||
|
sendPacket(new NvCtlPacket(PTYPE_HEARTBEAT, PPAYLEN_HEARTBEAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControlStream.NvCtlResponse pingPong() throws IOException
|
||||||
|
{
|
||||||
|
sendPacket(new NvCtlPacket(PTYPE_KEEPALIVE, PPAYLEN_KEEPALIVE));
|
||||||
|
return new ControlStream.NvCtlResponse(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NvCtlPacket {
|
||||||
|
public short type;
|
||||||
|
public short paylen;
|
||||||
|
public byte[] payload;
|
||||||
|
|
||||||
|
public NvCtlPacket(InputStream in) throws IOException
|
||||||
|
{
|
||||||
|
byte[] header = new byte[4];
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int bytesRead = in.read(header, offset, header.length - offset);
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += bytesRead;
|
||||||
|
} while (offset != header.length);
|
||||||
|
|
||||||
|
if (offset != header.length) {
|
||||||
|
throw new IOException("Socket closed prematurely");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
type = bb.getShort();
|
||||||
|
paylen = bb.getShort();
|
||||||
|
|
||||||
|
if (paylen != 0)
|
||||||
|
{
|
||||||
|
payload = new byte[paylen];
|
||||||
|
|
||||||
|
offset = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int bytesRead = in.read(payload, offset, payload.length - offset);
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += bytesRead;
|
||||||
|
} while (offset != payload.length);
|
||||||
|
|
||||||
|
if (offset != payload.length) {
|
||||||
|
throw new IOException("Socket closed prematurely");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvCtlPacket(byte[] payload)
|
||||||
|
{
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
type = bb.getShort();
|
||||||
|
paylen = bb.getShort();
|
||||||
|
|
||||||
|
if (bb.hasRemaining())
|
||||||
|
{
|
||||||
|
payload = new byte[bb.remaining()];
|
||||||
|
bb.get(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvCtlPacket(short type, short paylen)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.paylen = paylen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvCtlPacket(short type, short paylen, byte[] payload)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.paylen = paylen;
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getType()
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getPaylen()
|
||||||
|
{
|
||||||
|
return paylen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(short type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaylen(short paylen)
|
||||||
|
{
|
||||||
|
this.paylen = paylen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toWire()
|
||||||
|
{
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(4 + (payload != null ? payload.length : 0)).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
bb.putShort(type);
|
||||||
|
bb.putShort(paylen);
|
||||||
|
|
||||||
|
if (payload != null)
|
||||||
|
bb.put(payload);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NvCtlResponse extends NvCtlPacket {
|
||||||
|
public short status;
|
||||||
|
|
||||||
|
public NvCtlResponse(InputStream in) throws IOException {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvCtlResponse(short type, short paylen) {
|
||||||
|
super(type, paylen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvCtlResponse(short type, short paylen, byte[] payload) {
|
||||||
|
super(type, paylen, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvCtlResponse(byte[] payload) {
|
||||||
|
super(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusCode(short status)
|
||||||
|
{
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getStatusCode()
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionTerminated() {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionNeedsResync() {
|
||||||
|
synchronized (resyncNeeded) {
|
||||||
|
// Wake up the resync thread
|
||||||
|
resyncNeeded.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
133
moonlight-common/src/com/limelight/nvstream/Handshake.java
Normal file
133
moonlight-common/src/com/limelight/nvstream/Handshake.java
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
public class Handshake {
|
||||||
|
public static final int PORT = 47991;
|
||||||
|
|
||||||
|
public static final int HANDSHAKE_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
public static final byte[] PLATFORM_HELLO =
|
||||||
|
{
|
||||||
|
(byte)0x07,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00,
|
||||||
|
|
||||||
|
// android in ASCII
|
||||||
|
(byte)0x61,
|
||||||
|
(byte)0x6e,
|
||||||
|
(byte)0x64,
|
||||||
|
(byte)0x72,
|
||||||
|
(byte)0x6f,
|
||||||
|
(byte)0x69,
|
||||||
|
(byte)0x64,
|
||||||
|
|
||||||
|
(byte)0x03,
|
||||||
|
(byte)0x01,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final byte[] PACKET_2 =
|
||||||
|
{
|
||||||
|
(byte)0x01,
|
||||||
|
(byte)0x03,
|
||||||
|
(byte)0x02,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x08,
|
||||||
|
(byte)0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final byte[] PACKET_3 =
|
||||||
|
{
|
||||||
|
(byte)0x04,
|
||||||
|
(byte)0x01,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00,
|
||||||
|
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final byte[] PACKET_4 =
|
||||||
|
{
|
||||||
|
(byte)0x01,
|
||||||
|
(byte)0x01,
|
||||||
|
(byte)0x00,
|
||||||
|
(byte)0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
private static boolean waitAndDiscardResponse(InputStream in)
|
||||||
|
{
|
||||||
|
// Wait for response and discard response
|
||||||
|
try {
|
||||||
|
in.read();
|
||||||
|
|
||||||
|
// Wait for the full response to come in
|
||||||
|
Thread.sleep(250);
|
||||||
|
|
||||||
|
for (int i = 0; i < in.available(); i++)
|
||||||
|
in.read();
|
||||||
|
|
||||||
|
} catch (IOException e1) {
|
||||||
|
return false;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean performHandshake(InetAddress host) throws IOException
|
||||||
|
{
|
||||||
|
Socket s = new Socket();
|
||||||
|
s.connect(new InetSocketAddress(host, PORT), HANDSHAKE_TIMEOUT);
|
||||||
|
s.setSoTimeout(HANDSHAKE_TIMEOUT);
|
||||||
|
OutputStream out = s.getOutputStream();
|
||||||
|
InputStream in = s.getInputStream();
|
||||||
|
|
||||||
|
// First packet
|
||||||
|
out.write(PLATFORM_HELLO);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (!waitAndDiscardResponse(in)) {
|
||||||
|
s.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second packet
|
||||||
|
out.write(PACKET_2);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (!waitAndDiscardResponse(in)) {
|
||||||
|
s.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third packet
|
||||||
|
out.write(PACKET_3);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (!waitAndDiscardResponse(in)) {
|
||||||
|
s.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fourth packet
|
||||||
|
out.write(PACKET_4);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
// Done
|
||||||
|
s.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
321
moonlight-common/src/com/limelight/nvstream/NvConnection.java
Normal file
321
moonlight-common/src/com/limelight/nvstream/NvConnection.java
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.audio.AudioStream;
|
||||||
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoStream;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.input.NvController;
|
||||||
|
|
||||||
|
public class NvConnection {
|
||||||
|
private String host;
|
||||||
|
private NvConnectionListener listener;
|
||||||
|
|
||||||
|
private InetAddress hostAddr;
|
||||||
|
private ControlStream controlStream;
|
||||||
|
private NvController inputStream;
|
||||||
|
private VideoStream videoStream;
|
||||||
|
private AudioStream audioStream;
|
||||||
|
|
||||||
|
// Start parameters
|
||||||
|
private int drFlags;
|
||||||
|
private Object videoRenderTarget;
|
||||||
|
private VideoDecoderRenderer videoDecoderRenderer;
|
||||||
|
private AudioRenderer audioRenderer;
|
||||||
|
private String localDeviceName;
|
||||||
|
|
||||||
|
private ThreadPoolExecutor threadPool;
|
||||||
|
|
||||||
|
public NvConnection(String host, NvConnectionListener listener)
|
||||||
|
{
|
||||||
|
this.host = host;
|
||||||
|
this.listener = listener;
|
||||||
|
this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMacAddressString() throws SocketException {
|
||||||
|
Enumeration<NetworkInterface> ifaceList;
|
||||||
|
NetworkInterface selectedIface = null;
|
||||||
|
|
||||||
|
// First look for a WLAN interface (since those generally aren't removable)
|
||||||
|
ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (selectedIface == null && ifaceList.hasMoreElements()) {
|
||||||
|
NetworkInterface iface = ifaceList.nextElement();
|
||||||
|
|
||||||
|
if (iface.getName().startsWith("wlan") &&
|
||||||
|
iface.getHardwareAddress() != null) {
|
||||||
|
selectedIface = iface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find that, look for an Ethernet interface
|
||||||
|
ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (selectedIface == null && ifaceList.hasMoreElements()) {
|
||||||
|
NetworkInterface iface = ifaceList.nextElement();
|
||||||
|
|
||||||
|
if (iface.getName().startsWith("eth") &&
|
||||||
|
iface.getHardwareAddress() != null) {
|
||||||
|
selectedIface = iface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now just find something with a MAC address
|
||||||
|
ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (selectedIface == null && ifaceList.hasMoreElements()) {
|
||||||
|
NetworkInterface iface = ifaceList.nextElement();
|
||||||
|
|
||||||
|
if (iface.getHardwareAddress() != null) {
|
||||||
|
selectedIface = ifaceList.nextElement();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIface == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] macAddress = selectedIface.getHardwareAddress();
|
||||||
|
if (macAddress != null) {
|
||||||
|
StringBuilder addrStr = new StringBuilder();
|
||||||
|
for (int i = 0; i < macAddress.length; i++) {
|
||||||
|
addrStr.append(String.format("%02x", macAddress[i]));
|
||||||
|
if (i != macAddress.length - 1) {
|
||||||
|
addrStr.append(':');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addrStr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop()
|
||||||
|
{
|
||||||
|
threadPool.shutdownNow();
|
||||||
|
|
||||||
|
if (videoStream != null) {
|
||||||
|
videoStream.abort();
|
||||||
|
}
|
||||||
|
if (audioStream != null) {
|
||||||
|
audioStream.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlStream != null) {
|
||||||
|
controlStream.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputStream != null) {
|
||||||
|
inputStream.close();
|
||||||
|
inputStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startSteamBigPicture() throws XmlPullParserException, IOException
|
||||||
|
{
|
||||||
|
NvHTTP h = new NvHTTP(hostAddr, getMacAddressString(), localDeviceName);
|
||||||
|
|
||||||
|
if (!h.getPairState()) {
|
||||||
|
listener.displayMessage("Device not paired with computer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sessionId = h.getSessionId();
|
||||||
|
int appId = h.getSteamAppId(sessionId);
|
||||||
|
|
||||||
|
h.launchApp(sessionId, appId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startControlStream() throws IOException
|
||||||
|
{
|
||||||
|
controlStream = new ControlStream(hostAddr, listener);
|
||||||
|
controlStream.initialize();
|
||||||
|
controlStream.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startVideoStream() throws IOException
|
||||||
|
{
|
||||||
|
videoStream = new VideoStream(hostAddr, listener, controlStream);
|
||||||
|
videoStream.startVideoStream(videoDecoderRenderer, videoRenderTarget, drFlags);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startAudioStream() throws IOException
|
||||||
|
{
|
||||||
|
audioStream = new AudioStream(hostAddr, listener, audioRenderer);
|
||||||
|
audioStream.startAudioStream();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startInputConnection() throws IOException
|
||||||
|
{
|
||||||
|
inputStream = new NvController(hostAddr);
|
||||||
|
inputStream.initialize();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void establishConnection() {
|
||||||
|
for (NvConnectionListener.Stage currentStage : NvConnectionListener.Stage.values())
|
||||||
|
{
|
||||||
|
boolean success = false;
|
||||||
|
|
||||||
|
listener.stageStarting(currentStage);
|
||||||
|
try {
|
||||||
|
switch (currentStage)
|
||||||
|
{
|
||||||
|
case LAUNCH_APP:
|
||||||
|
success = startSteamBigPicture();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HANDSHAKE:
|
||||||
|
success = Handshake.performHandshake(hostAddr);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONTROL_START:
|
||||||
|
success = startControlStream();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIDEO_START:
|
||||||
|
success = startVideoStream();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AUDIO_START:
|
||||||
|
success = startAudioStream();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONTROL_START2:
|
||||||
|
controlStream.startJitterPackets();
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_START:
|
||||||
|
success = startInputConnection();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
listener.stageComplete(currentStage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listener.stageFailed(currentStage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.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;
|
||||||
|
this.videoDecoderRenderer = videoDecoderRenderer;
|
||||||
|
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
hostAddr = InetAddress.getByName(host);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
establishConnection();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||||
|
{
|
||||||
|
if (inputStream == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
threadPool.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
inputStream.sendMouseMove(deltaX, deltaY);
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonDown()
|
||||||
|
{
|
||||||
|
if (inputStream == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
threadPool.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
inputStream.sendMouseButtonDown();
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonUp()
|
||||||
|
{
|
||||||
|
if (inputStream == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
threadPool.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
inputStream.sendMouseButtonUp();
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInput(final short buttonFlags,
|
||||||
|
final byte leftTrigger, final byte rightTrigger,
|
||||||
|
final short leftStickX, final short leftStickY,
|
||||||
|
final short rightStickX, final short rightStickY)
|
||||||
|
{
|
||||||
|
if (inputStream == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
threadPool.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
inputStream.sendControllerInput(buttonFlags, leftTrigger,
|
||||||
|
rightTrigger, leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY);
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
public interface NvConnectionListener {
|
||||||
|
|
||||||
|
public enum Stage {
|
||||||
|
LAUNCH_APP("app"),
|
||||||
|
HANDSHAKE("handshake"),
|
||||||
|
CONTROL_START("control connection"),
|
||||||
|
VIDEO_START("video stream"),
|
||||||
|
AUDIO_START("audio stream"),
|
||||||
|
CONTROL_START2("control connection"),
|
||||||
|
INPUT_START("input connection");
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private Stage(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public void stageStarting(Stage stage);
|
||||||
|
public void stageComplete(Stage stage);
|
||||||
|
public void stageFailed(Stage stage);
|
||||||
|
|
||||||
|
public void connectionStarted();
|
||||||
|
public void connectionTerminated(Exception e);
|
||||||
|
|
||||||
|
public void displayMessage(String message);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
|
public class ByteBufferDescriptor {
|
||||||
|
public byte[] data;
|
||||||
|
public int offset;
|
||||||
|
public int length;
|
||||||
|
|
||||||
|
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||||
|
{
|
||||||
|
this.data = desc.data;
|
||||||
|
this.offset = desc.offset;
|
||||||
|
this.length = desc.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reinitialize(byte[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print()
|
||||||
|
{
|
||||||
|
print(offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print(int length)
|
||||||
|
{
|
||||||
|
print(this.offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print(int offset, int length)
|
||||||
|
{
|
||||||
|
for (int i = offset; i < offset+length; i++) {
|
||||||
|
System.out.printf("%d: %02x \n", i, data[i]);
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
|
public interface ConnectionStatusListener {
|
||||||
|
public void connectionTerminated();
|
||||||
|
|
||||||
|
public void connectionNeedsResync();
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DecodeUnit {
|
||||||
|
public static final int TYPE_UNKNOWN = 0;
|
||||||
|
public static final int TYPE_H264 = 1;
|
||||||
|
public static final int TYPE_OPUS = 2;
|
||||||
|
|
||||||
|
private int type;
|
||||||
|
private List<ByteBufferDescriptor> bufferList;
|
||||||
|
private int dataLength;
|
||||||
|
private int flags;
|
||||||
|
|
||||||
|
public DecodeUnit(int type, List<ByteBufferDescriptor> bufferList, int dataLength, int flags)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.bufferList = bufferList;
|
||||||
|
this.dataLength = dataLength;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType()
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFlags()
|
||||||
|
{
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ByteBufferDescriptor> getBufferList()
|
||||||
|
{
|
||||||
|
return bufferList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDataLength()
|
||||||
|
{
|
||||||
|
return dataLength;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class RtpPacket {
|
||||||
|
|
||||||
|
private byte packetType;
|
||||||
|
private short seqNum;
|
||||||
|
private ByteBufferDescriptor buffer;
|
||||||
|
|
||||||
|
public RtpPacket(ByteBufferDescriptor buffer)
|
||||||
|
{
|
||||||
|
this.buffer = new ByteBufferDescriptor(buffer);
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(buffer.data, buffer.offset, buffer.length);
|
||||||
|
|
||||||
|
// Discard the first byte
|
||||||
|
bb.position(bb.position()+1);
|
||||||
|
|
||||||
|
// Get the packet type
|
||||||
|
packetType = bb.get();
|
||||||
|
|
||||||
|
// Get the sequence number
|
||||||
|
seqNum = bb.getShort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getPacketType()
|
||||||
|
{
|
||||||
|
return packetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getSequenceNumber()
|
||||||
|
{
|
||||||
|
return seqNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBackingBuffer()
|
||||||
|
{
|
||||||
|
return buffer.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBufferDescriptor getNewPayloadDescriptor()
|
||||||
|
{
|
||||||
|
return new ByteBufferDescriptor(buffer.data, buffer.offset+12, buffer.length-12);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
|
public class ShortBufferDescriptor {
|
||||||
|
public short[] data;
|
||||||
|
public int offset;
|
||||||
|
public int length;
|
||||||
|
|
||||||
|
public ShortBufferDescriptor(short[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShortBufferDescriptor(ShortBufferDescriptor desc)
|
||||||
|
{
|
||||||
|
this.data = desc.data;
|
||||||
|
this.offset = desc.offset;
|
||||||
|
this.length = desc.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reinitialize(short[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
|
import com.limelight.nvstream.av.RtpPacket;
|
||||||
|
import com.limelight.nvstream.av.ShortBufferDescriptor;
|
||||||
|
|
||||||
|
public class AudioDepacketizer {
|
||||||
|
|
||||||
|
private static final int DU_LIMIT = 15;
|
||||||
|
private LinkedBlockingQueue<ShortBufferDescriptor> decodedUnits =
|
||||||
|
new LinkedBlockingQueue<ShortBufferDescriptor>(DU_LIMIT);
|
||||||
|
|
||||||
|
// Sequencing state
|
||||||
|
private short lastSequenceNumber;
|
||||||
|
|
||||||
|
private void decodeData(byte[] data, int off, int len)
|
||||||
|
{
|
||||||
|
// Submit this data to the decoder
|
||||||
|
short[] pcmData = new short[OpusDecoder.getMaxOutputShorts()];
|
||||||
|
int decodeLen = OpusDecoder.decode(data, off, len, pcmData);
|
||||||
|
|
||||||
|
if (decodeLen > 0) {
|
||||||
|
// Return value of decode is frames decoded per channel
|
||||||
|
decodeLen *= OpusDecoder.getChannelCount();
|
||||||
|
|
||||||
|
// Put it on the decoded queue
|
||||||
|
if (!decodedUnits.offer(new ShortBufferDescriptor(pcmData, 0, decodeLen))) {
|
||||||
|
// Clear out the queue
|
||||||
|
decodedUnits.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decodeInputData(RtpPacket packet)
|
||||||
|
{
|
||||||
|
short seq = packet.getSequenceNumber();
|
||||||
|
|
||||||
|
if (packet.getPacketType() != 97) {
|
||||||
|
// Only type 97 is audio
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toss out the current NAL if we receive a packet that is
|
||||||
|
// out of sequence
|
||||||
|
if (lastSequenceNumber != 0 &&
|
||||||
|
(short)(lastSequenceNumber + 1) != seq)
|
||||||
|
{
|
||||||
|
System.out.println("Received OOS audio data (expected "+(lastSequenceNumber + 1)+", got "+seq+")");
|
||||||
|
decodeData(null, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSequenceNumber = seq;
|
||||||
|
|
||||||
|
// This is all the depacketizing we need to do
|
||||||
|
ByteBufferDescriptor rtpPayload = packet.getNewPayloadDescriptor();
|
||||||
|
decodeData(rtpPayload.data, rtpPayload.offset, rtpPayload.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShortBufferDescriptor getNextDecodedData() throws InterruptedException
|
||||||
|
{
|
||||||
|
return decodedUnits.take();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
public interface AudioRenderer {
|
||||||
|
public void streamInitialized(int channelCount, int sampleRate);
|
||||||
|
|
||||||
|
public void playDecodedAudio(short[] audioData, int offset, int length);
|
||||||
|
|
||||||
|
public void streamClosing();
|
||||||
|
}
|
@ -0,0 +1,223 @@
|
|||||||
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
|
import com.limelight.nvstream.av.RtpPacket;
|
||||||
|
import com.limelight.nvstream.av.ShortBufferDescriptor;
|
||||||
|
|
||||||
|
public class AudioStream {
|
||||||
|
public static final int RTP_PORT = 48000;
|
||||||
|
public static final int RTCP_PORT = 47999;
|
||||||
|
|
||||||
|
private LinkedBlockingQueue<RtpPacket> packets = new LinkedBlockingQueue<RtpPacket>(100);
|
||||||
|
|
||||||
|
private DatagramSocket rtp;
|
||||||
|
|
||||||
|
private AudioDepacketizer depacketizer = new AudioDepacketizer();
|
||||||
|
|
||||||
|
private LinkedList<Thread> threads = new LinkedList<Thread>();
|
||||||
|
|
||||||
|
private boolean aborting = false;
|
||||||
|
|
||||||
|
private InetAddress host;
|
||||||
|
private NvConnectionListener connListener;
|
||||||
|
private AudioRenderer streamListener;
|
||||||
|
|
||||||
|
public AudioStream(InetAddress host, NvConnectionListener connListener, AudioRenderer streamListener)
|
||||||
|
{
|
||||||
|
this.host = host;
|
||||||
|
this.connListener = connListener;
|
||||||
|
this.streamListener = streamListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abort()
|
||||||
|
{
|
||||||
|
if (aborting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aborting = true;
|
||||||
|
|
||||||
|
for (Thread t : threads) {
|
||||||
|
t.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the socket to interrupt the receive thread
|
||||||
|
if (rtp != null) {
|
||||||
|
rtp.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for threads to terminate
|
||||||
|
for (Thread t : threads) {
|
||||||
|
try {
|
||||||
|
t.join();
|
||||||
|
} catch (InterruptedException e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
streamListener.streamClosing();
|
||||||
|
|
||||||
|
threads.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAudioStream() throws SocketException
|
||||||
|
{
|
||||||
|
setupRtpSession();
|
||||||
|
|
||||||
|
setupAudio();
|
||||||
|
|
||||||
|
startReceiveThread();
|
||||||
|
|
||||||
|
startDepacketizerThread();
|
||||||
|
|
||||||
|
startDecoderThread();
|
||||||
|
|
||||||
|
startUdpPingThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRtpSession() throws SocketException
|
||||||
|
{
|
||||||
|
rtp = new DatagramSocket(RTP_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupAudio()
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = OpusDecoder.init();
|
||||||
|
if (err != 0) {
|
||||||
|
throw new IllegalStateException("Opus decoder failed to initialize");
|
||||||
|
}
|
||||||
|
|
||||||
|
streamListener.streamInitialized(OpusDecoder.getChannelCount(), OpusDecoder.getSampleRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDepacketizerThread()
|
||||||
|
{
|
||||||
|
// This thread lessens the work on the receive thread
|
||||||
|
// so it can spend more time waiting for data
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
RtpPacket packet;
|
||||||
|
|
||||||
|
try {
|
||||||
|
packet = packets.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
connListener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
depacketizer.decodeInputData(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Audio - Depacketizer");
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDecoderThread()
|
||||||
|
{
|
||||||
|
// Decoder thread
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
ShortBufferDescriptor samples;
|
||||||
|
|
||||||
|
try {
|
||||||
|
samples = depacketizer.getNextDecodedData();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
connListener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamListener.playDecodedAudio(samples.data, samples.offset, samples.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Audio - Player");
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startReceiveThread()
|
||||||
|
{
|
||||||
|
// Receive thread
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ByteBufferDescriptor desc = new ByteBufferDescriptor(new byte[1500], 0, 1500);
|
||||||
|
DatagramPacket packet = new DatagramPacket(desc.data, desc.length);
|
||||||
|
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
rtp.receive(packet);
|
||||||
|
} catch (IOException e) {
|
||||||
|
connListener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the packet to the depacketizer thread
|
||||||
|
desc.length = packet.getLength();
|
||||||
|
if (packets.offer(new RtpPacket(desc))) {
|
||||||
|
desc.reinitialize(new byte[1500], 0, 1500);
|
||||||
|
packet.setData(desc.data, desc.offset, desc.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Audio - Receive");
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startUdpPingThread()
|
||||||
|
{
|
||||||
|
// Ping thread
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// Send PING every 100 ms
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
rtp.send(pingPacket);
|
||||||
|
} catch (IOException e) {
|
||||||
|
connListener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
connListener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
t.setName("Audio - Ping");
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
public class OpusDecoder {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("nv_opus_dec");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native int init();
|
||||||
|
public static native void destroy();
|
||||||
|
public static native int getChannelCount();
|
||||||
|
public static native int getMaxOutputShorts();
|
||||||
|
public static native int getSampleRate();
|
||||||
|
public static native int decode(byte[] indata, int inoff, int inlen, short[] outpcmdata);
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.limelight.nvstream.av.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
|
|
||||||
|
public interface VideoDecoderRenderer {
|
||||||
|
public static int FLAG_PREFER_QUALITY = 0x1;
|
||||||
|
|
||||||
|
public void setup(int width, int height, Object renderTarget, int drFlags);
|
||||||
|
|
||||||
|
public void start();
|
||||||
|
|
||||||
|
public void stop();
|
||||||
|
|
||||||
|
public void release();
|
||||||
|
|
||||||
|
public boolean submitDecodeUnit(DecodeUnit decodeUnit);
|
||||||
|
}
|
@ -0,0 +1,213 @@
|
|||||||
|
package com.limelight.nvstream.av.video;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
|
import com.limelight.nvstream.av.RtpPacket;
|
||||||
|
import com.limelight.nvstream.av.ConnectionStatusListener;
|
||||||
|
|
||||||
|
public class VideoDepacketizer {
|
||||||
|
|
||||||
|
// Current NAL state
|
||||||
|
private LinkedList<ByteBufferDescriptor> avcNalDataChain = null;
|
||||||
|
private int avcNalDataLength = 0;
|
||||||
|
|
||||||
|
// Sequencing state
|
||||||
|
private short lastSequenceNumber;
|
||||||
|
|
||||||
|
private ConnectionStatusListener controlListener;
|
||||||
|
|
||||||
|
private static final int DU_LIMIT = 15;
|
||||||
|
private LinkedBlockingQueue<DecodeUnit> decodedUnits = new LinkedBlockingQueue<DecodeUnit>(DU_LIMIT);
|
||||||
|
|
||||||
|
public VideoDepacketizer(ConnectionStatusListener controlListener)
|
||||||
|
{
|
||||||
|
this.controlListener = controlListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAvcNalState()
|
||||||
|
{
|
||||||
|
avcNalDataChain = null;
|
||||||
|
avcNalDataLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reassembleAvcNal()
|
||||||
|
{
|
||||||
|
// This is the start of a new NAL
|
||||||
|
if (avcNalDataChain != null && avcNalDataLength != 0) {
|
||||||
|
// Construct the H264 decode unit
|
||||||
|
DecodeUnit du = new DecodeUnit(DecodeUnit.TYPE_H264, avcNalDataChain, avcNalDataLength, 0);
|
||||||
|
if (!decodedUnits.offer(du)) {
|
||||||
|
// We need a new IDR frame since we're discarding data now
|
||||||
|
decodedUnits.clear();
|
||||||
|
controlListener.connectionNeedsResync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear old state
|
||||||
|
clearAvcNalState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInputData(VideoPacket packet)
|
||||||
|
{
|
||||||
|
ByteBufferDescriptor location = packet.getNewPayloadDescriptor();
|
||||||
|
|
||||||
|
// SPS and PPS packet doesn't have standard headers, so submit it as is
|
||||||
|
if (location.length < 968) {
|
||||||
|
avcNalDataChain = new LinkedList<ByteBufferDescriptor>();
|
||||||
|
avcNalDataLength = 0;
|
||||||
|
|
||||||
|
avcNalDataChain.add(location);
|
||||||
|
avcNalDataLength += location.length;
|
||||||
|
|
||||||
|
reassembleAvcNal();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int packetIndex = packet.getPacketIndex();
|
||||||
|
int packetsInFrame = packet.getTotalPackets();
|
||||||
|
|
||||||
|
// Check if this is the first packet for a frame
|
||||||
|
if (packetIndex == 0) {
|
||||||
|
// Setup state for the new frame
|
||||||
|
avcNalDataChain = new LinkedList<ByteBufferDescriptor>();
|
||||||
|
avcNalDataLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this packet falls in the range of packets in frame
|
||||||
|
if (packetIndex >= packetsInFrame) {
|
||||||
|
// This isn't H264 frame data
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the length to only contain valid data
|
||||||
|
location.length = packet.getPayloadLength();
|
||||||
|
|
||||||
|
// Add the payload data to the chain
|
||||||
|
if (avcNalDataChain != null) {
|
||||||
|
avcNalDataChain.add(location);
|
||||||
|
avcNalDataLength += location.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reassemble the NALs if this was the last packet for this frame
|
||||||
|
if (packetIndex + 1 == packetsInFrame) {
|
||||||
|
reassembleAvcNal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInputData(RtpPacket packet)
|
||||||
|
{
|
||||||
|
short seq = packet.getSequenceNumber();
|
||||||
|
|
||||||
|
// Toss out the current NAL if we receive a packet that is
|
||||||
|
// out of sequence
|
||||||
|
if (lastSequenceNumber != 0 &&
|
||||||
|
(short)(lastSequenceNumber + 1) != seq)
|
||||||
|
{
|
||||||
|
System.out.println("Received OOS video data (expected "+(lastSequenceNumber + 1)+", got "+seq+")");
|
||||||
|
|
||||||
|
// Reset the depacketizer state
|
||||||
|
clearAvcNalState();
|
||||||
|
|
||||||
|
// Request an IDR frame
|
||||||
|
controlListener.connectionNeedsResync();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSequenceNumber = seq;
|
||||||
|
|
||||||
|
// Pass the payload to the non-sequencing parser
|
||||||
|
ByteBufferDescriptor rtpPayload = packet.getNewPayloadDescriptor();
|
||||||
|
addInputData(new VideoPacket(rtpPayload));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecodeUnit getNextDecodeUnit() throws InterruptedException
|
||||||
|
{
|
||||||
|
return decodedUnits.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NAL {
|
||||||
|
|
||||||
|
// This assumes that the buffer passed in is already a special sequence
|
||||||
|
public static boolean isAvcStartSequence(ByteBufferDescriptor specialSeq)
|
||||||
|
{
|
||||||
|
// The start sequence is 00 00 01 or 00 00 00 01
|
||||||
|
return (specialSeq.data[specialSeq.offset+specialSeq.length-1] == 0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This assumes that the buffer passed in is already a special sequence
|
||||||
|
public static boolean isAvcFrameStart(ByteBufferDescriptor specialSeq)
|
||||||
|
{
|
||||||
|
if (specialSeq.length != 4)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// The frame start sequence is 00 00 00 01
|
||||||
|
return (specialSeq.data[specialSeq.offset+specialSeq.length-1] == 0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a buffer descriptor describing the start sequence
|
||||||
|
public static boolean getSpecialSequenceDescriptor(ByteBufferDescriptor buffer, ByteBufferDescriptor outputDesc)
|
||||||
|
{
|
||||||
|
// NAL start sequence is 00 00 00 01 or 00 00 01
|
||||||
|
if (buffer.length < 3)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 00 00 is magic
|
||||||
|
if (buffer.data[buffer.offset] == 0x00 &&
|
||||||
|
buffer.data[buffer.offset+1] == 0x00)
|
||||||
|
{
|
||||||
|
// Another 00 could be the end of the special sequence
|
||||||
|
// 00 00 00 or the middle of 00 00 00 01
|
||||||
|
if (buffer.data[buffer.offset+2] == 0x00)
|
||||||
|
{
|
||||||
|
if (buffer.length >= 4 &&
|
||||||
|
buffer.data[buffer.offset+3] == 0x01)
|
||||||
|
{
|
||||||
|
// It's the AVC start sequence 00 00 00 01
|
||||||
|
outputDesc.reinitialize(buffer.data, buffer.offset, 4);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// It's 00 00 00
|
||||||
|
outputDesc.reinitialize(buffer.data, buffer.offset, 3);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (buffer.data[buffer.offset+2] == 0x01 ||
|
||||||
|
buffer.data[buffer.offset+2] == 0x02)
|
||||||
|
{
|
||||||
|
// These are easy: 00 00 01 or 00 00 02
|
||||||
|
outputDesc.reinitialize(buffer.data, buffer.offset, 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (buffer.data[buffer.offset+2] == 0x03)
|
||||||
|
{
|
||||||
|
// 00 00 03 is special because it's a subsequence of the
|
||||||
|
// NAL wrapping substitute for 00 00 00, 00 00 01, 00 00 02,
|
||||||
|
// or 00 00 03 in the RBSP sequence. We need to check the next
|
||||||
|
// byte to see whether it's 00, 01, 02, or 03 (a valid RBSP substitution)
|
||||||
|
// or whether it's something else
|
||||||
|
|
||||||
|
if (buffer.length < 4)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (buffer.data[buffer.offset+3] >= 0x00 &&
|
||||||
|
buffer.data[buffer.offset+3] <= 0x03)
|
||||||
|
{
|
||||||
|
// It's not really a special sequence after all
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// It's not a standard replacement so it's a special sequence
|
||||||
|
outputDesc.reinitialize(buffer.data, buffer.offset, 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.limelight.nvstream.av.video;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
|
|
||||||
|
public class VideoPacket {
|
||||||
|
private ByteBufferDescriptor buffer;
|
||||||
|
|
||||||
|
private int frameIndex;
|
||||||
|
private int packetIndex;
|
||||||
|
private int totalPackets;
|
||||||
|
private int payloadLength;
|
||||||
|
|
||||||
|
public VideoPacket(ByteBufferDescriptor rtpPayload)
|
||||||
|
{
|
||||||
|
buffer = new ByteBufferDescriptor(rtpPayload);
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(buffer.data).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
bb.position(buffer.offset);
|
||||||
|
|
||||||
|
frameIndex = bb.getInt();
|
||||||
|
packetIndex = bb.getInt();
|
||||||
|
totalPackets = bb.getInt();
|
||||||
|
|
||||||
|
bb.position(bb.position()+4);
|
||||||
|
|
||||||
|
payloadLength = bb.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFrameIndex()
|
||||||
|
{
|
||||||
|
return frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPacketIndex()
|
||||||
|
{
|
||||||
|
return packetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPayloadLength()
|
||||||
|
{
|
||||||
|
return payloadLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalPackets()
|
||||||
|
{
|
||||||
|
return totalPackets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBufferDescriptor getNewPayloadDescriptor()
|
||||||
|
{
|
||||||
|
return new ByteBufferDescriptor(buffer.data, buffer.offset+56, buffer.length-56);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,290 @@
|
|||||||
|
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;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
|
import com.limelight.nvstream.av.RtpPacket;
|
||||||
|
import com.limelight.nvstream.av.ConnectionStatusListener;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private LinkedBlockingQueue<RtpPacket> packets = new LinkedBlockingQueue<RtpPacket>(100);
|
||||||
|
|
||||||
|
private InetAddress host;
|
||||||
|
private DatagramSocket rtp;
|
||||||
|
private Socket firstFrameSocket;
|
||||||
|
|
||||||
|
private LinkedList<Thread> threads = new LinkedList<Thread>();
|
||||||
|
|
||||||
|
private NvConnectionListener listener;
|
||||||
|
private VideoDepacketizer depacketizer;
|
||||||
|
|
||||||
|
private VideoDecoderRenderer decRend;
|
||||||
|
private boolean startedRendering;
|
||||||
|
|
||||||
|
private boolean aborting = false;
|
||||||
|
|
||||||
|
public VideoStream(InetAddress host, NvConnectionListener listener, ConnectionStatusListener avConnListener)
|
||||||
|
{
|
||||||
|
this.host = host;
|
||||||
|
this.listener = listener;
|
||||||
|
this.depacketizer = new VideoDepacketizer(avConnListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abort()
|
||||||
|
{
|
||||||
|
if (aborting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aborting = true;
|
||||||
|
|
||||||
|
// Interrupt threads
|
||||||
|
for (Thread t : threads) {
|
||||||
|
t.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the socket to interrupt the receive thread
|
||||||
|
if (rtp != null) {
|
||||||
|
rtp.close();
|
||||||
|
}
|
||||||
|
if (firstFrameSocket != null) {
|
||||||
|
try {
|
||||||
|
firstFrameSocket.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for threads to terminate
|
||||||
|
for (Thread t : threads) {
|
||||||
|
try {
|
||||||
|
t.join();
|
||||||
|
} catch (InterruptedException e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startedRendering) {
|
||||||
|
decRend.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decRend != null) {
|
||||||
|
decRend.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
threads.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readFirstFrame() throws IOException
|
||||||
|
{
|
||||||
|
byte[] firstFrame = new byte[1500];
|
||||||
|
|
||||||
|
firstFrameSocket = new Socket();
|
||||||
|
firstFrameSocket.setSoTimeout(FIRST_FRAME_TIMEOUT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
firstFrameSocket.connect(new InetSocketAddress(host, FIRST_FRAME_PORT), FIRST_FRAME_TIMEOUT);
|
||||||
|
InputStream firstFrameStream = firstFrameSocket.getInputStream();
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
int bytesRead = firstFrameStream.read(firstFrame, offset, firstFrame.length-offset);
|
||||||
|
|
||||||
|
if (bytesRead == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
offset += bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
depacketizer.addInputData(new VideoPacket(new ByteBufferDescriptor(firstFrame, 0, offset)));
|
||||||
|
} finally {
|
||||||
|
firstFrameSocket.close();
|
||||||
|
firstFrameSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setupRtpSession() throws SocketException
|
||||||
|
{
|
||||||
|
rtp = new DatagramSocket(RTP_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setupDecoderRenderer(VideoDecoderRenderer decRend, Object renderTarget, int drFlags) {
|
||||||
|
this.decRend = decRend;
|
||||||
|
if (decRend != null) {
|
||||||
|
decRend.setup(1280, 720, renderTarget, drFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startVideoStream(VideoDecoderRenderer decRend, Object renderTarget, int drFlags) throws IOException
|
||||||
|
{
|
||||||
|
// Setup the decoder and renderer
|
||||||
|
setupDecoderRenderer(decRend, renderTarget, drFlags);
|
||||||
|
|
||||||
|
// Open RTP sockets and start session
|
||||||
|
setupRtpSession();
|
||||||
|
|
||||||
|
// Start pinging before reading the first frame
|
||||||
|
// so Shield Proxy knows we're here and sends us
|
||||||
|
// the reference frame
|
||||||
|
startUdpPingThread();
|
||||||
|
|
||||||
|
// Read the first frame to start the UDP video stream
|
||||||
|
// This MUST be called before the normal UDP receive thread
|
||||||
|
// starts in order to avoid state corruption caused by two
|
||||||
|
// threads simultaneously adding input data.
|
||||||
|
readFirstFrame();
|
||||||
|
|
||||||
|
if (decRend != null) {
|
||||||
|
// Start the receive thread early to avoid missing
|
||||||
|
// early packets
|
||||||
|
startReceiveThread();
|
||||||
|
|
||||||
|
// Start the depacketizer thread to deal with the RTP data
|
||||||
|
startDepacketizerThread();
|
||||||
|
|
||||||
|
// Start decoding the data we're receiving
|
||||||
|
startDecoderThread();
|
||||||
|
|
||||||
|
// Start the renderer
|
||||||
|
decRend.start();
|
||||||
|
startedRendering = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDecoderThread()
|
||||||
|
{
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Read the decode units generated from the RTP stream
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
DecodeUnit du;
|
||||||
|
|
||||||
|
try {
|
||||||
|
du = depacketizer.getNextDecodeUnit();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
decRend.submitDecodeUnit(du);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Video - Decoder");
|
||||||
|
t.setPriority(Thread.MAX_PRIORITY);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDepacketizerThread()
|
||||||
|
{
|
||||||
|
// This thread lessens the work on the receive thread
|
||||||
|
// so it can spend more time waiting for data
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
RtpPacket packet;
|
||||||
|
|
||||||
|
try {
|
||||||
|
packet = packets.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// !!! We no longer own the data buffer at this point !!!
|
||||||
|
depacketizer.addInputData(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Video - Depacketizer");
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startReceiveThread()
|
||||||
|
{
|
||||||
|
// Receive thread
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ByteBufferDescriptor desc = new ByteBufferDescriptor(new byte[1500], 0, 1500);
|
||||||
|
DatagramPacket packet = new DatagramPacket(desc.data, desc.length);
|
||||||
|
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
rtp.receive(packet);
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the packet to the depacketizer thread
|
||||||
|
desc.length = packet.getLength();
|
||||||
|
if (packets.offer(new RtpPacket(desc))) {
|
||||||
|
desc.reinitialize(new byte[1500], 0, 1500);
|
||||||
|
packet.setData(desc.data, desc.offset, desc.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Video - Receive");
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startUdpPingThread()
|
||||||
|
{
|
||||||
|
// Ping thread
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// Send PING every 100 ms
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
rtp.send(pingPacket);
|
||||||
|
} catch (IOException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
listener.connectionTerminated(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
threads.add(t);
|
||||||
|
t.setName("Video - Ping");
|
||||||
|
t.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package com.limelight.nvstream.av.video.cpu;
|
||||||
|
|
||||||
|
public class AvcDecoder {
|
||||||
|
static {
|
||||||
|
// FFMPEG dependencies
|
||||||
|
System.loadLibrary("avutil-52");
|
||||||
|
System.loadLibrary("swresample-0");
|
||||||
|
System.loadLibrary("swscale-2");
|
||||||
|
System.loadLibrary("avcodec-55");
|
||||||
|
System.loadLibrary("avformat-55");
|
||||||
|
System.loadLibrary("avfilter-3");
|
||||||
|
|
||||||
|
System.loadLibrary("nv_avc_dec");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disables the deblocking filter at the cost of image quality */
|
||||||
|
public static final int DISABLE_LOOP_FILTER = 0x1;
|
||||||
|
/** Uses the low latency decode flag (disables multithreading) */
|
||||||
|
public static final int LOW_LATENCY_DECODE = 0x2;
|
||||||
|
/** Threads process each slice, rather than each frame */
|
||||||
|
public static final int SLICE_THREADING = 0x4;
|
||||||
|
/** Uses nonstandard speedup tricks */
|
||||||
|
public static final int FAST_DECODE = 0x8;
|
||||||
|
/** Uses bilinear filtering instead of bicubic */
|
||||||
|
public static final int BILINEAR_FILTERING = 0x10;
|
||||||
|
/** Uses a faster bilinear filtering with lower image quality */
|
||||||
|
public static final int FAST_BILINEAR_FILTERING = 0x20;
|
||||||
|
/** Disables color conversion (output is NV21) */
|
||||||
|
public static final int NO_COLOR_CONVERSION = 0x40;
|
||||||
|
|
||||||
|
public static native int init(int width, int height, int perflvl, int threadcount);
|
||||||
|
public static native void destroy();
|
||||||
|
|
||||||
|
// Rendering API when NO_COLOR_CONVERSION == 0
|
||||||
|
public static native boolean setRenderTarget(Object androidSurface);
|
||||||
|
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
|
||||||
|
public static native boolean redraw();
|
||||||
|
|
||||||
|
// Rendering API when NO_COLOR_CONVERSION == 1
|
||||||
|
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
|
||||||
|
|
||||||
|
public static native int getInputPaddingSize();
|
||||||
|
public static native int decode(byte[] indata, int inoff, int inlen);
|
||||||
|
}
|
31
moonlight-common/src/com/limelight/nvstream/http/NvApp.java
Normal file
31
moonlight-common/src/com/limelight/nvstream/http/NvApp.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
public class NvApp {
|
||||||
|
private String appName;
|
||||||
|
private int appId;
|
||||||
|
private boolean isRunning;
|
||||||
|
|
||||||
|
public void setAppName(String appName) {
|
||||||
|
this.appName = appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppId(String appId) {
|
||||||
|
this.appId = Integer.parseInt(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsRunning(String isRunning) {
|
||||||
|
this.isRunning = isRunning.equals("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppName() {
|
||||||
|
return this.appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAppId() {
|
||||||
|
return this.appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getIsRunning() {
|
||||||
|
return this.isRunning;
|
||||||
|
}
|
||||||
|
}
|
145
moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java
Normal file
145
moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory;
|
||||||
|
|
||||||
|
|
||||||
|
public class NvHTTP {
|
||||||
|
private String macAddress;
|
||||||
|
private String deviceName;
|
||||||
|
|
||||||
|
public static final int PORT = 47989;
|
||||||
|
public static final int CONNECTION_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
public String baseUrl;
|
||||||
|
|
||||||
|
public NvHTTP(InetAddress host, String macAddress, String deviceName) {
|
||||||
|
this.macAddress = macAddress;
|
||||||
|
this.deviceName = deviceName;
|
||||||
|
this.baseUrl = "http://" + host.getHostAddress() + ":" + PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getXmlString(InputStream in, String tagname)
|
||||||
|
throws XmlPullParserException, IOException {
|
||||||
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
XmlPullParser xpp = factory.newPullParser();
|
||||||
|
|
||||||
|
xpp.setInput(new InputStreamReader(in));
|
||||||
|
int eventType = xpp.getEventType();
|
||||||
|
Stack<String> currentTag = new Stack<String>();
|
||||||
|
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
switch (eventType) {
|
||||||
|
case (XmlPullParser.START_TAG):
|
||||||
|
currentTag.push(xpp.getName());
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.END_TAG):
|
||||||
|
currentTag.pop();
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.TEXT):
|
||||||
|
if (currentTag.peek().equals(tagname)) {
|
||||||
|
return xpp.getText();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eventType = xpp.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream openHttpConnection(String url) throws IOException {
|
||||||
|
URLConnection conn = new URL(url).openConnection();
|
||||||
|
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||||
|
conn.setDefaultUseCaches(false);
|
||||||
|
conn.connect();
|
||||||
|
return conn.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppVersion() throws XmlPullParserException, IOException {
|
||||||
|
InputStream in = openHttpConnection(baseUrl + "/appversion");
|
||||||
|
return getXmlString(in, "appversion");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getPairState() throws IOException, XmlPullParserException {
|
||||||
|
InputStream in = openHttpConnection(baseUrl + "/pairstate?mac=" + macAddress);
|
||||||
|
String paired = getXmlString(in, "paired");
|
||||||
|
return Integer.valueOf(paired) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSessionId() throws IOException, XmlPullParserException {
|
||||||
|
InputStream in = openHttpConnection(baseUrl + "/pair?mac=" + macAddress
|
||||||
|
+ "&devicename=" + deviceName);
|
||||||
|
String sessionId = getXmlString(in, "sessionid");
|
||||||
|
return Integer.parseInt(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSteamAppId(int sessionId) throws IOException,
|
||||||
|
XmlPullParserException {
|
||||||
|
LinkedList<NvApp> appList = getAppList(sessionId);
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
if (app.getAppName().equals("Steam")) {
|
||||||
|
return app.getAppId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedList<NvApp> getAppList(int sessionId) throws IOException, XmlPullParserException {
|
||||||
|
InputStream in = openHttpConnection(baseUrl + "/applist?session=" + sessionId);
|
||||||
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
XmlPullParser xpp = factory.newPullParser();
|
||||||
|
|
||||||
|
xpp.setInput(new InputStreamReader(in));
|
||||||
|
int eventType = xpp.getEventType();
|
||||||
|
LinkedList<NvApp> appList = new LinkedList<NvApp>();
|
||||||
|
Stack<String> currentTag = new Stack<String>();
|
||||||
|
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
switch (eventType) {
|
||||||
|
case (XmlPullParser.START_TAG):
|
||||||
|
currentTag.push(xpp.getName());
|
||||||
|
if (xpp.getName().equals("App")) {
|
||||||
|
appList.addLast(new NvApp());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.END_TAG):
|
||||||
|
currentTag.pop();
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.TEXT):
|
||||||
|
NvApp app = appList.getLast();
|
||||||
|
if (currentTag.peek().equals("AppTitle")) {
|
||||||
|
app.setAppName(xpp.getText());
|
||||||
|
} else if (currentTag.peek().equals("ID")) {
|
||||||
|
app.setAppId(xpp.getText());
|
||||||
|
} else if (currentTag.peek().equals("IsRunning")) {
|
||||||
|
app.setIsRunning(xpp.getText());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eventType = xpp.next();
|
||||||
|
}
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns gameSession XML attribute
|
||||||
|
public int launchApp(int sessionId, int appId) throws IOException,
|
||||||
|
XmlPullParserException {
|
||||||
|
InputStream in = openHttpConnection(baseUrl + "/launch?session="
|
||||||
|
+ sessionId + "&appid=" + appId);
|
||||||
|
String gameSession = getXmlString(in, "gamesession");
|
||||||
|
return Integer.parseInt(gameSession);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public class ControllerPacket extends InputPacket {
|
||||||
|
public static final byte[] HEADER =
|
||||||
|
{
|
||||||
|
0x0A,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x14
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final byte[] TAIL =
|
||||||
|
{
|
||||||
|
(byte)0x9C,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x55,
|
||||||
|
0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final int PACKET_TYPE = 0x18;
|
||||||
|
|
||||||
|
public static final short A_FLAG = 0x1000;
|
||||||
|
public static final short B_FLAG = 0x2000;
|
||||||
|
public static final short X_FLAG = 0x4000;
|
||||||
|
public static final short Y_FLAG = (short)0x8000;
|
||||||
|
public static final short UP_FLAG = 0x0001;
|
||||||
|
public static final short DOWN_FLAG = 0x0002;
|
||||||
|
public static final short LEFT_FLAG = 0x0004;
|
||||||
|
public static final short RIGHT_FLAG = 0x0008;
|
||||||
|
public static final short LB_FLAG = 0x0100;
|
||||||
|
public static final short RB_FLAG = 0x0200;
|
||||||
|
public static final short PLAY_FLAG = 0x0010;
|
||||||
|
public static final short BACK_FLAG = 0x0020;
|
||||||
|
public static final short LS_CLK_FLAG = 0x0040;
|
||||||
|
public static final short RS_CLK_FLAG = 0x0080;
|
||||||
|
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||||
|
|
||||||
|
public static final short PAYLOAD_LENGTH = 24;
|
||||||
|
public static final short PACKET_LENGTH = PAYLOAD_LENGTH +
|
||||||
|
InputPacket.HEADER_LENGTH;
|
||||||
|
|
||||||
|
private short buttonFlags;
|
||||||
|
private byte leftTrigger;
|
||||||
|
private byte rightTrigger;
|
||||||
|
private short leftStickX;
|
||||||
|
private short leftStickY;
|
||||||
|
private short rightStickX;
|
||||||
|
private short rightStickY;
|
||||||
|
|
||||||
|
public ControllerPacket(short buttonFlags, byte leftTrigger, byte rightTrigger,
|
||||||
|
short leftStickX, short leftStickY,
|
||||||
|
short rightStickX, short rightStickY)
|
||||||
|
{
|
||||||
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
|
this.buttonFlags = buttonFlags;
|
||||||
|
this.leftTrigger = leftTrigger;
|
||||||
|
this.rightTrigger = rightTrigger;
|
||||||
|
this.leftStickX = leftStickX;
|
||||||
|
this.leftStickY = leftStickY;
|
||||||
|
this.rightStickX = rightStickX;
|
||||||
|
this.rightStickY = rightStickY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toWire()
|
||||||
|
{
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(PACKET_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
bb.put(toWireHeader());
|
||||||
|
bb.put(HEADER);
|
||||||
|
bb.putShort(buttonFlags);
|
||||||
|
bb.put(leftTrigger);
|
||||||
|
bb.put(rightTrigger);
|
||||||
|
bb.putShort(leftStickX);
|
||||||
|
bb.putShort(leftStickY);
|
||||||
|
bb.putShort(rightStickX);
|
||||||
|
bb.putShort(rightStickY);
|
||||||
|
bb.put(TAIL);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public abstract class InputPacket {
|
||||||
|
public static final int HEADER_LENGTH = 0x4;
|
||||||
|
|
||||||
|
protected int packetType;
|
||||||
|
|
||||||
|
public InputPacket(int packetType)
|
||||||
|
{
|
||||||
|
this.packetType = packetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract byte[] toWire();
|
||||||
|
|
||||||
|
public byte[] toWireHeader()
|
||||||
|
{
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
bb.putInt(packetType);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public class MouseButtonPacket extends InputPacket {
|
||||||
|
|
||||||
|
private byte buttonEventType;
|
||||||
|
|
||||||
|
public static final int PACKET_TYPE = 0x5;
|
||||||
|
public static final int PAYLOAD_LENGTH = 5;
|
||||||
|
public static final int PACKET_LENGTH = PAYLOAD_LENGTH +
|
||||||
|
InputPacket.HEADER_LENGTH;
|
||||||
|
|
||||||
|
public static final byte PRESS_EVENT = 0x07;
|
||||||
|
public static final byte RELEASE_EVENT = 0x08;
|
||||||
|
|
||||||
|
public MouseButtonPacket(boolean leftButtonDown)
|
||||||
|
{
|
||||||
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
|
buttonEventType = leftButtonDown ?
|
||||||
|
PRESS_EVENT : RELEASE_EVENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toWire() {
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(PACKET_LENGTH).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
bb.put(toWireHeader());
|
||||||
|
bb.put(buttonEventType);
|
||||||
|
bb.putInt(1); // FIXME: button index?
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class MouseMovePacket extends InputPacket {
|
||||||
|
|
||||||
|
private static final byte[] HEADER =
|
||||||
|
{
|
||||||
|
0x06,
|
||||||
|
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;
|
||||||
|
|
||||||
|
private short deltaX;
|
||||||
|
private short deltaY;
|
||||||
|
|
||||||
|
public MouseMovePacket(short deltaX, short deltaY)
|
||||||
|
{
|
||||||
|
super(PACKET_TYPE);
|
||||||
|
|
||||||
|
this.deltaX = deltaX;
|
||||||
|
this.deltaY = deltaY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toWire() {
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(PACKET_LENGTH);
|
||||||
|
|
||||||
|
bb.put(toWireHeader());
|
||||||
|
bb.put(HEADER);
|
||||||
|
bb.putShort(deltaX);
|
||||||
|
bb.putShort(deltaY);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class NvController {
|
||||||
|
|
||||||
|
public final static int PORT = 35043;
|
||||||
|
|
||||||
|
public final static int CONTROLLER_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
private InetAddress host;
|
||||||
|
private Socket s;
|
||||||
|
private OutputStream out;
|
||||||
|
|
||||||
|
public NvController(InetAddress host)
|
||||||
|
{
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize() throws IOException
|
||||||
|
{
|
||||||
|
s = new Socket();
|
||||||
|
s.connect(new InetSocketAddress(host, PORT), CONTROLLER_TIMEOUT);
|
||||||
|
s.setTcpNoDelay(true);
|
||||||
|
out = s.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
s.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInput(short buttonFlags, byte leftTrigger, byte rightTrigger,
|
||||||
|
short leftStickX, short leftStickY, short rightStickX, short rightStickY) throws IOException
|
||||||
|
{
|
||||||
|
out.write(new ControllerPacket(buttonFlags, leftTrigger,
|
||||||
|
rightTrigger, leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY).toWire());
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonDown() throws IOException
|
||||||
|
{
|
||||||
|
out.write(new MouseButtonPacket(true).toWire());
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonUp() throws IOException
|
||||||
|
{
|
||||||
|
out.write(new MouseButtonPacket(false).toWire());
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseMove(short deltaX, short deltaY) throws IOException
|
||||||
|
{
|
||||||
|
out.write(new MouseMovePacket(deltaX, deltaY).toWire());
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user