Add implementation of http negotiation, handshake, control, and input packets

This commit is contained in:
Cameron Gutman
2013-09-21 18:51:48 -04:00
parent b263367528
commit fcdc23eeb7
16 changed files with 1012 additions and 35 deletions

View File

@@ -0,0 +1,30 @@
package com.limelight;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.widget.MediaController;
import android.widget.VideoView;
public class Game extends Activity {
private VideoView vv;
private MediaController mc;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
vv = (VideoView) findViewById(R.id.videoView);
mc = new MediaController(this);
mc.setAnchorView(vv);
Uri video = Uri.parse("rtsp://141.213.191.236:47902/nvstream");
vv.setMediaController(mc);
vv.setVideoURI(video);
vv.start();
}
}

View File

@@ -0,0 +1,73 @@
package com.limelight.nvstream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import android.net.rtp.AudioCodec;
import android.net.rtp.AudioGroup;
import android.net.rtp.AudioStream;
public class NvAudioStream {
private AudioGroup group;
private AudioStream stream;
public static final int PORT = 48000;
/*public void startStream(String host) throws SocketException, UnknownHostException
{
System.out.println("Starting audio group");
group = new AudioGroup();
group.setMode(AudioGroup.MODE_NORMAL);
System.out.println("Starting audio stream");
stream = new AudioStream(InetAddress.getByAddress(new byte[]{0,0,0,0}));
stream.setMode(AudioStream.MODE_NORMAL);
stream.associate(InetAddress.getByName(host), PORT);
stream.setCodec(AudioCodec.PCMA);
stream.join(group);
for (AudioCodec c : AudioCodec.getCodecs())
System.out.println(c.type + " " + c.fmtp + " " + c.rtpmap);
System.out.println("Joined");
}*/
public void start()
{
new Thread(new Runnable() {
@Override
public void run() {
DatagramSocket ds;
try {
ds = new DatagramSocket(PORT);
} catch (SocketException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return;
}
for (;;)
{
DatagramPacket dp = new DatagramPacket(new byte[1500], 1500);
try {
ds.receive(dp);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
//System.out.println("Got UDP 48000: "+dp.getLength());
}
}
}).start();
}
}

View File

@@ -12,20 +12,67 @@ public class NvConnection {
this.host = host;
}
private void delay(int ms)
{
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
return;
}
}
public void doShit() throws XmlPullParserException, IOException
{
NvHttp h = new NvHttp(host, "b0:ee:45:57:5d:5f");
System.out.println("Shield Shit");
System.out.println("Begin Shield Action");
System.out.println(h.getAppVersion());
System.out.println(h.getPairState());
int sessionId = h.getSessionId();
System.out.println("Session ID: "+sessionId);
int appId = h.getSteamAppId(sessionId);
System.out.println("Steam app ID: "+appId);
int gameSession = h.launchApp(sessionId, appId);
System.out.println("Started game session: "+gameSession);
System.out.println("Starting handshake");
NvHandshake.performHandshake(host);
System.out.println("Handshake complete");
NvControl nvC = new NvControl(host);
System.out.println("Starting control");
nvC.beginControl();
System.out.println("Startup controller");
NvController controller = new NvController(host);
// Wait 3 seconds to start input
delay(3000);
System.out.println("Beginning controller input");
controller.sendLeftButton();
delay(100);
controller.clearButtons();
delay(250);
controller.sendRightButton();
delay(100);
controller.clearButtons();
delay(250);
controller.sendRightButton();
delay(100);
controller.clearButtons();
delay(250);
controller.sendRightButton();
delay(100);
controller.clearButtons();
delay(250);
controller.sendLeftButton();
delay(100);
controller.clearButtons();
new NvAudioStream().start();
new NvVideoStream().start(host);
}
}

View File

@@ -0,0 +1,384 @@
package com.limelight.nvstream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class NvControl {
public static final int PORT = 47995;
public static final short PTYPE_1 = 0x1204;
public static final short PPAYLEN_1 = 0x0004;
public static final byte[] PPAYLOAD_1 =
{
(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_1404 = 0x1404;
public static final short PPAYLEN_1404 = 0x0010;
public static final byte[] PPAYLOAD_1404 = new byte[]
{
0x02,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00
};
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,
167773203,
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_HELLO = 0x1204;
public static final short PPAYLEN_HELLO = 0x0004;
public static final byte[] PPAYLOAD_HELLO = new byte[] {
0x04, 0x00, 0x00, 0x05
};
private Socket s;
private InputStream in;
private OutputStream out;
public NvControl(String host) throws UnknownHostException, IOException
{
s = new Socket(host, PORT);
in = s.getInputStream();
out = s.getOutputStream();
}
public void sendPacket(NvCtlPacket packet) throws IOException
{
out.write(packet.toWire());
out.flush();
}
public NvControl.NvCtlResponse sendAndGetReply(NvCtlPacket packet) throws IOException
{
sendPacket(packet);
return new NvCtlResponse(in);
}
public void beginControl() throws IOException
{
System.out.println("CTL: Sending hello");
sendHello();
System.out.println("CTL: Sending config");
sendConfig();
System.out.println("CTL: Initial ping/pong");
pingPong();
System.out.println("CTL: Sending and waiting for 1405");
send1405AndGetResponse();
//System.out.println("CTL: Sending 1404");
//send1404();
System.out.println("CTL: Launching heartbeat thread");
new Thread(new Runnable() {
@Override
public void run() {
for (;;)
{
try {
sendHeartbeat();
} catch (IOException e1) {
e1.printStackTrace();
break;
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
break;
}
}
}
}).start();
}
public void endControl() throws IOException
{
s.close();
}
private NvControl.NvCtlResponse send1405AndGetResponse() throws IOException
{
return sendAndGetReply(new NvCtlPacket(PTYPE_1405, PPAYLEN_1405));
}
private void send1404() throws IOException
{
sendPacket(new NvCtlPacket(PTYPE_1404, PPAYLEN_1404));
}
private void sendHello() throws IOException
{
sendPacket(new NvCtlPacket(PTYPE_HELLO, PPAYLEN_HELLO, PPAYLOAD_HELLO));
}
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 NvControl.NvCtlResponse pingPong() throws IOException
{
sendPacket(new NvCtlPacket(PTYPE_KEEPALIVE, PPAYLEN_KEEPALIVE));
return new NvControl.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
{
offset = in.read(header, offset, header.length - offset);
} while (offset != header.length);
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
{
offset = in.read(payload, offset, payload.length - offset);
} while (offset != payload.length);
}
}
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;
}
}
}

