mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 02:20:55 +00:00
Remove code shared with limelight-common and implement Android bindings.
This commit is contained in:
@@ -5,5 +5,6 @@
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||
<classpathentry kind="lib" path="libs/limelight-common.jar"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
||||
|
||||
BIN
libs/limelight-common.jar
Normal file
BIN
libs/limelight-common.jar
Normal file
Binary file not shown.
@@ -8,12 +8,11 @@ import java.net.UnknownHostException;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.NvHTTP;
|
||||
import com.limelight.nvstream.NvmDNS;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
@@ -50,13 +49,6 @@ public class Connection extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Log.v("NvmDNS", "onCreate");
|
||||
|
||||
|
||||
NvmDNS dns = new NvmDNS();
|
||||
dns.execute();
|
||||
|
||||
|
||||
setContentView(R.layout.activity_connection);
|
||||
|
||||
this.statusButton = (Button) findViewById(R.id.statusButton);
|
||||
@@ -109,7 +101,8 @@ public class Connection extends Activity {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()), macAddress);
|
||||
httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
|
||||
macAddress, PlatformBinding.getDeviceName());
|
||||
try {
|
||||
if (httpConn.getPairState()) {
|
||||
message = "Already paired";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.limelight;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.NvConnectionListener;
|
||||
import com.limelight.nvstream.av.video.DecoderRenderer;
|
||||
import com.limelight.nvstream.input.NvControllerPacket;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
@@ -11,6 +12,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
@@ -22,6 +24,7 @@ import android.view.View.OnGenericMotionListener;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
||||
public class Game extends Activity implements OnGenericMotionListener, OnTouchListener, NvConnectionListener {
|
||||
@@ -77,15 +80,27 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
|
||||
int drFlags = 0;
|
||||
if (prefs.getBoolean(QUALITY_PREF_STRING, false)) {
|
||||
drFlags |= DecoderRenderer.FLAG_PREFER_QUALITY;
|
||||
drFlags |= VideoDecoderRenderer.FLAG_PREFER_QUALITY;
|
||||
}
|
||||
|
||||
// Warn the user if they're on a metered connection
|
||||
checkDataConnection();
|
||||
|
||||
// Start the connection
|
||||
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this, sv.getHolder(), drFlags);
|
||||
conn.start();
|
||||
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this);
|
||||
conn.start(PlatformBinding.getDeviceName(), sv.getHolder(), drFlags,
|
||||
PlatformBinding.getAudioRenderer(), PlatformBinding.chooseDecoderRenderer());
|
||||
}
|
||||
|
||||
private void checkDataConnection()
|
||||
{
|
||||
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (mgr.isActiveNetworkMetered()) {
|
||||
displayMessage("Warning: Your active network connection is metered!");
|
||||
}
|
||||
}
|
||||
|
||||
public void hideSystemUi() {
|
||||
private void hideSystemUi() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -128,58 +143,58 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
inputMap |= NvControllerPacket.PLAY_FLAG;
|
||||
inputMap |= ControllerPacket.PLAY_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
inputMap |= NvControllerPacket.BACK_FLAG;
|
||||
inputMap |= ControllerPacket.BACK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
inputMap |= NvControllerPacket.LEFT_FLAG;
|
||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
inputMap |= NvControllerPacket.RIGHT_FLAG;
|
||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
inputMap |= NvControllerPacket.UP_FLAG;
|
||||
inputMap |= ControllerPacket.UP_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
inputMap |= NvControllerPacket.DOWN_FLAG;
|
||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
inputMap |= NvControllerPacket.B_FLAG;
|
||||
inputMap |= ControllerPacket.B_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
inputMap |= NvControllerPacket.A_FLAG;
|
||||
inputMap |= ControllerPacket.A_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
inputMap |= NvControllerPacket.X_FLAG;
|
||||
inputMap |= ControllerPacket.X_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
inputMap |= NvControllerPacket.Y_FLAG;
|
||||
inputMap |= ControllerPacket.Y_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
inputMap |= NvControllerPacket.LB_FLAG;
|
||||
inputMap |= ControllerPacket.LB_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
inputMap |= NvControllerPacket.RB_FLAG;
|
||||
inputMap |= ControllerPacket.RB_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
inputMap |= NvControllerPacket.LS_CLK_FLAG;
|
||||
inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
inputMap |= NvControllerPacket.RS_CLK_FLAG;
|
||||
inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||
break;
|
||||
default:
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
// We detect back+start as the special button combo
|
||||
if ((inputMap & NvControllerPacket.BACK_FLAG) != 0 &&
|
||||
(inputMap & NvControllerPacket.PLAY_FLAG) != 0)
|
||||
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
|
||||
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||
{
|
||||
inputMap &= ~(NvControllerPacket.BACK_FLAG | NvControllerPacket.PLAY_FLAG);
|
||||
inputMap |= NvControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
|
||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
}
|
||||
|
||||
sendControllerInputPacket();
|
||||
@@ -191,57 +206,57 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
inputMap &= ~NvControllerPacket.PLAY_FLAG;
|
||||
inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
inputMap &= ~NvControllerPacket.BACK_FLAG;
|
||||
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
inputMap &= ~NvControllerPacket.LEFT_FLAG;
|
||||
inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
inputMap &= ~NvControllerPacket.RIGHT_FLAG;
|
||||
inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
inputMap &= ~NvControllerPacket.UP_FLAG;
|
||||
inputMap &= ~ControllerPacket.UP_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
inputMap &= ~NvControllerPacket.DOWN_FLAG;
|
||||
inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
inputMap &= ~NvControllerPacket.B_FLAG;
|
||||
inputMap &= ~ControllerPacket.B_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
inputMap &= ~NvControllerPacket.A_FLAG;
|
||||
inputMap &= ~ControllerPacket.A_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
inputMap &= ~NvControllerPacket.X_FLAG;
|
||||
inputMap &= ~ControllerPacket.X_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
inputMap &= ~NvControllerPacket.Y_FLAG;
|
||||
inputMap &= ~ControllerPacket.Y_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
inputMap &= ~NvControllerPacket.LB_FLAG;
|
||||
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
inputMap &= ~NvControllerPacket.RB_FLAG;
|
||||
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
inputMap &= ~NvControllerPacket.LS_CLK_FLAG;
|
||||
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
inputMap &= ~NvControllerPacket.RS_CLK_FLAG;
|
||||
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||
break;
|
||||
default:
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
// If one of the two is up, the special button comes up too
|
||||
if ((inputMap & NvControllerPacket.BACK_FLAG) == 0 ||
|
||||
(inputMap & NvControllerPacket.PLAY_FLAG) == 0)
|
||||
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
|
||||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
|
||||
{
|
||||
inputMap &= ~NvControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
}
|
||||
|
||||
sendControllerInputPacket();
|
||||
@@ -398,19 +413,19 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||
|
||||
inputMap &= ~(NvControllerPacket.LEFT_FLAG | NvControllerPacket.RIGHT_FLAG);
|
||||
inputMap &= ~(NvControllerPacket.UP_FLAG | NvControllerPacket.DOWN_FLAG);
|
||||
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||
if (hatX < -0.5) {
|
||||
inputMap |= NvControllerPacket.LEFT_FLAG;
|
||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
}
|
||||
if (hatX > 0.5) {
|
||||
inputMap |= NvControllerPacket.RIGHT_FLAG;
|
||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
}
|
||||
if (hatY < -0.5) {
|
||||
inputMap |= NvControllerPacket.UP_FLAG;
|
||||
inputMap |= ControllerPacket.UP_FLAG;
|
||||
}
|
||||
if (hatY > 0.5) {
|
||||
inputMap |= NvControllerPacket.DOWN_FLAG;
|
||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,5 +521,17 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
public void connectionStarted() {
|
||||
spinner.dismiss();
|
||||
spinner = null;
|
||||
|
||||
hideSystemUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayMessage(final String message) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
36
src/com/limelight/binding/PlatformBinding.java
Normal file
36
src/com/limelight/binding/PlatformBinding.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.limelight.binding;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||
import com.limelight.binding.video.AndroidCpuDecoderRenderer;
|
||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
public class PlatformBinding {
|
||||
public static VideoDecoderRenderer chooseDecoderRenderer() {
|
||||
if (Build.HARDWARE.equals("goldfish")) {
|
||||
// Emulator - don't render video (it's slow!)
|
||||
return null;
|
||||
}
|
||||
/*else if (MediaCodecDecoderRenderer.findSafeDecoder() != null) {
|
||||
// Hardware decoding
|
||||
return new MediaCodecDecoderRenderer();
|
||||
}*/
|
||||
else {
|
||||
// Software decoding
|
||||
return new AndroidCpuDecoderRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getDeviceName() {
|
||||
String deviceName = android.os.Build.MODEL;
|
||||
deviceName = deviceName.replace(" ", "");
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public static AudioRenderer getAudioRenderer() {
|
||||
return new AndroidAudioRenderer();
|
||||
}
|
||||
}
|
||||
50
src/com/limelight/binding/audio/AndroidAudioRenderer.java
Normal file
50
src/com/limelight/binding/audio/AndroidAudioRenderer.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.limelight.binding.audio;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
|
||||
public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
@Override
|
||||
public void streamInitialized(int channelCount, int sampleRate) {
|
||||
int channelConfig;
|
||||
|
||||
switch (channelCount)
|
||||
{
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Decoder returned unhandled channel count");
|
||||
}
|
||||
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
1024, // 1KB buffer
|
||||
AudioTrack.MODE_STREAM);
|
||||
|
||||
track.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(short[] audioData, int offset, int length) {
|
||||
track.write(audioData, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamClosing() {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.limelight.nvstream.av.video.cpu;
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
@@ -6,14 +6,14 @@ import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import com.limelight.nvstream.av.AvByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.AvDecodeUnit;
|
||||
import com.limelight.nvstream.av.video.DecoderRenderer;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||
|
||||
public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
private Thread rendererThread;
|
||||
private int targetFps;
|
||||
@@ -21,9 +21,6 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||
private ByteBuffer decoderBuffer;
|
||||
|
||||
private RsRenderer rsRenderer;
|
||||
private byte[] frameBuffer;
|
||||
|
||||
// Only sleep if the difference is above this value
|
||||
private static final int WAIT_CEILING_MS = 8;
|
||||
|
||||
@@ -81,7 +78,7 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup(Context context, int width, int height, SurfaceHolder renderTarget, int drFlags) {
|
||||
public void setup(int width, int height, Object renderTarget, int drFlags) {
|
||||
this.targetFps = 30;
|
||||
|
||||
int perfLevel = findOptimalPerformanceLevel();
|
||||
@@ -115,14 +112,8 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
break;
|
||||
}
|
||||
|
||||
// Create and initialize the RenderScript intrinsic we'll be using
|
||||
rsRenderer = new RsRenderer(context, width, height, renderTarget.getSurface());
|
||||
|
||||
// Allocate the frame buffer that the RGBA frame will be copied into
|
||||
frameBuffer = new byte[width*height*4];
|
||||
|
||||
// If the user wants quality, we'll remove the low IQ flags
|
||||
if ((drFlags & DecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||
// Make sure the loop filter is enabled
|
||||
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
||||
|
||||
@@ -137,6 +128,8 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||
}
|
||||
|
||||
AvcDecoder.setRenderTarget(((SurfaceHolder)renderTarget).getSurface());
|
||||
|
||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||
|
||||
System.out.println("Using software decoding (performance level: "+perfLevel+")");
|
||||
@@ -162,9 +155,7 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
}
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
if (AvcDecoder.getRgbFrame(frameBuffer, frameBuffer.length)) {
|
||||
rsRenderer.render(frameBuffer);
|
||||
}
|
||||
AvcDecoder.redraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -187,22 +178,18 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (rsRenderer != null) {
|
||||
rsRenderer.release();
|
||||
}
|
||||
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(AvDecodeUnit decodeUnit) {
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||
decoderBuffer.clear();
|
||||
|
||||
for (AvByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||
}
|
||||
|
||||
@@ -212,7 +199,7 @@ public class CpuDecoderRenderer implements DecoderRenderer {
|
||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||
|
||||
int offset = 0;
|
||||
for (AvByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||
offset += bbd.length;
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import com.limelight.nvstream.av.AvByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.AvDecodeUnit;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.os.Build;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class MediaCodecDecoderRenderer implements DecoderRenderer {
|
||||
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
private ByteBuffer[] videoDecoderInputBuffers;
|
||||
private MediaCodec videoDecoder;
|
||||
@@ -74,11 +71,11 @@ public class MediaCodecDecoderRenderer implements DecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup(Context context, int width, int height, SurfaceHolder renderTarget, int drFlags) {
|
||||
public void setup(int width, int height, Object renderTarget, int drFlags) {
|
||||
videoDecoder = MediaCodec.createByCodecName(findSafeDecoder().getName());
|
||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
||||
|
||||
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
|
||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
@@ -157,8 +154,8 @@ public class MediaCodecDecoderRenderer implements DecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(AvDecodeUnit decodeUnit) {
|
||||
if (decodeUnit.getType() != AvDecodeUnit.TYPE_H264) {
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
if (decodeUnit.getType() != DecodeUnit.TYPE_H264) {
|
||||
System.err.println("Unknown decode unit type");
|
||||
return false;
|
||||
}
|
||||
@@ -172,7 +169,7 @@ public class MediaCodecDecoderRenderer implements DecoderRenderer {
|
||||
buf.clear();
|
||||
|
||||
// Copy data from our buffer list into the input buffer
|
||||
for (AvByteBufferDescriptor desc : decodeUnit.getBufferList())
|
||||
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
||||
{
|
||||
buf.put(desc.data, desc.offset, desc.length);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.limelight.nvstream.av.video.cpu;
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import android.content.Context;
|
||||
import android.renderscript.Allocation;
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
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.av.AvByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.AvRtpPacket;
|
||||
import com.limelight.nvstream.av.AvShortBufferDescriptor;
|
||||
import com.limelight.nvstream.av.audio.AvAudioDepacketizer;
|
||||
import com.limelight.nvstream.av.audio.OpusDecoder;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
|
||||
public class NvAudioStream {
|
||||
public static final int RTP_PORT = 48000;
|
||||
public static final int RTCP_PORT = 47999;
|
||||
|
||||
private LinkedBlockingQueue<AvRtpPacket> packets = new LinkedBlockingQueue<AvRtpPacket>(100);
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
private DatagramSocket rtp;
|
||||
|
||||
private AvAudioDepacketizer depacketizer = new AvAudioDepacketizer();
|
||||
|
||||
private LinkedList<Thread> threads = new LinkedList<Thread>();
|
||||
|
||||
private boolean aborting = false;
|
||||
|
||||
private InetAddress host;
|
||||
private NvConnectionListener listener;
|
||||
|
||||
public NvAudioStream(InetAddress host, NvConnectionListener listener)
|
||||
{
|
||||
this.host = host;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
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) { }
|
||||
}
|
||||
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
|
||||
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 channelConfig;
|
||||
int err;
|
||||
|
||||
err = OpusDecoder.init();
|
||||
if (err != 0) {
|
||||
throw new IllegalStateException("Opus decoder failed to initialize");
|
||||
}
|
||||
|
||||
switch (OpusDecoder.getChannelCount())
|
||||
{
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Opus decoder returned unhandled channel count");
|
||||
}
|
||||
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
OpusDecoder.getSampleRate(),
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
1024, // 1KB buffer
|
||||
AudioTrack.MODE_STREAM);
|
||||
|
||||
track.play();
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
AvRtpPacket packet;
|
||||
|
||||
try {
|
||||
packet = packets.take();
|
||||
} catch (InterruptedException e) {
|
||||
listener.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())
|
||||
{
|
||||
AvShortBufferDescriptor samples;
|
||||
|
||||
try {
|
||||
samples = depacketizer.getNextDecodedData();
|
||||
} catch (InterruptedException e) {
|
||||
listener.connectionTerminated(e);
|
||||
return;
|
||||
}
|
||||
|
||||
track.write(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() {
|
||||
AvByteBufferDescriptor desc = new AvByteBufferDescriptor(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 AvRtpPacket(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) {
|
||||
listener.connectionTerminated(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
listener.connectionTerminated(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
threads.add(t);
|
||||
t.setPriority(Thread.MIN_PRIORITY);
|
||||
t.setName("Audio - Ping");
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class NvComputer {
|
||||
private String hostname;
|
||||
private InetAddress ipAddress;
|
||||
private String ipAddressString;
|
||||
private int state;
|
||||
private int numOfApps;
|
||||
private String gpuType;
|
||||
private String mac;
|
||||
private UUID uniqueID;
|
||||
|
||||
private NvHTTP nvHTTP;
|
||||
|
||||
|
||||
private int sessionID;
|
||||
private boolean pairState;
|
||||
private boolean isBusy;
|
||||
|
||||
public NvComputer(String hostname, InetAddress ipAddress, int state, int numOfApps, String gpuType, String mac, UUID uniqueID) {
|
||||
this.hostname = hostname;
|
||||
this.ipAddress = ipAddress;
|
||||
this.ipAddressString = this.ipAddress.getHostAddress();
|
||||
this.state = state;
|
||||
this.numOfApps = numOfApps;
|
||||
this.gpuType = gpuType;
|
||||
this.mac = mac;
|
||||
this.uniqueID = uniqueID;
|
||||
|
||||
try {
|
||||
this.nvHTTP = new NvHTTP(this.ipAddress, NvConnection.getMacAddressString());
|
||||
} catch (SocketException e) {
|
||||
Log.e("NvComputer Constructor", "Unable to get MAC Address " + e.getMessage());
|
||||
this.nvHTTP = new NvHTTP(this.ipAddress, "00:00:00:00:00:00");
|
||||
}
|
||||
|
||||
this.updatePairState();
|
||||
}
|
||||
|
||||
public String getHostname() {
|
||||
return this.hostname;
|
||||
}
|
||||
|
||||
public InetAddress getIpAddress() {
|
||||
return this.ipAddress;
|
||||
}
|
||||
|
||||
public String getIpAddressString() {
|
||||
return this.ipAddressString;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public int getNumOfApps() {
|
||||
return this.numOfApps;
|
||||
}
|
||||
|
||||
public String getGpuType() {
|
||||
return this.gpuType;
|
||||
}
|
||||
|
||||
public String getMac() {
|
||||
return this.mac;
|
||||
}
|
||||
|
||||
public UUID getUniqueID() {
|
||||
return this.uniqueID;
|
||||
}
|
||||
|
||||
public void updateAfterPairQuery(int sessionID, boolean paired, boolean isBusy) {
|
||||
this.sessionID = sessionID;
|
||||
this.pairState = paired;
|
||||
this.isBusy = isBusy;
|
||||
}
|
||||
|
||||
public int getSessionID() {
|
||||
return this.sessionID;
|
||||
}
|
||||
|
||||
public void updatePairState() {
|
||||
try {
|
||||
this.pairState = this.nvHTTP.getPairState();
|
||||
} catch (IOException e) {
|
||||
Log.e("NvComputer UpdatePaired", "Unable to get Pair State " + e.getMessage());
|
||||
this.pairState = false;
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.e("NvComputer UpdatePaired", "Unable to get Pair State " + e.getMessage());
|
||||
this.pairState = false;
|
||||
}
|
||||
|
||||
/*if (this.pairState == true) {
|
||||
try {
|
||||
this.sessionID = this.nvHTTP.getSessionId();
|
||||
} catch (IOException e) {
|
||||
Log.e("NvComputer UpdatePaired", "Unable to get Session ID " + e.getMessage());
|
||||
this.sessionID = 0;
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.e("NvComputer UpdatePaired", "Unable to get Session ID " + e.getMessage());
|
||||
this.sessionID = 0;
|
||||
}
|
||||
|
||||
}*/
|
||||
}
|
||||
|
||||
public boolean getPairState() {
|
||||
return this.pairState;
|
||||
}
|
||||
|
||||
public boolean getIsBusy() {
|
||||
return this.isBusy;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
if (this.ipAddress == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return this.ipAddressString.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder returnStringBuilder = new StringBuilder();
|
||||
returnStringBuilder.append("NvComputer 0x");
|
||||
returnStringBuilder.append(Integer.toHexString(this.hashCode()).toUpperCase(Locale.getDefault()));
|
||||
returnStringBuilder.append("\n|- Hostname: ");
|
||||
returnStringBuilder.append(this.hostname);
|
||||
returnStringBuilder.append("\n|- IP Address: ");
|
||||
returnStringBuilder.append(this.ipAddressString);
|
||||
returnStringBuilder.append("\n|- Computer State: ");
|
||||
returnStringBuilder.append(this.state);
|
||||
returnStringBuilder.append("\n|- Number of Apps: ");
|
||||
returnStringBuilder.append(this.numOfApps);
|
||||
returnStringBuilder.append("\n|- GPU: ");
|
||||
returnStringBuilder.append(this.gpuType);
|
||||
returnStringBuilder.append("\n|- MAC: ");
|
||||
returnStringBuilder.append(this.mac);
|
||||
returnStringBuilder.append("\n|- UniqueID: ");
|
||||
returnStringBuilder.append(this.uniqueID);
|
||||
returnStringBuilder.append("\n\\- Pair State: ");
|
||||
returnStringBuilder.append(this.pairState);
|
||||
returnStringBuilder.append("\n");
|
||||
return returnStringBuilder.toString();
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof UUID) {
|
||||
return this.uniqueID.equals(obj);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
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 android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.Game;
|
||||
import com.limelight.nvstream.input.NvController;
|
||||
|
||||
public class NvConnection {
|
||||
private String host;
|
||||
private Game activity;
|
||||
private NvConnectionListener listener;
|
||||
private int drFlags;
|
||||
|
||||
private InetAddress hostAddr;
|
||||
private NvControl controlStream;
|
||||
private NvController inputStream;
|
||||
private SurfaceHolder video;
|
||||
private NvVideoStream videoStream;
|
||||
private NvAudioStream audioStream;
|
||||
|
||||
private ThreadPoolExecutor threadPool;
|
||||
|
||||
public NvConnection(String host, Game activity, SurfaceHolder video, int drFlags)
|
||||
{
|
||||
this.host = host;
|
||||
this.listener = activity;
|
||||
this.activity = activity;
|
||||
this.video = video;
|
||||
this.drFlags = drFlags;
|
||||
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());
|
||||
|
||||
if (!h.getPairState()) {
|
||||
displayToast("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 NvControl(hostAddr, listener);
|
||||
controlStream.initialize();
|
||||
controlStream.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean startVideoStream() throws IOException
|
||||
{
|
||||
videoStream = new NvVideoStream(hostAddr, listener, controlStream);
|
||||
videoStream.startVideoStream(activity, video, drFlags);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean startAudioStream() throws IOException
|
||||
{
|
||||
audioStream = new NvAudioStream(hostAddr, listener);
|
||||
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 = NvHandshake.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()
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkDataConnection();
|
||||
|
||||
try {
|
||||
hostAddr = InetAddress.getByName(host);
|
||||
} catch (UnknownHostException e) {
|
||||
displayToast(e.getMessage());
|
||||
listener.connectionTerminated(e);
|
||||
return;
|
||||
}
|
||||
|
||||
establishConnection();
|
||||
|
||||
activity.hideSystemUi();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void checkDataConnection()
|
||||
{
|
||||
ConnectivityManager mgr = (ConnectivityManager) activity.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (mgr.isActiveNetworkMetered()) {
|
||||
displayToast("Warning: Your active network connection is metered!");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayToast(final String message)
|
||||
{
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,505 +0,0 @@
|
||||
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 NvControl 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 NvControl(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 NvControl.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 NvControl.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 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
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
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;
|
||||
|
||||
public static final int PORT = 47989;
|
||||
|
||||
public static final int CONNECTION_TIMEOUT = 5000;
|
||||
|
||||
|
||||
public String baseUrl;
|
||||
|
||||
public NvHTTP(InetAddress host, String macAddress) {
|
||||
this.macAddress = macAddress;
|
||||
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 {
|
||||
/* Pass the model (minus spaces) as the device name */
|
||||
String deviceName = android.os.Build.MODEL;
|
||||
deviceName = deviceName.replace(" ", "");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
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 NvHandshake {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
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.av.AvByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.AvDecodeUnit;
|
||||
import com.limelight.nvstream.av.AvRtpPacket;
|
||||
import com.limelight.nvstream.av.ConnectionStatusListener;
|
||||
import com.limelight.nvstream.av.video.AvVideoDepacketizer;
|
||||
import com.limelight.nvstream.av.video.AvVideoPacket;
|
||||
import com.limelight.nvstream.av.video.DecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.MediaCodecDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.cpu.CpuDecoderRenderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
public class NvVideoStream {
|
||||
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<AvRtpPacket> packets = new LinkedBlockingQueue<AvRtpPacket>(100);
|
||||
|
||||
private InetAddress host;
|
||||
private DatagramSocket rtp;
|
||||
private Socket firstFrameSocket;
|
||||
|
||||
private LinkedList<Thread> threads = new LinkedList<Thread>();
|
||||
|
||||
private NvConnectionListener listener;
|
||||
private AvVideoDepacketizer depacketizer;
|
||||
|
||||
private DecoderRenderer decrend;
|
||||
private boolean startedRendering;
|
||||
|
||||
private boolean aborting = false;
|
||||
|
||||
public NvVideoStream(InetAddress host, NvConnectionListener listener, ConnectionStatusListener avConnListener)
|
||||
{
|
||||
this.host = host;
|
||||
this.listener = listener;
|
||||
this.depacketizer = new AvVideoDepacketizer(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 AvVideoPacket(new AvByteBufferDescriptor(firstFrame, 0, offset)));
|
||||
} finally {
|
||||
firstFrameSocket.close();
|
||||
firstFrameSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setupRtpSession() throws SocketException
|
||||
{
|
||||
rtp = new DatagramSocket(RTP_PORT);
|
||||
}
|
||||
|
||||
public void setupDecoderRenderer(Context context, SurfaceHolder renderTarget, int drFlags) {
|
||||
if (Build.HARDWARE.equals("goldfish")) {
|
||||
// Emulator - don't render video (it's slow!)
|
||||
decrend = null;
|
||||
}
|
||||
else if (MediaCodecDecoderRenderer.findSafeDecoder() != null) {
|
||||
// Hardware decoding
|
||||
decrend = new MediaCodecDecoderRenderer();
|
||||
}
|
||||
else {
|
||||
// Software decoding
|
||||
decrend = new CpuDecoderRenderer();
|
||||
}
|
||||
|
||||
if (decrend != null) {
|
||||
decrend.setup(context, 1280, 720, renderTarget, drFlags);
|
||||
}
|
||||
}
|
||||
|
||||
public void startVideoStream(Context context, SurfaceHolder surface, int drFlags) throws IOException
|
||||
{
|
||||
// Setup the decoder and renderer
|
||||
setupDecoderRenderer(context, surface, 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())
|
||||
{
|
||||
AvDecodeUnit 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())
|
||||
{
|
||||
AvRtpPacket 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() {
|
||||
AvByteBufferDescriptor desc = new AvByteBufferDescriptor(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 AvRtpPacket(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();
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.MulticastSocket;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.xbill.DNS.DClass;
|
||||
import org.xbill.DNS.Header;
|
||||
import org.xbill.DNS.Message;
|
||||
import org.xbill.DNS.Name;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Section;
|
||||
import org.xbill.DNS.TXTRecord;
|
||||
import org.xbill.DNS.TextParseException;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
public class NvmDNS extends AsyncTask<Void, Integer, Void> {
|
||||
|
||||
public static String MDNS_QUERY = "_nvstream._tcp.local.";
|
||||
public static String MDNS_MULTICAST_GROUP = "224.0.0.251";
|
||||
public static InetAddress MDNS_MULTICAST_ADDRESS;
|
||||
public static final short MDNS_PORT = 5353;
|
||||
|
||||
public static final int WAIT_MS = 1000;
|
||||
|
||||
private HashSet<NvComputer> responses;
|
||||
private MulticastSocket socket;
|
||||
|
||||
static {
|
||||
try {
|
||||
MDNS_MULTICAST_ADDRESS = InetAddress.getByName(NvmDNS.MDNS_MULTICAST_GROUP);
|
||||
} catch (UnknownHostException e) {
|
||||
MDNS_MULTICAST_ADDRESS = null;
|
||||
}
|
||||
}
|
||||
|
||||
public NvmDNS() {
|
||||
this.responses = new HashSet<NvComputer>();
|
||||
|
||||
// Create our Socket Connection
|
||||
try {
|
||||
this.socket = new MulticastSocket(NvmDNS.MDNS_PORT);
|
||||
this.socket.setLoopbackMode(false);
|
||||
this.socket.joinGroup(NvmDNS.MDNS_MULTICAST_ADDRESS);
|
||||
Log.v("NvmDNS Socket Constructor", "Created mDNS listening socket");
|
||||
} catch (IOException e) {
|
||||
Log.e("NvmDNS Socket Constructor", "There was an error creating the DNS socket.");
|
||||
Log.e("NvmDNS Socket Constructor", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<NvComputer> getComputers() {
|
||||
return Collections.unmodifiableSet(this.responses);
|
||||
}
|
||||
|
||||
private void sendQuery() {
|
||||
Header queryHeader = new Header();
|
||||
|
||||
// If we set the RA (Recursion Available) flag and our message ID to 0
|
||||
// then the packet matches the real mDNS query packet as displayed in Wireshark
|
||||
queryHeader.setFlag(org.xbill.DNS.Flags.RA);
|
||||
queryHeader.setID(0);
|
||||
|
||||
Record question = null;
|
||||
try {
|
||||
// We need to create our "Question" DNS query that is a pointer record to
|
||||
// the mDNS Query "Name"
|
||||
question = Record.newRecord(new Name(NvmDNS.MDNS_QUERY), Type.PTR, DClass.IN);
|
||||
} catch (TextParseException e) {
|
||||
Log.e("NvmDNS Query", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// We combine our header and our question into a single message
|
||||
Message query = new Message();
|
||||
query.setHeader(queryHeader);
|
||||
query.addRecord(question, Section.QUESTION);
|
||||
|
||||
// Convert the message into Network Byte Order
|
||||
byte[] wireQuery = query.toWire();
|
||||
Log.i("NvmDNS Query", query.toString());
|
||||
|
||||
// Convert our byte array into a Packet
|
||||
DatagramPacket transmitPacket = new DatagramPacket(wireQuery, wireQuery.length);
|
||||
transmitPacket.setAddress(NvmDNS.MDNS_MULTICAST_ADDRESS);
|
||||
transmitPacket.setPort(NvmDNS.MDNS_PORT);
|
||||
|
||||
// And (attempt) to send the packet
|
||||
try {
|
||||
Log.d("NvmDNS Query", "Blocking on this.nvstream_socket.send(transmitPacket)");
|
||||
this.socket.send(transmitPacket);
|
||||
Log.d("NvmDNS Query", "Passed this.nvstream_socket.send(transmitPacket)");
|
||||
} catch (IOException e) {
|
||||
Log.e("NvmDNS Query", "There was an error sending the DNS query.");
|
||||
Log.e("NvmDNS Query", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForResponses() {
|
||||
Log.v("NvmDNS Response", "mDNS Loop Started");
|
||||
|
||||
// We support up to 1500 byte packets
|
||||
byte[] data = new byte[1500];
|
||||
DatagramPacket packet = new DatagramPacket(data, data.length);
|
||||
|
||||
Message message = null;
|
||||
|
||||
while (!this.socket.isClosed()) {
|
||||
// Attempt to receive a packet/response
|
||||
try {
|
||||
Log.d("NvmDNS Response", "Blocking on this.nvstream_query_socket.recieve()");
|
||||
this.socket.receive(packet);
|
||||
Log.d("NvmDNS Response", "Blocking passed on this.nvstream_query_socket.recieve()");
|
||||
message = new Message(packet.getData());
|
||||
this.parseRecord(message, packet.getAddress());
|
||||
} catch (IOException e) {
|
||||
if (this.socket.isClosed()) {
|
||||
Log.e("NvmDNS Response", "The socket was closed on us. The timer must have been reached.");
|
||||
return;
|
||||
} else {
|
||||
Log.e("NvmDNS Response", "There was an error receiving the response.");
|
||||
Log.e("NvmDNS Response", e.getMessage());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseRecord(Message message, InetAddress address) {
|
||||
// We really only care about the ADDITIONAL section (specifically the text records)
|
||||
Record[] responses = message.getSectionArray(Section.ADDITIONAL);
|
||||
// We only want to process records that actually have a length, have an ANSWER
|
||||
// section that has stuff in it and that the ANSWER to our query is what we sent
|
||||
if (responses.length != 0 &&
|
||||
message.getSectionArray(Section.ANSWER).length != 0 &&
|
||||
message.getSectionArray(Section.ANSWER)[0].getName().toString().equals(NvmDNS.MDNS_QUERY)) {
|
||||
|
||||
Log.v("NvmDNS Response", "Got a packet from " + address.getCanonicalHostName());
|
||||
Log.v("NvmDNS Response", "Question: " + message.getSectionArray(Section.ANSWER)[0].getName().toString());
|
||||
Log.v("NvmDNS Response", "Response: " + responses[0].getName().toString());
|
||||
|
||||
|
||||
// TODO: The DNS entry we get is "XENITH._nvstream._tcp.local."
|
||||
// And the .'s in there are not actually periods. Or something.
|
||||
String hostname = responses[0].getName().toString();
|
||||
|
||||
// The records can be returned in any order, so we need to figure out which one is the TXTRecord
|
||||
// We get three records back: A TXTRecord, a SRVRecord and an ARecord
|
||||
TXTRecord txtRecord = null;
|
||||
|
||||
for (Record record : responses) {
|
||||
Log.v("NvmDNS Response", "We recieved a DNS repsonse with a " + record.getClass().getName() + " record.");
|
||||
if (record instanceof TXTRecord) {
|
||||
txtRecord = (TXTRecord)record;
|
||||
}
|
||||
}
|
||||
|
||||
if (txtRecord == null) {
|
||||
Log.e("NvmDNS Response", "We recieved a malformed DNS repsonse with no TXTRecord");
|
||||
return;
|
||||
}
|
||||
|
||||
this.parseTXTRecord(txtRecord, address, hostname);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseTXTRecord(TXTRecord txtRecord, InetAddress address, String hostname) {
|
||||
// The DNS library we are using does not use inferred generics :(
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayList<String> txtRecordStringList = new ArrayList<String>(txtRecord.getStrings());
|
||||
|
||||
if (txtRecordStringList.size() != 5) {
|
||||
Log.e("NvmDNS Response", "We recieved a malformed DNS repsonse with the improper amount of TXTRecord Entries.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The general format of the text records is:
|
||||
// SERVICE_STATE=1
|
||||
// SERVICE_NUMOFAPPS=5
|
||||
// SERVICE_GPUTYPE=GeForce GTX 760 x2
|
||||
// SERVICE_MAC=DE:AD:BE:EF:CA:FE
|
||||
// SERVICE_UNIQUEID={A Wild UUID Appeared!}
|
||||
// Every single record I've seen so far has been in this format
|
||||
try {
|
||||
int serviceState = Integer.parseInt(this.parseTXTRecordField(txtRecordStringList.get(0), "SERVICE_STATE"));
|
||||
int numberOfApps = Integer.parseInt(this.parseTXTRecordField(txtRecordStringList.get(1), "SERVICE_NUMOFAPPS"));
|
||||
String gpuType = this.parseTXTRecordField(txtRecordStringList.get(2), "SERVICE_GPUTYPE");
|
||||
String mac = this.parseTXTRecordField(txtRecordStringList.get(3), "SERVICE_MAC");
|
||||
UUID uniqueID = UUID.fromString(this.parseTXTRecordField(txtRecordStringList.get(4), "SERVICE_UNIQUEID"));
|
||||
|
||||
// We need to resolve the hostname in this thread so that we can use it in the GUI
|
||||
address.getCanonicalHostName();
|
||||
|
||||
NvComputer computer = new NvComputer(hostname, address, serviceState, numberOfApps, gpuType, mac, uniqueID);
|
||||
this.responses.add(computer);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
Log.e("NvmDNS Response", "We recieved a malformed DNS repsonse.");
|
||||
}
|
||||
}
|
||||
|
||||
private String parseTXTRecordField(String field, String key) {
|
||||
// Make sure that our key=value pair actually has our key in it
|
||||
if (!field.contains(key)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Make sure that our key=value pair only has one "=" in it.
|
||||
if (field.indexOf('=') != field.lastIndexOf('=')) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String[] split = field.split("=");
|
||||
|
||||
if (split.length != 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return split[1];
|
||||
}
|
||||
|
||||
// What follows is an implementation of Android's AsyncTask.
|
||||
// The first step is to send our query, then we start our
|
||||
// RX thread to parse responses. However we only want to accept
|
||||
// responses for a limited amount of time so we start a new thread
|
||||
// to kill the socket after a set amount of time
|
||||
// Then we return control to the foreground thread
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... thisParameterIsUseless) {
|
||||
Log.v("NvmDNS ASync", "doInBackground entered");
|
||||
|
||||
this.sendQuery();
|
||||
|
||||
|
||||
// We want to run our wait thread for an amount of time then close the socket.
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.v("NvmDNS Wait", "Going to sleep for " + NvmDNS.WAIT_MS + "ms");
|
||||
try {
|
||||
Thread.sleep(NvmDNS.WAIT_MS);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e("NvmDNS Wait", "Woke up from sleep before time.");
|
||||
Log.e("NvmDNS Wait", e.getMessage());
|
||||
}
|
||||
Log.v("NvmDNS Wait", "Woke up from sleep");
|
||||
NvmDNS.this.socket.close();
|
||||
Log.v("NvmDNS Wait", "Socket Closed");
|
||||
}
|
||||
}).start();
|
||||
|
||||
this.waitForResponses();
|
||||
|
||||
Log.v("NvmDNS ASync", "doInBackground exit");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Integer... progress) {
|
||||
Log.v("NvmDNS ASync", "onProgressUpdate");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void moreUselessParameters) {
|
||||
Log.v("NvmDNS ASync", "onPostExecute");
|
||||
for (NvComputer computer : this.responses) {
|
||||
Log.i("NvmDNS NvComputer", computer.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class AvByteBufferDescriptor {
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public AvByteBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public AvByteBufferDescriptor(AvByteBufferDescriptor 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();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AvDecodeUnit {
|
||||
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<AvByteBufferDescriptor> bufferList;
|
||||
private int dataLength;
|
||||
private int flags;
|
||||
|
||||
public AvDecodeUnit(int type, List<AvByteBufferDescriptor> 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<AvByteBufferDescriptor> getBufferList()
|
||||
{
|
||||
return bufferList;
|
||||
}
|
||||
|
||||
public int getDataLength()
|
||||
{
|
||||
return dataLength;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class AvRtpPacket {
|
||||
|
||||
private byte packetType;
|
||||
private short seqNum;
|
||||
private AvByteBufferDescriptor buffer;
|
||||
|
||||
public AvRtpPacket(AvByteBufferDescriptor buffer)
|
||||
{
|
||||
this.buffer = new AvByteBufferDescriptor(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 AvByteBufferDescriptor getNewPayloadDescriptor()
|
||||
{
|
||||
return new AvByteBufferDescriptor(buffer.data, buffer.offset+12, buffer.length-12);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class AvShortBufferDescriptor {
|
||||
public short[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public AvShortBufferDescriptor(short[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public AvShortBufferDescriptor(AvShortBufferDescriptor 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public interface ConnectionStatusListener {
|
||||
public void connectionTerminated();
|
||||
|
||||
public void connectionNeedsResync();
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.limelight.nvstream.av.audio;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.nvstream.av.AvByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.AvRtpPacket;
|
||||
import com.limelight.nvstream.av.AvShortBufferDescriptor;
|
||||
|
||||
public class AvAudioDepacketizer {
|
||||
|
||||
private static final int DU_LIMIT = 15;
|
||||
private LinkedBlockingQueue<AvShortBufferDescriptor> decodedUnits =
|
||||
new LinkedBlockingQueue<AvShortBufferDescriptor>(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 AvShortBufferDescriptor(pcmData, 0, decodeLen))) {
|
||||
// Clear out the queue
|
||||
decodedUnits.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void decodeInputData(AvRtpPacket 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
|
||||
AvByteBufferDescriptor rtpPayload = packet.getNewPayloadDescriptor();
|
||||
decodeData(rtpPayload.data, rtpPayload.offset, rtpPayload.length);
|
||||
}
|
||||
|
||||
public AvShortBufferDescriptor getNextDecodedData() throws InterruptedException
|
||||
{
|
||||
return decodedUnits.take();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.nvstream.av.AvByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.AvDecodeUnit;
|
||||
import com.limelight.nvstream.av.AvRtpPacket;
|
||||
import com.limelight.nvstream.av.ConnectionStatusListener;
|
||||
|
||||
public class AvVideoDepacketizer {
|
||||
|
||||
// Current NAL state
|
||||
private LinkedList<AvByteBufferDescriptor> avcNalDataChain = null;
|
||||
private int avcNalDataLength = 0;
|
||||
|
||||
// Sequencing state
|
||||
private short lastSequenceNumber;
|
||||
|
||||
private ConnectionStatusListener controlListener;
|
||||
|
||||
private static final int DU_LIMIT = 15;
|
||||
private LinkedBlockingQueue<AvDecodeUnit> decodedUnits = new LinkedBlockingQueue<AvDecodeUnit>(DU_LIMIT);
|
||||
|
||||
public AvVideoDepacketizer(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
|
||||
AvDecodeUnit du = new AvDecodeUnit(AvDecodeUnit.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(AvVideoPacket packet)
|
||||
{
|
||||
AvByteBufferDescriptor location = packet.getNewPayloadDescriptor();
|
||||
|
||||
// SPS and PPS packet doesn't have standard headers, so submit it as is
|
||||
if (location.length < 968) {
|
||||
avcNalDataChain = new LinkedList<AvByteBufferDescriptor>();
|
||||
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<AvByteBufferDescriptor>();
|
||||
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(AvRtpPacket 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
|
||||
AvByteBufferDescriptor rtpPayload = packet.getNewPayloadDescriptor();
|
||||
addInputData(new AvVideoPacket(rtpPayload));
|
||||
}
|
||||
|
||||
public AvDecodeUnit getNextDecodeUnit() throws InterruptedException
|
||||
{
|
||||
return decodedUnits.take();
|
||||
}
|
||||
}
|
||||
|
||||
class NAL {
|
||||
|
||||
// This assumes that the buffer passed in is already a special sequence
|
||||
public static boolean isAvcStartSequence(AvByteBufferDescriptor 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(AvByteBufferDescriptor 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(AvByteBufferDescriptor buffer, AvByteBufferDescriptor 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;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import com.limelight.nvstream.av.AvByteBufferDescriptor;
|
||||
|
||||
public class AvVideoPacket {
|
||||
private AvByteBufferDescriptor buffer;
|
||||
|
||||
private int frameIndex;
|
||||
private int packetIndex;
|
||||
private int totalPackets;
|
||||
private int payloadLength;
|
||||
|
||||
public AvVideoPacket(AvByteBufferDescriptor rtpPayload)
|
||||
{
|
||||
buffer = new AvByteBufferDescriptor(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 AvByteBufferDescriptor getNewPayloadDescriptor()
|
||||
{
|
||||
return new AvByteBufferDescriptor(buffer.data, buffer.offset+56, buffer.length-56);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
|
||||
import com.limelight.nvstream.av.AvDecodeUnit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
public interface DecoderRenderer {
|
||||
public static int FLAG_PREFER_QUALITY = 0x1;
|
||||
|
||||
public void setup(Context context, int width, int height, SurfaceHolder renderTarget, int drFlags);
|
||||
|
||||
public void start();
|
||||
|
||||
public void stop();
|
||||
|
||||
public void release();
|
||||
|
||||
public boolean submitDecodeUnit(AvDecodeUnit decodeUnit);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.limelight.nvstream.av.video.cpu;
|
||||
|
||||
import android.view.Surface;
|
||||
|
||||
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(Surface surface);
|
||||
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);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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 NvControllerPacket(buttonFlags, leftTrigger,
|
||||
rightTrigger, leftStickX, leftStickY,
|
||||
rightStickX, rightStickY).toWire());
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown() throws IOException
|
||||
{
|
||||
out.write(new NvMouseButtonPacket(true).toWire());
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public void sendMouseButtonUp() throws IOException
|
||||
{
|
||||
out.write(new NvMouseButtonPacket(false).toWire());
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public void sendMouseMove(short deltaX, short deltaY) throws IOException
|
||||
{
|
||||
out.write(new NvMouseMovePacket(deltaX, deltaY).toWire());
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class NvControllerPacket extends NvInputPacket {
|
||||
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 +
|
||||
NvInputPacket.HEADER_LENGTH;
|
||||
|
||||
private short buttonFlags;
|
||||
private byte leftTrigger;
|
||||
private byte rightTrigger;
|
||||
private short leftStickX;
|
||||
private short leftStickY;
|
||||
private short rightStickX;
|
||||
private short rightStickY;
|
||||
|
||||
public NvControllerPacket(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();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public abstract class NvInputPacket {
|
||||
public static final int HEADER_LENGTH = 0x4;
|
||||
|
||||
protected int packetType;
|
||||
|
||||
public NvInputPacket(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();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class NvMouseButtonPacket extends NvInputPacket {
|
||||
|
||||
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 +
|
||||
NvInputPacket.HEADER_LENGTH;
|
||||
|
||||
public static final byte PRESS_EVENT = 0x07;
|
||||
public static final byte RELEASE_EVENT = 0x08;
|
||||
|
||||
public NvMouseButtonPacket(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();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class NvMouseMovePacket extends NvInputPacket {
|
||||
|
||||
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 +
|
||||
NvInputPacket.HEADER_LENGTH;
|
||||
|
||||
private short deltaX;
|
||||
private short deltaY;
|
||||
|
||||
public NvMouseMovePacket(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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user