View File

@@ -0,0 +1,40 @@
package com.limelight.nvstream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class NvController {
public final static int PORT = 35043;
private Socket s;
private OutputStream out;
public NvController(String host) throws UnknownHostException, IOException
{
s = new Socket(host, PORT);
out = s.getOutputStream();
}
// Example
public void sendLeftButton() throws IOException
{
out.write(new NvInputPacket(NvInputPacket.LEFT_FLAG, (byte)0, (byte)0, (short)0, (short)0).toWire());
}
// Example
public void sendRightButton() throws IOException
{
out.write(new NvInputPacket(NvInputPacket.RIGHT_FLAG, (byte)0, (byte)0, (short)0, (short)0).toWire());
}
// Example
public void clearButtons() throws IOException
{
out.write(new NvInputPacket((short)0, (byte)0, (byte)0, (short)0, (short)0).toWire());
}
}

View File

@@ -0,0 +1,107 @@
package com.limelight.nvstream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;;
public class NvHandshake {
public static final int PORT = 47991;
// android
public static final byte[] PLATFORM_HELLO =
{
(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 void waitAndDiscardResponse(InputStream in) throws IOException
{
// Wait for response and discard response
in.read();
try {
Thread.sleep(250);
} catch (InterruptedException e) { }
for (int i = 0; i < in.available(); i++)
in.read();
}
public static void performHandshake(String host) throws UnknownHostException, IOException
{
Socket s = new Socket(host, PORT);
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
// First packet
out.write(new byte[]{0x07, 0x00, 0x00, 0x00});
out.write(PLATFORM_HELLO);
System.out.println("HS: Waiting for hello response");
waitAndDiscardResponse(in);
// Second packet
out.write(PACKET_2);
System.out.println("HS: Waiting stage 2 response");
waitAndDiscardResponse(in);
// Third packet
out.write(PACKET_3);
System.out.println("HS: Waiting for stage 3 response");
waitAndDiscardResponse(in);
// Fourth packet
out.write(PACKET_4);
out.flush();
// Done
s.close();
}
}

View File

@@ -0,0 +1,78 @@
package com.limelight.nvstream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class NvInputPacket {
public static final byte[] HEADER =
{
0x00,
0x00,
0x00,
0x18,
0x0A,
0x00,
0x00,
0x00,
0x00,
0x14
};
public static final byte[] TAIL =
{
(byte)0x9C,
0x00,
0x00,
0x00,
0x55,
0x00
};
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 RB_FLAG = 0x0100;
public static final short LB_FLAG = 0x0200;
public static final short LS_CLK_FLAG = 0x0004;
public static final short RS_CLK_FLAG = 0x0008;
public static final short PLAY_FLAG = 0x0001;
public static final short BACK_FLAG = 0x0002;
public static final short PACKET_LENGTH = 28;
private short buttonFlags;
private byte leftTrigger;
private byte rightTrigger;
private short leftStick;
private short rightStick;
public NvInputPacket(short buttonFlags, byte leftTrigger, byte rightTrigger,
short leftStick, short rightStick)
{
this.buttonFlags = buttonFlags;
this.leftTrigger = leftTrigger;
this.rightTrigger = rightTrigger;
this.leftStick = leftStick;
this.rightStick = rightStick;
}
public byte[] toWire()
{
ByteBuffer bb = ByteBuffer.allocate(PACKET_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
bb.put(HEADER);
bb.putShort(buttonFlags);
bb.put(leftTrigger);
bb.put(rightTrigger);
bb.putShort(leftStick);
bb.putShort(rightStick);
bb.put(TAIL);
return bb.array();
}
}

View File

@@ -0,0 +1,71 @@
package com.limelight.nvstream;
import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
public class NvVideoStream {
public static final int PORT = 47998;
public static final int FIRST_FRAME_PORT = 47996;
private InputStream getFirstFrame(String host) throws UnknownHostException, IOException
{
Socket s = new Socket(host, FIRST_FRAME_PORT);
return s.getInputStream();
}
public void start(final String host)
{
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("VID: Waiting for first frame");
InputStream firstFrameStream = getFirstFrame(host);
firstFrameStream.read();
System.out.println("VID: First frame: "+firstFrameStream.available()+1);
firstFrameStream.close();
System.out.println("VID: Got first frame");
} catch (UnknownHostException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
return;
} catch (IOException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
return;
}
DatagramSocket ds;
try {
ds = new DatagramSocket(PORT);
} catch (SocketException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return;
}
for (;;)
{
DatagramPacket dp = new DatagramPacket(new byte[1500], 1500);
try {
ds.receive(dp);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
System.out.println("Got UDP 47998: "+dp.getLength());
}
}
}).start();
}
}