Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59f7595050 | |||
| 9e20d25093 | |||
| 155432b4b8 | |||
| 0c614516fb | |||
| 1734d21b8e | |||
| 825233309d | |||
| 0f4e5a4585 | |||
| 9be149942a | |||
| 2ab14cdbf5 | |||
| b79fd8860c | |||
| 6f6eb1fb95 | |||
| c50ec21f39 | |||
| 3176ee72fe | |||
| cc37da9a56 | |||
| 87572bb083 | |||
| a74de39879 | |||
| 1d27309e53 | |||
| 3f00885d2c | |||
| ea3b1a7438 | |||
| 3dd57e9f38 | |||
| a639143e94 | |||
| b3c78ce1b1 | |||
| ddf9284ece | |||
| 438be4b8e5 | |||
| 8f43b95129 | |||
| faa2be431f | |||
| 16de523e78 | |||
| b24abc6ddd | |||
| a450cd5b01 | |||
| 2e3b7a2c09 | |||
| 22bba877d7 | |||
| 7142db3fac | |||
| 37cf572c0c | |||
| 33c5254d6f | |||
| faa82ca9d6 | |||
| 0d35ea5207 | |||
| 579645c07c | |||
| 869cbe2e81 | |||
| 329a938bf8 | |||
| 411931cc27 | |||
| ce01223683 | |||
| e7501a488d | |||
| 5626e9663b | |||
| 01b35ccdd3 | |||
| e83bc747c8 | |||
| cbe4af7623 | |||
| fc9e45270a | |||
| 94c1fc2b66 | |||
| 49999634c1 | |||
| 09f4827d02 | |||
| 52e4e81e35 | |||
| 56b752f63f | |||
| 2e6e835a8e | |||
| d8b0a0ffb5 | |||
| b82d74474a | |||
| 508b855e36 |
@@ -5,6 +5,5 @@
|
|||||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
<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.LIBRARIES"/>
|
||||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
<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"/>
|
<classpathentry kind="output" path="bin/classes"/>
|
||||||
</classpath>
|
</classpath>
|
||||||
|
|||||||
+43
-3
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.limelight"
|
package="com.limelight"
|
||||||
android:versionCode="13"
|
android:versionCode="20"
|
||||||
android:versionName="2.2" >
|
android:versionName="2.5" >
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
android:minSdkVersion="16"
|
android:minSdkVersion="16"
|
||||||
@@ -10,8 +10,11 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -19,13 +22,44 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.Connection"
|
android:name="com.limelight.PcView"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:label="@string/app_name" >
|
android:label="@string/app_name" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.limelight.AppView"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
|
android:label="App List" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.PcView" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.limelight.StreamSettings"
|
||||||
|
android:label="Streaming Settings" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.PcView" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.limelight.AdvancedSettings"
|
||||||
|
android:label="Advanced Settings" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.StreamSettings" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.limelight.AddComputerManually"
|
||||||
|
android:label="Add Computer Manually" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.StreamSettings" />
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.Game"
|
android:name="com.limelight.Game"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
@@ -37,6 +71,12 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.Connection" />
|
android:value="com.limelight.Connection" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name="com.limelight.discovery.DiscoveryService"
|
||||||
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
|
<service
|
||||||
|
android:name="com.limelight.computers.ComputerManagerService"
|
||||||
|
android:label="Computer Management Service" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"SupportedControllers" : {
|
|
||||||
"Gamepad" : {},
|
|
||||||
"Remote" : "false",
|
|
||||||
"SecondScreen" : {
|
|
||||||
"DPad" : "false",
|
|
||||||
"AnalogSticks" : "0",
|
|
||||||
"DigitalButtons" : "0",
|
|
||||||
"Mouse" : "false"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
This file serves to document some of the decoder errata when using MediaCodec hardware decoders on certain devices.
|
||||||
|
|
||||||
|
1. num_ref_frames is set to 16 by NVENC which causes decoders to allocate 16+ buffers. This can cause an OOM error on some devices.
|
||||||
|
- Affected decoders: TI OMAP4, possibly some Qualcomm chips too (Galaxy S3 on 4.3+)
|
||||||
|
|
||||||
|
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
|
||||||
|
- Affected decoders: NVIDIA Tegra 3 and 4
|
||||||
|
|
||||||
|
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
|
||||||
|
- Affected decoders: TI OMAP4
|
||||||
+31
-17
@@ -32,28 +32,42 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
|
|||||||
public static final int activity_vertical_margin=0x7f050001;
|
public static final int activity_vertical_margin=0x7f050001;
|
||||||
}
|
}
|
||||||
public static final class drawable {
|
public static final class drawable {
|
||||||
public static final int ic_launcher=0x7f020000;
|
public static final int app_icon=0x7f020000;
|
||||||
|
public static final int ic_launcher=0x7f020001;
|
||||||
|
public static final int list_view_unselected=0x7f020002;
|
||||||
|
public static final int ouya_icon=0x7f020003;
|
||||||
}
|
}
|
||||||
public static final class id {
|
public static final class id {
|
||||||
public static final int autoDec=0x7f080006;
|
public static final int addPc=0x7f080001;
|
||||||
public static final int bitrateLabel=0x7f08000c;
|
public static final int advancedSettingsButton=0x7f080012;
|
||||||
public static final int bitrateSeekBar=0x7f08000d;
|
public static final int appListText=0x7f080009;
|
||||||
public static final int config1080p30Selected=0x7f08000a;
|
public static final int autoDec=0x7f080004;
|
||||||
public static final int config1080p60Selected=0x7f08000b;
|
public static final int bitrateLabel=0x7f080006;
|
||||||
public static final int config720p30Selected=0x7f080008;
|
public static final int bitrateSeekBar=0x7f080007;
|
||||||
public static final int config720p60Selected=0x7f080009;
|
public static final int config1080p30Selected=0x7f080010;
|
||||||
public static final int decoderConfigGroup=0x7f080003;
|
public static final int config1080p60Selected=0x7f080011;
|
||||||
public static final int hardwareDec=0x7f080007;
|
public static final int config720p30Selected=0x7f08000e;
|
||||||
|
public static final int config720p60Selected=0x7f08000f;
|
||||||
|
public static final int decoderConfigGroup=0x7f080002;
|
||||||
|
public static final int discoveryText=0x7f08000b;
|
||||||
|
public static final int hardwareDec=0x7f080005;
|
||||||
public static final int hostTextView=0x7f080000;
|
public static final int hostTextView=0x7f080000;
|
||||||
public static final int pairButton=0x7f080002;
|
public static final int manuallyAddPc=0x7f080013;
|
||||||
public static final int softwareDec=0x7f080005;
|
public static final int pcListView=0x7f080008;
|
||||||
public static final int statusButton=0x7f080001;
|
public static final int rowTextView=0x7f080014;
|
||||||
public static final int streamConfigGroup=0x7f080004;
|
public static final int settingsButton=0x7f08000c;
|
||||||
public static final int surfaceView=0x7f08000e;
|
public static final int softwareDec=0x7f080003;
|
||||||
|
public static final int streamConfigGroup=0x7f08000d;
|
||||||
|
public static final int surfaceView=0x7f08000a;
|
||||||
}
|
}
|
||||||
public static final class layout {
|
public static final class layout {
|
||||||
public static final int activity_connection=0x7f030000;
|
public static final int activity_add_computer_manually=0x7f030000;
|
||||||
public static final int activity_game=0x7f030001;
|
public static final int activity_advanced_settings=0x7f030001;
|
||||||
|
public static final int activity_app_view=0x7f030002;
|
||||||
|
public static final int activity_game=0x7f030003;
|
||||||
|
public static final int activity_pc_view=0x7f030004;
|
||||||
|
public static final int activity_stream_settings=0x7f030005;
|
||||||
|
public static final int simplerow=0x7f030006;
|
||||||
}
|
}
|
||||||
public static final class string {
|
public static final class string {
|
||||||
public static final int app_name=0x7f060000;
|
public static final int app_name=0x7f060000;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<lint>
|
||||||
|
<issue id="IconDuplicates">
|
||||||
|
<ignore path="res/drawable/app_icon.png" />
|
||||||
|
</issue>
|
||||||
|
<issue id="IconLocation">
|
||||||
|
<ignore path="res/drawable/app_icon.png" />
|
||||||
|
</issue>
|
||||||
|
<issue id="InvalidPackage">
|
||||||
|
<ignore path="libs/bcprov-jdk15on-150.jar" />
|
||||||
|
</issue>
|
||||||
|
<issue id="UnusedResources">
|
||||||
|
<ignore path="res/drawable-xhdpi/ouya_icon.png" />
|
||||||
|
<ignore path="res/drawable/app_icon.png" />
|
||||||
|
</issue>
|
||||||
|
</lint>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle" >
|
||||||
|
|
||||||
|
<stroke android:width="1dip" android:color="#ffffff"/>
|
||||||
|
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
tools:context=".Connection" >
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/hostTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:ems="10"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:hint="IP address of GeForce PC" >
|
||||||
|
|
||||||
|
<requestFocus />
|
||||||
|
</EditText>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/addPc"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/hostTextView"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="Manually Add PC" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
tools:context=".Connection" >
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content" >
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/decoderConfigGroup"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/softwareDec"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Force Software Decoding" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/autoDec"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Auto-select Decoder (Recommended)" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/hardwareDec"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Force Hardware Decoding" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bitrateLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_below="@+id/decoderConfigGroup"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginTop="10dp" />
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/bitrateSeekBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_below="@+id/decoderConfigGroup"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_toLeftOf="@+id/bitrateLabel" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
|
tools:context=".AppView" >
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/pcListView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_below="@+id/appListText"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
android:longClickable="false"
|
||||||
|
android:background="@drawable/list_view_unselected"
|
||||||
|
android:stackFromBottom="false">
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/appListText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:text="Applications" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
tools:context=".Connection" >
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content" >
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/hostTextView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:ems="10"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:inputType="textNoSuggestions"
|
|
||||||
android:hint="IP address of GeForce PC" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/statusButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/hostTextView"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="Start Streaming Steam!" >
|
|
||||||
|
|
||||||
<requestFocus />
|
|
||||||
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/pairButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/statusButton"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="Pair with PC" />
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/decoderConfigGroup"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_below="@+id/streamConfigGroup"
|
|
||||||
android:layout_marginTop="15dp"
|
|
||||||
android:orientation="vertical" >
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/softwareDec"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Force Software Decoding" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/autoDec"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Auto-select Decoder (Recommended)" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/hardwareDec"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Force Hardware Decoding" />
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/streamConfigGroup"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_below="@+id/pairButton"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:orientation="vertical" >
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/config720p30Selected"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="720p 30 FPS (Only recommended for poor devices or networks)" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/config720p60Selected"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:text="720p 60 FPS (Recommended for most devices and networks)" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/config1080p30Selected"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:text="1080p 30 FPS (Recommended for most devices if 1080p streaming is desired)" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/config1080p60Selected"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:text="1080p 60 FPS (Requires extremely fast device and network)" />
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bitrateLabel"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="5dp"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_below="@+id/decoderConfigGroup" />
|
|
||||||
|
|
||||||
<SeekBar
|
|
||||||
android:id="@+id/bitrateSeekBar"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_below="@+id/decoderConfigGroup"
|
|
||||||
android:layout_toLeftOf="@+id/bitrateLabel" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
|
tools:context=".PcView" >
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/pcListView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_below="@+id/discoveryText"
|
||||||
|
android:background="@drawable/list_view_unselected"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
android:longClickable="false"
|
||||||
|
android:stackFromBottom="false" >
|
||||||
|
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/discoveryText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:layout_below="@+id/settingsButton"
|
||||||
|
android:paddingTop="20dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:text="Discovered PC List" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/settingsButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:text="Streaming Settings" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
tools:context=".Connection" >
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content" >
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/streamConfigGroup"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/config720p30Selected"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="720p 30 FPS (Only recommended for poor devices or networks)" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/config720p60Selected"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:text="720p 60 FPS (Recommended for most devices and networks)" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/config1080p30Selected"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:text="1080p 30 FPS (Recommended for most devices if 1080p streaming is desired)" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/config1080p60Selected"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:text="1080p 60 FPS (Requires extremely fast device and network)" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/advancedSettingsButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/streamConfigGroup"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:text="Advanced Settings" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/manuallyAddPc"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/advancedSettingsButton"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="Add PC Manually" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/rowTextView"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
android:textSize="16sp" >
|
||||||
|
</TextView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
public class AddComputerManually extends Activity {
|
||||||
|
private Button addPcButton;
|
||||||
|
private TextView hostText;
|
||||||
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String msg;
|
||||||
|
boolean finish = false;
|
||||||
|
try {
|
||||||
|
InetAddress addr = InetAddress.getByName(hostText.getText().toString());
|
||||||
|
|
||||||
|
if (!((ComputerManagerService.ComputerManagerBinder)binder).addComputerBlocking(addr)){
|
||||||
|
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = "Successfully added computer";
|
||||||
|
finish = true;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean toastFinish = finish;
|
||||||
|
final String toastMsg = msg;
|
||||||
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Unbind from this service
|
||||||
|
unbindService(AddComputerManually.this.serviceConnection);
|
||||||
|
|
||||||
|
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
if (toastFinish) {
|
||||||
|
// Close the activity
|
||||||
|
AddComputerManually.this.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
|
this.addPcButton = (Button) findViewById(R.id.addPc);
|
||||||
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
|
|
||||||
|
addPcButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (hostText.getText().length() == 0) {
|
||||||
|
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
// Bind to the service which will try to add the PC
|
||||||
|
bindService(new Intent(AddComputerManually.this, ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.SeekBar.OnSeekBarChangeListener;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||||
|
|
||||||
|
public class AdvancedSettings extends Activity {
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
private RadioButton forceSoftDec, autoDec, forceHardDec;
|
||||||
|
private SeekBar bitrateSlider;
|
||||||
|
private TextView bitrateLabel;
|
||||||
|
|
||||||
|
private static final int BITRATE_FLOOR = 1;
|
||||||
|
private static final int BITRATE_CEILING = 100;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
||||||
|
editor.putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress());
|
||||||
|
editor.apply();
|
||||||
|
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_advanced_settings);
|
||||||
|
|
||||||
|
this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
|
||||||
|
this.autoDec = (RadioButton) findViewById(R.id.autoDec);
|
||||||
|
this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
|
||||||
|
this.bitrateLabel = (TextView) findViewById(R.id.bitrateLabel);
|
||||||
|
this.bitrateSlider = (SeekBar) findViewById(R.id.bitrateSeekBar);
|
||||||
|
|
||||||
|
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
|
||||||
|
|
||||||
|
bitrateSlider.setMax(BITRATE_CEILING);
|
||||||
|
bitrateSlider.setProgress(prefs.getInt(Game.BITRATE_PREF_STRING, Game.DEFAULT_BITRATE));
|
||||||
|
updateBitrateLabel();
|
||||||
|
|
||||||
|
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
|
||||||
|
case Game.FORCE_SOFTWARE_DECODER:
|
||||||
|
forceSoftDec.setChecked(true);
|
||||||
|
autoDec.setChecked(false);
|
||||||
|
forceHardDec.setChecked(false);
|
||||||
|
break;
|
||||||
|
case Game.AUTOSELECT_DECODER:
|
||||||
|
forceSoftDec.setChecked(false);
|
||||||
|
autoDec.setChecked(true);
|
||||||
|
forceHardDec.setChecked(false);
|
||||||
|
break;
|
||||||
|
case Game.FORCE_HARDWARE_DECODER:
|
||||||
|
forceSoftDec.setChecked(false);
|
||||||
|
autoDec.setChecked(false);
|
||||||
|
forceHardDec.setChecked(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnCheckedChangeListener occl = new OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView,
|
||||||
|
boolean isChecked) {
|
||||||
|
if (!isChecked) {
|
||||||
|
// Ignore non-checked buttons
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonView == forceSoftDec) {
|
||||||
|
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
|
||||||
|
}
|
||||||
|
else if (buttonView == forceHardDec) {
|
||||||
|
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_HARDWARE_DECODER).commit();
|
||||||
|
}
|
||||||
|
else if (buttonView == autoDec) {
|
||||||
|
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.AUTOSELECT_DECODER).commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
forceSoftDec.setOnCheckedChangeListener(occl);
|
||||||
|
forceHardDec.setOnCheckedChangeListener(occl);
|
||||||
|
autoDec.setOnCheckedChangeListener(occl);
|
||||||
|
|
||||||
|
this.bitrateSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress,
|
||||||
|
boolean fromUser) {
|
||||||
|
|
||||||
|
// Verify the user's selection
|
||||||
|
if (fromUser) {
|
||||||
|
if (progress < BITRATE_FLOOR) {
|
||||||
|
seekBar.setProgress(BITRATE_FLOOR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBitrateLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBitrateLabel() {
|
||||||
|
bitrateLabel.setText("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ContextMenu;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
public class AppView extends Activity {
|
||||||
|
private ListView appList;
|
||||||
|
private ArrayAdapter<AppObject> appListAdapter;
|
||||||
|
private InetAddress ipAddress;
|
||||||
|
private String uniqueId;
|
||||||
|
private boolean remote;
|
||||||
|
|
||||||
|
private final static int RESUME_ID = 1;
|
||||||
|
private final static int QUIT_ID = 2;
|
||||||
|
|
||||||
|
public final static String ADDRESS_EXTRA = "Address";
|
||||||
|
public final static String UNIQUEID_EXTRA = "UniqueId";
|
||||||
|
public final static String NAME_EXTRA = "Name";
|
||||||
|
public final static String REMOTE_EXTRA = "Remote";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
|
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
||||||
|
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
||||||
|
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
||||||
|
if (address == null || uniqueId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle("App List for "+getIntent().getStringExtra(NAME_EXTRA));
|
||||||
|
|
||||||
|
try {
|
||||||
|
ipAddress = InetAddress.getByAddress(address);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the list view
|
||||||
|
appList = (ListView)findViewById(R.id.pcListView);
|
||||||
|
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
|
||||||
|
appListAdapter.setNotifyOnChange(false);
|
||||||
|
appList.setAdapter(appListAdapter);
|
||||||
|
appList.setItemsCanFocus(true);
|
||||||
|
appList.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
AppObject app = (AppObject) appListAdapter.getItem(pos);
|
||||||
|
if (app == null || app.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only open the context menu if it's running, otherwise start it
|
||||||
|
if (app.app.getIsRunning()) {
|
||||||
|
openContextMenu(arg1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
doStart(app.app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(appList);
|
||||||
|
updateAppList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
SpinnerDialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
updateAppList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
|
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
|
||||||
|
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
|
AppObject app = (AppObject) appListAdapter.getItem(info.position);
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case RESUME_ID:
|
||||||
|
// Resume is the same as start for us
|
||||||
|
doStart(app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case QUIT_ID:
|
||||||
|
doQuit(app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateString(NvApp app) {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append(app.getAppName());
|
||||||
|
if (app.getIsRunning()) {
|
||||||
|
str.append(" - Running");
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addListPlaceholder() {
|
||||||
|
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAppList() {
|
||||||
|
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<NvApp> appList = httpConn.getAppList();
|
||||||
|
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
appListAdapter.clear();
|
||||||
|
if (appList.isEmpty()) {
|
||||||
|
addListPlaceholder();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
appListAdapter.add(new AppObject(generateString(app), app));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appListAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Success case
|
||||||
|
return;
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
} catch (IOException e) {
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
} finally {
|
||||||
|
spinner.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doStart(NvApp app) {
|
||||||
|
Intent intent = new Intent(this, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doQuit(final NvApp app) {
|
||||||
|
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = "Successfully quit "+app.getAppName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "Failed to quit "+app.getAppName();
|
||||||
|
}
|
||||||
|
updateAppList();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = "Failed to resolve host";
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||||
|
+ "Try rebooting your machine or reinstalling GFE.";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppObject {
|
||||||
|
public String text;
|
||||||
|
public NvApp app;
|
||||||
|
|
||||||
|
public AppObject(String text, NvApp app) {
|
||||||
|
this.text = text;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.SeekBar;
|
|
||||||
import android.widget.SeekBar.OnSeekBarChangeListener;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
public class Connection extends Activity {
|
|
||||||
private Button statusButton, pairButton;
|
|
||||||
private TextView hostText;
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
|
|
||||||
private RadioButton forceSoftDec, autoDec, forceHardDec;
|
|
||||||
private SeekBar bitrateSlider;
|
|
||||||
private TextView bitrateLabel;
|
|
||||||
|
|
||||||
private static final String DEFAULT_HOST = "";
|
|
||||||
public static final String HOST_KEY = "hostText";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
|
|
||||||
editor.putString(Connection.HOST_KEY, this.hostText.getText().toString());
|
|
||||||
editor.putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress());
|
|
||||||
editor.apply();
|
|
||||||
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_connection);
|
|
||||||
|
|
||||||
// Hide the keyboard by default
|
|
||||||
this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
|
||||||
|
|
||||||
this.statusButton = (Button) findViewById(R.id.statusButton);
|
|
||||||
this.pairButton = (Button) findViewById(R.id.pairButton);
|
|
||||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
|
||||||
this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected);
|
|
||||||
this.rbutton720p60 = (RadioButton) findViewById(R.id.config720p60Selected);
|
|
||||||
this.rbutton1080p30 = (RadioButton) findViewById(R.id.config1080p30Selected);
|
|
||||||
this.rbutton1080p60 = (RadioButton) findViewById(R.id.config1080p60Selected);
|
|
||||||
this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
|
|
||||||
this.autoDec = (RadioButton) findViewById(R.id.autoDec);
|
|
||||||
this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
|
|
||||||
this.bitrateLabel = (TextView) findViewById(R.id.bitrateLabel);
|
|
||||||
this.bitrateSlider = (SeekBar) findViewById(R.id.bitrateSeekBar);
|
|
||||||
|
|
||||||
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
|
|
||||||
this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST));
|
|
||||||
|
|
||||||
boolean res720p = prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720;
|
|
||||||
boolean fps30 = prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30;
|
|
||||||
|
|
||||||
bitrateSlider.setMax(Game.BITRATE_CEILING);
|
|
||||||
bitrateSlider.setProgress(prefs.getInt(Game.BITRATE_PREF_STRING, Game.DEFAULT_BITRATE));
|
|
||||||
updateBitrateLabel();
|
|
||||||
|
|
||||||
rbutton720p30.setChecked(false);
|
|
||||||
rbutton720p60.setChecked(false);
|
|
||||||
rbutton1080p30.setChecked(false);
|
|
||||||
rbutton1080p60.setChecked(false);
|
|
||||||
if (res720p) {
|
|
||||||
if (fps30) {
|
|
||||||
rbutton720p30.setChecked(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rbutton720p60.setChecked(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (fps30) {
|
|
||||||
rbutton1080p30.setChecked(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rbutton1080p60.setChecked(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
|
|
||||||
case Game.FORCE_SOFTWARE_DECODER:
|
|
||||||
forceSoftDec.setChecked(true);
|
|
||||||
autoDec.setChecked(false);
|
|
||||||
forceHardDec.setChecked(false);
|
|
||||||
break;
|
|
||||||
case Game.AUTOSELECT_DECODER:
|
|
||||||
forceSoftDec.setChecked(false);
|
|
||||||
autoDec.setChecked(true);
|
|
||||||
forceHardDec.setChecked(false);
|
|
||||||
break;
|
|
||||||
case Game.FORCE_HARDWARE_DECODER:
|
|
||||||
forceSoftDec.setChecked(false);
|
|
||||||
autoDec.setChecked(false);
|
|
||||||
forceHardDec.setChecked(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnCheckedChangeListener occl = new OnCheckedChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onCheckedChanged(CompoundButton buttonView,
|
|
||||||
boolean isChecked) {
|
|
||||||
if (!isChecked) {
|
|
||||||
// Ignore non-checked buttons
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttonView == rbutton720p30) {
|
|
||||||
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
|
|
||||||
putInt(Game.HEIGHT_PREF_STRING, 720).
|
|
||||||
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
|
|
||||||
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_30).commit();
|
|
||||||
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_720_30);
|
|
||||||
}
|
|
||||||
else if (buttonView == rbutton720p60) {
|
|
||||||
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
|
|
||||||
putInt(Game.HEIGHT_PREF_STRING, 720).
|
|
||||||
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
|
|
||||||
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_60).commit();
|
|
||||||
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_720_60);
|
|
||||||
}
|
|
||||||
else if (buttonView == rbutton1080p30) {
|
|
||||||
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
|
|
||||||
putInt(Game.HEIGHT_PREF_STRING, 1080).
|
|
||||||
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
|
|
||||||
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_30).commit();
|
|
||||||
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_1080_30);
|
|
||||||
}
|
|
||||||
else if (buttonView == rbutton1080p60) {
|
|
||||||
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
|
|
||||||
putInt(Game.HEIGHT_PREF_STRING, 1080).
|
|
||||||
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
|
|
||||||
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_60).commit();
|
|
||||||
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_1080_60);
|
|
||||||
}
|
|
||||||
else if (buttonView == forceSoftDec) {
|
|
||||||
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
|
|
||||||
}
|
|
||||||
else if (buttonView == forceHardDec) {
|
|
||||||
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_HARDWARE_DECODER).commit();
|
|
||||||
}
|
|
||||||
else if (buttonView == autoDec) {
|
|
||||||
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.AUTOSELECT_DECODER).commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
rbutton720p30.setOnCheckedChangeListener(occl);
|
|
||||||
rbutton720p60.setOnCheckedChangeListener(occl);
|
|
||||||
rbutton1080p30.setOnCheckedChangeListener(occl);
|
|
||||||
rbutton1080p60.setOnCheckedChangeListener(occl);
|
|
||||||
forceSoftDec.setOnCheckedChangeListener(occl);
|
|
||||||
forceHardDec.setOnCheckedChangeListener(occl);
|
|
||||||
autoDec.setOnCheckedChangeListener(occl);
|
|
||||||
|
|
||||||
this.bitrateSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onProgressChanged(SeekBar seekBar, int progress,
|
|
||||||
boolean fromUser) {
|
|
||||||
|
|
||||||
// Verify the user's selection
|
|
||||||
if (fromUser) {
|
|
||||||
int floor;
|
|
||||||
if (rbutton720p30.isChecked()) {
|
|
||||||
floor = Game.BITRATE_FLOOR_720_30;
|
|
||||||
}
|
|
||||||
else if (rbutton720p60.isChecked()){
|
|
||||||
floor = Game.BITRATE_FLOOR_720_60;
|
|
||||||
}
|
|
||||||
else if (rbutton1080p30.isChecked()){
|
|
||||||
floor = Game.BITRATE_FLOOR_1080_30;
|
|
||||||
}
|
|
||||||
else /*if (rbutton1080p60.isChecked())*/ {
|
|
||||||
floor = Game.BITRATE_FLOOR_1080_60;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress < floor) {
|
|
||||||
seekBar.setProgress(floor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBitrateLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.statusButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View arg0) {
|
|
||||||
if (Connection.this.hostText.getText().length() == 0) {
|
|
||||||
Toast.makeText(Connection.this, "Please enter the target PC's IP address in the text box at the top of the screen.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(Connection.this, Game.class);
|
|
||||||
intent.putExtra("host", Connection.this.hostText.getText().toString());
|
|
||||||
Connection.this.startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pairButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View arg0) {
|
|
||||||
if (Connection.this.hostText.getText().length() == 0) {
|
|
||||||
Toast.makeText(Connection.this, "Please enter the target PC's IP address in the text box at the top of the screen.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_LONG).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
String macAddress;
|
|
||||||
try {
|
|
||||||
macAddress = NvConnection.getMacAddressString();
|
|
||||||
} catch (SocketException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (macAddress == null) {
|
|
||||||
LimeLog.severe("Couldn't find a MAC address");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
|
|
||||||
macAddress, PlatformBinding.getDeviceName());
|
|
||||||
try {
|
|
||||||
if (httpConn.getPairState()) {
|
|
||||||
message = "Already paired";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
int session = httpConn.getSessionId();
|
|
||||||
if (session == 0) {
|
|
||||||
message = "Pairing was declined by the target";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = "Pairing was successful";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e1) {
|
|
||||||
message = "Failed to resolve host";
|
|
||||||
}
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(Connection.this, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateBitrateLabel() {
|
|
||||||
bitrateLabel.setText(bitrateSlider.getProgress()+" Mbps");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+90
-21
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
@@ -19,7 +20,9 @@ import android.content.SharedPreferences;
|
|||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -55,8 +58,17 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
|
||||||
|
private ConfigurableDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
|
private WifiManager.WifiLock wifiLock;
|
||||||
|
|
||||||
private int drFlags = 0;
|
private int drFlags = 0;
|
||||||
|
|
||||||
|
public static final String EXTRA_HOST = "Host";
|
||||||
|
public static final String EXTRA_APP = "App";
|
||||||
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
|
||||||
public static final String PREFS_FILE_NAME = "gameprefs";
|
public static final String PREFS_FILE_NAME = "gameprefs";
|
||||||
|
|
||||||
public static final String WIDTH_PREF_STRING = "ResH";
|
public static final String WIDTH_PREF_STRING = "ResH";
|
||||||
@@ -65,18 +77,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
public static final String DECODER_PREF_STRING = "Decoder";
|
public static final String DECODER_PREF_STRING = "Decoder";
|
||||||
public static final String BITRATE_PREF_STRING = "Bitrate";
|
public static final String BITRATE_PREF_STRING = "Bitrate";
|
||||||
|
|
||||||
public static final int BITRATE_FLOOR_720_30 = 4;
|
public static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
public static final int BITRATE_FLOOR_720_60 = 8;
|
|
||||||
public static final int BITRATE_FLOOR_1080_30 = 10;
|
|
||||||
public static final int BITRATE_FLOOR_1080_60 = 20;
|
|
||||||
|
|
||||||
public static final int BITRATE_DEFAULT_720_30 = 7;
|
|
||||||
public static final int BITRATE_DEFAULT_720_60 = 10;
|
public static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
public static final int BITRATE_DEFAULT_1080_30 = 16;
|
public static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||||
public static final int BITRATE_DEFAULT_1080_60 = 30;
|
public static final int BITRATE_DEFAULT_1080_60 = 30;
|
||||||
|
|
||||||
public static final int BITRATE_CEILING = 50;
|
|
||||||
|
|
||||||
public static final int DEFAULT_WIDTH = 1280;
|
public static final int DEFAULT_WIDTH = 1280;
|
||||||
public static final int DEFAULT_HEIGHT = 720;
|
public static final int DEFAULT_HEIGHT = 720;
|
||||||
public static final int DEFAULT_REFRESH_RATE = 60;
|
public static final int DEFAULT_REFRESH_RATE = 60;
|
||||||
@@ -91,15 +96,24 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// We don't want a title bar
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
// Full-screen and don't let the display go off
|
// Full-screen and don't let the display go off
|
||||||
getWindow().setFlags(
|
getWindow().addFlags(
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
|
||||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
// We don't want a title bar
|
// If we're going to use immersive mode, we want to have
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
// the entire screen
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||||
|
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
||||||
|
}
|
||||||
|
|
||||||
// Change volume button behavior
|
// Change volume button behavior
|
||||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||||
@@ -142,12 +156,26 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
|
|
||||||
|
// Make sure Wi-Fi is fully powered up
|
||||||
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
|
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
||||||
|
wifiLock.setReferenceCounted(false);
|
||||||
|
wifiLock.acquire();
|
||||||
|
|
||||||
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
|
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
||||||
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
|
boolean enableLargePackets = !Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, true);
|
||||||
|
LimeLog.info("Using large packets? "+enableLargePackets);
|
||||||
|
|
||||||
// Start the connection
|
// Start the connection
|
||||||
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
|
conn = new NvConnection(host, uniqueId, Game.this,
|
||||||
new StreamConfiguration(width, height, refreshRate, bitrate * 1000));
|
new StreamConfiguration(app, width, height, refreshRate, bitrate * 1000,
|
||||||
|
enableLargePackets ? 1460 : 1024), PlatformBinding.getCryptoProvider(this));
|
||||||
keybTranslator = new KeyboardTranslator(conn);
|
keybTranslator = new KeyboardTranslator(conn);
|
||||||
controllerHandler = new ControllerHandler(conn);
|
controllerHandler = new ControllerHandler(conn);
|
||||||
|
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||||
|
|
||||||
// The connection will be started when the surface gets created
|
// The connection will be started when the surface gets created
|
||||||
sh.addCallback(this);
|
sh.addCallback(this);
|
||||||
@@ -162,8 +190,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private void hideSystemUi() {
|
private Runnable hideSystemUi = new Runnable() {
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
||||||
@@ -182,7 +209,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
View.SYSTEM_UI_FLAG_LOW_PROFILE);
|
View.SYSTEM_UI_FLAG_LOW_PROFILE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
private void hideSystemUi() {
|
||||||
|
Handler h = getWindow().getDecorView().getHandler();
|
||||||
|
if (h != null) {
|
||||||
|
h.removeCallbacks(hideSystemUi);
|
||||||
|
h.postDelayed(hideSystemUi, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -195,9 +229,33 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
conn.stop();
|
conn.stop();
|
||||||
|
|
||||||
|
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||||
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
|
String message = null;
|
||||||
|
if (averageEndToEndLat > 0) {
|
||||||
|
message = "Average total frame latency: "+averageEndToEndLat+" ms";
|
||||||
|
if (averageDecoderLat > 0) {
|
||||||
|
message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (averageDecoderLat > 0) {
|
||||||
|
message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
wifiLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
private static byte getModifierState(KeyEvent event) {
|
private static byte getModifierState(KeyEvent event) {
|
||||||
byte modifier = 0;
|
byte modifier = 0;
|
||||||
if (event.isShiftPressed()) {
|
if (event.isShiftPressed()) {
|
||||||
@@ -235,6 +293,16 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
|
// Pressing a volume button drops the immersive flag so the UI shows up again and doesn't
|
||||||
|
// go away. I'm not sure if that's a bug or a feature, but we're working around it here
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
|
Handler h = getWindow().getDecorView().getHandler();
|
||||||
|
if (h != null) {
|
||||||
|
h.removeCallbacks(hideSystemUi);
|
||||||
|
h.postDelayed(hideSystemUi, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.getDevice() != null &&
|
if (event.getDevice() != null &&
|
||||||
(event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) {
|
(event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) {
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
@@ -413,6 +481,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
return onGenericMotionEvent(event);
|
return onGenericMotionEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
// Send it to the activity's touch event handler
|
// Send it to the activity's touch event handler
|
||||||
@@ -494,7 +563,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
if (!connected && !connecting) {
|
if (!connected && !connecting) {
|
||||||
connecting = true;
|
connecting = true;
|
||||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||||
PlatformBinding.getAudioRenderer(), new ConfigurableDecoderRenderer());
|
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,545 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.ContextMenu;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
public class PcView extends Activity {
|
||||||
|
private Button settingsButton;
|
||||||
|
private ListView pcList;
|
||||||
|
private ArrayAdapter<ComputerObject> pcListAdapter;
|
||||||
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private boolean freezeUpdates, runningPolling;
|
||||||
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
|
|
||||||
|
// Wait in a separate thread to avoid stalling the UI
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Wait for the binder to be ready
|
||||||
|
localBinder.waitForReady();
|
||||||
|
|
||||||
|
// Now make the binder visible
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
|
// Start updates
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int APP_LIST_ID = 1;
|
||||||
|
private final static int PAIR_ID = 2;
|
||||||
|
private final static int UNPAIR_ID = 3;
|
||||||
|
private final static int WOL_ID = 4;
|
||||||
|
private final static int DELETE_ID = 5;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_pc_view);
|
||||||
|
|
||||||
|
// Bind to the computer manager service
|
||||||
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
// Setup the list view
|
||||||
|
settingsButton = (Button)findViewById(R.id.settingsButton);
|
||||||
|
pcList = (ListView)findViewById(R.id.pcListView);
|
||||||
|
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
|
||||||
|
pcListAdapter.setNotifyOnChange(false);
|
||||||
|
pcList.setAdapter(pcListAdapter);
|
||||||
|
pcList.setItemsCanFocus(true);
|
||||||
|
pcList.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
|
||||||
|
if (computer.details == null) {
|
||||||
|
// Placeholder item; no context menu for it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Open the context menu if a PC is offline
|
||||||
|
openContextMenu(arg1);
|
||||||
|
}
|
||||||
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
// Pair an unpaired machine by default
|
||||||
|
doPair(computer.details);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
doAppList(computer.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(pcList);
|
||||||
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
startActivity(new Intent(PcView.this, StreamSettings.class));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addListPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startComputerUpdates() {
|
||||||
|
if (managerBinder != null) {
|
||||||
|
if (runningPolling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
freezeUpdates = false;
|
||||||
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||||
|
if (!freezeUpdates) {
|
||||||
|
PcView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateListView(details);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
runningPolling = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopComputerUpdates() {
|
||||||
|
freezeUpdates = true;
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
runningPolling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
stopComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
stopComputerUpdates();
|
||||||
|
|
||||||
|
// Call superclass
|
||||||
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
|
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
|
||||||
|
if (computer == null || computer.details == null) {
|
||||||
|
startComputerUpdates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inflate the context menu
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
||||||
|
}
|
||||||
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
|
||||||
|
menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doPair(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (computer.runningGameId != 0) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is currently in a game. " +
|
||||||
|
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
InetAddress addr = null;
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
addr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConn = new NvHTTP(addr,
|
||||||
|
managerBinder.getUniqueId(),
|
||||||
|
PlatformBinding.getDeviceName(),
|
||||||
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
|
message = "Already paired";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final String pinStr = PairingManager.generatePinString();
|
||||||
|
|
||||||
|
// Spin the dialog off in a thread because it blocks
|
||||||
|
Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
|
||||||
|
|
||||||
|
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||||
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
|
message = "Incorrect PIN";
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.FAILED) {
|
||||||
|
message = "Pairing failed";
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
|
message = "Paired successfully";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Should be no other values
|
||||||
|
message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = "Failed to resolve host";
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||||
|
+ "Try rebooting your machine or reinstalling GFE.";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doWakeOnLan(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
WakeOnLanSender.sendWolPacket(computer);
|
||||||
|
message = "It may take a few seconds for your PC to wake up. " +
|
||||||
|
"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
|
||||||
|
} catch (IOException e) {
|
||||||
|
message = "Failed to send Wake-On-LAN packets";
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doUnpair(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
InetAddress addr = null;
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
addr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConn = new NvHTTP(addr,
|
||||||
|
managerBinder.getUniqueId(),
|
||||||
|
PlatformBinding.getDeviceName(),
|
||||||
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
|
httpConn.unpair();
|
||||||
|
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
||||||
|
message = "Unpaired successfully";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "Failed to unpair";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "Device was not paired";
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = "Failed to resolve host";
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||||
|
+ "Try rebooting your machine or reinstalling GFE.";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doAppList(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent i = new Intent(this, AppView.class);
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
|
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
|
||||||
|
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
|
||||||
|
i.putExtra(AppView.REMOTE_EXTRA, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
|
||||||
|
i.putExtra(AppView.REMOTE_EXTRA, true);
|
||||||
|
}
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
|
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case PAIR_ID:
|
||||||
|
doPair(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case UNPAIR_ID:
|
||||||
|
doUnpair(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case WOL_ID:
|
||||||
|
doWakeOnLan(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case DELETE_ID:
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.removeComputer(computer.details.name);
|
||||||
|
}
|
||||||
|
removeListView(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case APP_LIST_ID:
|
||||||
|
doAppList(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateString(ComputerDetails details) {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append(details.name).append(" - ");
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
str.append("Online ");
|
||||||
|
if (details.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
str.append("(Local) - ");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("(Remote) - ");
|
||||||
|
}
|
||||||
|
if (details.pairState == PairState.PAIRED) {
|
||||||
|
if (details.runningGameId == 0) {
|
||||||
|
str.append("Available");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("In Game");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("Not Paired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("Offline");
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addListPlaceholder() {
|
||||||
|
pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
|
||||||
|
"If your PC doesn't show up in about 15 seconds, " +
|
||||||
|
"make sure your computer is running GFE or add your PC manually on the settings page.", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeListView(ComputerDetails details) {
|
||||||
|
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(i);
|
||||||
|
|
||||||
|
if (details.equals(computer.details)) {
|
||||||
|
pcListAdapter.remove(computer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pcListAdapter.getCount() == 0) {
|
||||||
|
// Add the placeholder if we're down to 0 computers
|
||||||
|
addListPlaceholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateListView(ComputerDetails details) {
|
||||||
|
String computerString = generateString(details);
|
||||||
|
ComputerObject existingEntry = null;
|
||||||
|
boolean placeholderPresent = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(i);
|
||||||
|
|
||||||
|
// If there's a placeholder, there's nothing else
|
||||||
|
if (computer.details == null) {
|
||||||
|
placeholderPresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is the same computer
|
||||||
|
if (details.equals(computer.details)) {
|
||||||
|
existingEntry = computer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry != null) {
|
||||||
|
// Replace the information in the existing entry
|
||||||
|
existingEntry.text = computerString;
|
||||||
|
existingEntry.details = details;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If the placeholder is the only object, remove it
|
||||||
|
if (placeholderPresent) {
|
||||||
|
pcListAdapter.remove(pcListAdapter.getItem(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new entry
|
||||||
|
pcListAdapter.add(new ComputerObject(computerString, details));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the view that the data has changed
|
||||||
|
pcListAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ComputerObject {
|
||||||
|
public String text;
|
||||||
|
public ComputerDetails details;
|
||||||
|
|
||||||
|
public ComputerObject(String text, ComputerDetails details) {
|
||||||
|
this.text = text;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
public class StreamSettings extends Activity {
|
||||||
|
private Button advancedSettingsButton, addComputerButton;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_stream_settings);
|
||||||
|
|
||||||
|
this.advancedSettingsButton = (Button) findViewById(R.id.advancedSettingsButton);
|
||||||
|
this.addComputerButton = (Button) findViewById(R.id.manuallyAddPc);
|
||||||
|
this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected);
|
||||||
|
this.rbutton720p60 = (RadioButton) findViewById(R.id.config720p60Selected);
|
||||||
|
this.rbutton1080p30 = (RadioButton) findViewById(R.id.config1080p30Selected);
|
||||||
|
this.rbutton1080p60 = (RadioButton) findViewById(R.id.config1080p60Selected);
|
||||||
|
|
||||||
|
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
|
||||||
|
|
||||||
|
boolean res720p = prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720;
|
||||||
|
boolean fps30 = prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30;
|
||||||
|
|
||||||
|
rbutton720p30.setChecked(false);
|
||||||
|
rbutton720p60.setChecked(false);
|
||||||
|
rbutton1080p30.setChecked(false);
|
||||||
|
rbutton1080p60.setChecked(false);
|
||||||
|
if (res720p) {
|
||||||
|
if (fps30) {
|
||||||
|
rbutton720p30.setChecked(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rbutton720p60.setChecked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (fps30) {
|
||||||
|
rbutton1080p30.setChecked(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rbutton1080p60.setChecked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnCheckedChangeListener occl = new OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView,
|
||||||
|
boolean isChecked) {
|
||||||
|
if (!isChecked) {
|
||||||
|
// Ignore non-checked buttons
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonView == rbutton720p30) {
|
||||||
|
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
|
||||||
|
putInt(Game.HEIGHT_PREF_STRING, 720).
|
||||||
|
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
|
||||||
|
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_30).commit();
|
||||||
|
}
|
||||||
|
else if (buttonView == rbutton720p60) {
|
||||||
|
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
|
||||||
|
putInt(Game.HEIGHT_PREF_STRING, 720).
|
||||||
|
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
|
||||||
|
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_60).commit();
|
||||||
|
}
|
||||||
|
else if (buttonView == rbutton1080p30) {
|
||||||
|
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
|
||||||
|
putInt(Game.HEIGHT_PREF_STRING, 1080).
|
||||||
|
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
|
||||||
|
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_30).commit();
|
||||||
|
}
|
||||||
|
else if (buttonView == rbutton1080p60) {
|
||||||
|
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
|
||||||
|
putInt(Game.HEIGHT_PREF_STRING, 1080).
|
||||||
|
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
|
||||||
|
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_60).commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rbutton720p30.setOnCheckedChangeListener(occl);
|
||||||
|
rbutton720p60.setOnCheckedChangeListener(occl);
|
||||||
|
rbutton1080p30.setOnCheckedChangeListener(occl);
|
||||||
|
rbutton1080p60.setOnCheckedChangeListener(occl);
|
||||||
|
|
||||||
|
advancedSettingsButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent i = new Intent(StreamSettings.this, AdvancedSettings.class);
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addComputerButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent i = new Intent(StreamSettings.this, AddComputerManually.class);
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.limelight.binding;
|
package com.limelight.binding;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||||
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
public class PlatformBinding {
|
public class PlatformBinding {
|
||||||
public static String getDeviceName() {
|
public static String getDeviceName() {
|
||||||
@@ -13,4 +17,8 @@ public class PlatformBinding {
|
|||||||
public static AudioRenderer getAudioRenderer() {
|
public static AudioRenderer getAudioRenderer() {
|
||||||
return new AndroidAudioRenderer();
|
return new AndroidAudioRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||||
|
return new AndroidCryptoProvider(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void streamInitialized(int channelCount, int sampleRate) {
|
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
int bufferSize;
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Decoder returned unhandled channel count");
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
@@ -47,6 +48,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
AudioTrack.MODE_STREAM);
|
AudioTrack.MODE_STREAM);
|
||||||
|
|
||||||
track.play();
|
track.play();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package com.limelight.binding.crypto;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.PEMWriter;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||||
|
|
||||||
|
private File certFile;
|
||||||
|
private File keyFile;
|
||||||
|
|
||||||
|
private X509Certificate cert;
|
||||||
|
private RSAPrivateKey key;
|
||||||
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Install the Bouncy Castle provider
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidCryptoProvider(Context c) {
|
||||||
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
|
|
||||||
|
certFile = new File(dataPath + File.separator + "client.crt");
|
||||||
|
keyFile = new File(dataPath + File.separator + "client.key");
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] loadFileToBytes(File f) {
|
||||||
|
if (!f.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream fin = new FileInputStream(f);
|
||||||
|
byte[] fileData = new byte[(int) f.length()];
|
||||||
|
fin.read(fileData);
|
||||||
|
fin.close();
|
||||||
|
return fileData;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean loadCertKeyPair() {
|
||||||
|
byte[] certBytes = loadFileToBytes(certFile);
|
||||||
|
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||||
|
|
||||||
|
// If either file was missing, we definitely can't succeed
|
||||||
|
if (certBytes == null || keyBytes == null) {
|
||||||
|
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||||
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
pemCertBytes = certBytes;
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||||
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// May happen if the cert is corrupt
|
||||||
|
LimeLog.warning("Corrupted certificate");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
// May happen if the key is corrupt
|
||||||
|
LimeLog.warning("Corrupted key");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("TrulyRandom")
|
||||||
|
private boolean generateCertKeyPair() {
|
||||||
|
byte[] snBytes = new byte[8];
|
||||||
|
new SecureRandom().nextBytes(snBytes);
|
||||||
|
|
||||||
|
KeyPair keyPair;
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
|
// Should never happen
|
||||||
|
e1.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// Expires in 20 years
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(now);
|
||||||
|
calendar.add(Calendar.YEAR, 20);
|
||||||
|
Date expirationDate = calendar.getTime();
|
||||||
|
|
||||||
|
BigInteger serial = new BigInteger(snBytes).abs();
|
||||||
|
|
||||||
|
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||||
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||||
|
X500Name name = nameBuilder.build();
|
||||||
|
|
||||||
|
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(name, serial, now, expirationDate, name, keyPair.getPublic());
|
||||||
|
|
||||||
|
try {
|
||||||
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||||
|
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||||
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Nothing should go wrong here
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|
||||||
|
// Save the resulting pair
|
||||||
|
saveCertKeyPair();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCertKeyPair() {
|
||||||
|
try {
|
||||||
|
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||||
|
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||||
|
|
||||||
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
|
StringWriter strWriter = new StringWriter();
|
||||||
|
PEMWriter pemWriter = new PEMWriter(strWriter);
|
||||||
|
pemWriter.writeObject(cert);
|
||||||
|
pemWriter.close();
|
||||||
|
|
||||||
|
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||||
|
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||||
|
String pemStr = strWriter.getBuffer().toString();
|
||||||
|
for (int i = 0; i < pemStr.length(); i++) {
|
||||||
|
char c = pemStr.charAt(i);
|
||||||
|
if (c != '\r')
|
||||||
|
certWriter.append(c);
|
||||||
|
}
|
||||||
|
certWriter.close();
|
||||||
|
|
||||||
|
// Write the private out in PKCS8 format
|
||||||
|
keyOut.write(key.getEncoded());
|
||||||
|
|
||||||
|
certOut.close();
|
||||||
|
keyOut.close();
|
||||||
|
|
||||||
|
LimeLog.info("Saved generated key pair to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This isn't good because it means we'll have
|
||||||
|
// to re-pair next time
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getClientCertificate() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (this) {
|
||||||
|
// Return a loaded cert if we have one
|
||||||
|
if (cert != null) {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded cert yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (this) {
|
||||||
|
// Return a loaded key if we have one
|
||||||
|
if (key != null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded key yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
|
synchronized (this) {
|
||||||
|
// Call our helper function to do the cert loading/generation for us
|
||||||
|
getClientCertificate();
|
||||||
|
|
||||||
|
// Return a cached value if we have it
|
||||||
|
return pemCertBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeBase64String(byte[] data) {
|
||||||
|
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -17,6 +18,24 @@ public class ControllerHandler {
|
|||||||
private short rightStickY = 0x0000;
|
private short rightStickY = 0x0000;
|
||||||
private short leftStickX = 0x0000;
|
private short leftStickX = 0x0000;
|
||||||
private short leftStickY = 0x0000;
|
private short leftStickY = 0x0000;
|
||||||
|
private int emulatingButtonFlags = 0;
|
||||||
|
|
||||||
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
|
// a bumper up and compare that to our maximum delay when we receive
|
||||||
|
// a Start button press to see if we should activate one of our
|
||||||
|
// emulated button combos.
|
||||||
|
private long lastLbUpTime = 0;
|
||||||
|
private long lastRbUpTime = 0;
|
||||||
|
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||||
|
|
||||||
|
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||||
|
|
||||||
|
private static final int EMULATING_SPECIAL = 0x1;
|
||||||
|
private static final int EMULATING_SELECT = 0x2;
|
||||||
|
|
||||||
|
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
||||||
|
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
||||||
|
|
||||||
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||||
|
|
||||||
@@ -298,6 +317,18 @@ public class ControllerHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the button hasn't been down long enough, sleep for a bit before sending the up event
|
||||||
|
// This allows "instant" button presses (like OUYA's virtual menu button) to work. This
|
||||||
|
// path should not be triggered during normal usage.
|
||||||
|
if (SystemClock.uptimeMillis() - event.getDownTime() < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
|
||||||
|
{
|
||||||
|
// Since our sleep time is so short (10 ms), it shouldn't cause a problem doing this in the
|
||||||
|
// UI thread.
|
||||||
|
try {
|
||||||
|
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
@@ -337,9 +368,11 @@ public class ControllerHandler {
|
|||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
inputMap &= ~ControllerPacket.LB_FLAG;
|
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||||
|
lastLbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
inputMap &= ~ControllerPacket.RB_FLAG;
|
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||||
|
lastRbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
@@ -357,11 +390,39 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If one of the two is up, the special button comes up too
|
// Check if we're emulating the select button
|
||||||
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
|
|
||||||
{
|
{
|
||||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
// If either start or LB is up, select comes up too
|
||||||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
|
(inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||||
|
{
|
||||||
|
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're emulating the special button
|
||||||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||||
|
{
|
||||||
|
// If either start or select and RB is up, the special button comes up too
|
||||||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
|
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||||
|
(inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||||
|
{
|
||||||
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
sendControllerInputPacket();
|
||||||
@@ -438,12 +499,27 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We detect back+start as the special button combo
|
// Start+LB acts like select for controllers with one button
|
||||||
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
|
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
|
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||||
|
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
|
{
|
||||||
|
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||||
|
inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We detect select+start or start+RB as the special button combo
|
||||||
|
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||||
|
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||||
|
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||||
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||||
{
|
{
|
||||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
|
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
sendControllerInputPacket();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
import com.limelight.nvstream.av.DecodeUnit;
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||||
|
|
||||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||||
@@ -80,7 +81,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.targetFps = redrawRate;
|
this.targetFps = redrawRate;
|
||||||
|
|
||||||
int perfLevel = findOptimalPerformanceLevel();
|
int perfLevel = findOptimalPerformanceLevel();
|
||||||
@@ -138,25 +139,28 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||||
|
|
||||||
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public boolean start(final VideoDepacketizer depacketizer) {
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
|
DecodeUnit du;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
submitDecodeUnit(du);
|
||||||
|
}
|
||||||
|
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
if (diff > WAIT_CEILING_MS) {
|
||||||
try {
|
continue;
|
||||||
Thread.sleep(diff);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||||
@@ -165,7 +169,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
rendererThread.setName("Video - Renderer (CPU)");
|
rendererThread.setName("Video - Renderer (CPU)");
|
||||||
|
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||||
rendererThread.start();
|
rendererThread.start();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long computePresentationTimeMs(int frameRate) {
|
private long computePresentationTimeMs(int frameRate) {
|
||||||
@@ -186,8 +192,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
AvcDecoder.destroy();
|
AvcDecoder.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
|
||||||
byte[] data;
|
byte[] data;
|
||||||
|
|
||||||
// Use the reserved decoder buffer if this decode unit will fit
|
// Use the reserved decoder buffer if this decode unit will fit
|
||||||
@@ -217,4 +222,14 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
import com.limelight.nvstream.av.DecodeUnit;
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
|
||||||
@@ -9,11 +9,13 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
decoderRenderer.release();
|
if (decoderRenderer != null) {
|
||||||
|
decoderRenderer.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||||
MediaCodecDecoderRenderer.findSafeDecoder() != null)) {
|
MediaCodecDecoderRenderer.findSafeDecoder() != null)) {
|
||||||
@@ -22,12 +24,12 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
else {
|
else {
|
||||||
decoderRenderer = new AndroidCpuDecoderRenderer();
|
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||||
}
|
}
|
||||||
decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public boolean start(VideoDepacketizer depacketizer) {
|
||||||
decoderRenderer.start();
|
return decoderRenderer.start(depacketizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -35,14 +37,28 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
decoderRenderer.stop();
|
decoderRenderer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean submitDecodeUnit(DecodeUnit du) {
|
|
||||||
return decoderRenderer.submitDecodeUnit(du);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return decoderRenderer.getCapabilities();
|
return decoderRenderer.getCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getAverageDecoderLatency();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getAverageEndToEndLatency();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
import com.limelight.nvstream.av.DecodeUnit;
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
@@ -23,15 +24,19 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private int redrawRate;
|
|
||||||
private boolean needsSpsBitstreamFixup;
|
private boolean needsSpsBitstreamFixup;
|
||||||
private boolean needsSpsNumRefFixup;
|
private boolean needsSpsNumRefFixup;
|
||||||
private boolean fastInputQueueing;
|
private VideoDepacketizer depacketizer;
|
||||||
|
|
||||||
|
private long totalTimeMs;
|
||||||
|
private long decoderTimeMs;
|
||||||
|
private int totalFrames;
|
||||||
|
|
||||||
|
private final static byte[] BITSTREAM_RESTRICTIONS = new byte[] {(byte) 0xF1, (byte) 0x83, 0x2A, 0x00};
|
||||||
|
|
||||||
public static final List<String> blacklistedDecoderPrefixes;
|
public static final List<String> blacklistedDecoderPrefixes;
|
||||||
public static final List<String> spsFixupBitsreamFixupDecoderPrefixes;
|
public static final List<String> spsFixupBitsreamFixupDecoderPrefixes;
|
||||||
public static final List<String> spsFixupNumRefFixupDecoderPrefixes;
|
public static final List<String> spsFixupNumRefFixupDecoderPrefixes;
|
||||||
public static final List<String> fastInputQueueingPrefixes;
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||||
@@ -45,11 +50,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
spsFixupNumRefFixupDecoderPrefixes = new LinkedList<String>();
|
spsFixupNumRefFixupDecoderPrefixes = new LinkedList<String>();
|
||||||
spsFixupNumRefFixupDecoderPrefixes.add("omx.TI");
|
spsFixupNumRefFixupDecoderPrefixes.add("omx.TI");
|
||||||
}
|
spsFixupNumRefFixupDecoderPrefixes.add("omx.qcom");
|
||||||
|
|
||||||
static {
|
|
||||||
fastInputQueueingPrefixes = new LinkedList<String>();
|
|
||||||
fastInputQueueingPrefixes.add("omx.nvidia");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
@@ -124,32 +125,32 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.redrawRate = redrawRate;
|
|
||||||
|
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
MediaCodecInfo safeDecoder = findSafeDecoder();
|
// It's nasty to put all this in a try-catch block,
|
||||||
if (safeDecoder != null) {
|
// but codecs have been known to throw all sorts of crazy runtime exceptions
|
||||||
videoDecoder = MediaCodec.createByCodecName(safeDecoder.getName());
|
// due to implementation problems
|
||||||
needsSpsBitstreamFixup = isDecoderInList(spsFixupBitsreamFixupDecoderPrefixes, safeDecoder.getName());
|
try {
|
||||||
needsSpsNumRefFixup = isDecoderInList(spsFixupNumRefFixupDecoderPrefixes, safeDecoder.getName());
|
MediaCodecInfo safeDecoder = findSafeDecoder();
|
||||||
if (needsSpsBitstreamFixup) {
|
if (safeDecoder != null) {
|
||||||
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS bitstream restrictions fixup");
|
videoDecoder = MediaCodec.createByCodecName(safeDecoder.getName());
|
||||||
|
needsSpsBitstreamFixup = isDecoderInList(spsFixupBitsreamFixupDecoderPrefixes, safeDecoder.getName());
|
||||||
|
needsSpsNumRefFixup = isDecoderInList(spsFixupNumRefFixupDecoderPrefixes, safeDecoder.getName());
|
||||||
|
if (needsSpsBitstreamFixup) {
|
||||||
|
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS bitstream restrictions fixup");
|
||||||
|
}
|
||||||
|
if (needsSpsNumRefFixup) {
|
||||||
|
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (needsSpsNumRefFixup) {
|
else {
|
||||||
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
|
videoDecoder = MediaCodec.createDecoderByType("video/avc");
|
||||||
|
needsSpsBitstreamFixup = false;
|
||||||
|
needsSpsNumRefFixup = false;
|
||||||
}
|
}
|
||||||
fastInputQueueing = isDecoderInList(fastInputQueueingPrefixes, safeDecoder.getName());
|
} catch (Exception e) {
|
||||||
if (fastInputQueueing) {
|
return false;
|
||||||
LimeLog.info("Decoder "+safeDecoder.getName()+" supports fast input queueing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
videoDecoder = MediaCodec.createDecoderByType("video/avc");
|
|
||||||
needsSpsBitstreamFixup = false;
|
|
||||||
needsSpsNumRefFixup = false;
|
|
||||||
fastInputQueueing = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
||||||
@@ -160,6 +161,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||||
|
|
||||||
LimeLog.info("Using hardware decoding");
|
LimeLog.info("Using hardware decoding");
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRendererThread()
|
private void startRendererThread()
|
||||||
@@ -167,66 +170,66 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTimeUs = 0;
|
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
|
DecodeUnit du;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
// Block for a maximum of 100 ms
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100000);
|
if (du != null) {
|
||||||
switch (outIndex) {
|
submitDecodeUnit(du);
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
}
|
||||||
LimeLog.info("Output buffers changed");
|
|
||||||
break;
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
|
||||||
LimeLog.info("Output format changed");
|
|
||||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outIndex >= 0) {
|
if (outIndex >= 0) {
|
||||||
int lastIndex = outIndex;
|
int lastIndex = outIndex;
|
||||||
boolean render = false;
|
|
||||||
|
|
||||||
if (currentTimeUs() >= nextFrameTimeUs) {
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
render = true;
|
long delta = System.currentTimeMillis()-(info.presentationTimeUs/1000);
|
||||||
nextFrameTimeUs = computePresentationTime(redrawRate);
|
if (delta > 5 && delta < 300) {
|
||||||
}
|
decoderTimeMs += delta;
|
||||||
|
totalTimeMs += delta;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the last output buffer in the queue
|
// Get the last output buffer in the queue
|
||||||
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||||
lastIndex = outIndex;
|
lastIndex = outIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render that buffer if it's time for the next frame
|
// Render the last buffer
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, render);
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
} else {
|
||||||
|
switch (outIndex) {
|
||||||
|
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||||
|
LimeLog.info("Output buffers changed");
|
||||||
|
break;
|
||||||
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
|
LimeLog.info("Output format changed");
|
||||||
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
rendererThread.setName("Video - Renderer (MediaCodec)");
|
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||||
|
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||||
rendererThread.start();
|
rendererThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long currentTimeUs() {
|
|
||||||
return System.nanoTime() / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long computePresentationTime(int frameRate) {
|
|
||||||
return currentTimeUs() + (1000000 / frameRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public boolean start(VideoDepacketizer depacketizer) {
|
||||||
|
this.depacketizer = depacketizer;
|
||||||
startRendererThread();
|
startRendererThread();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
rendererThread.interrupt();
|
rendererThread.interrupt();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException e) { }
|
} catch (InterruptedException e) { }
|
||||||
@@ -239,35 +242,37 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
|
||||||
if (decodeUnit.getType() != DecodeUnit.TYPE_H264) {
|
|
||||||
System.err.println("Unknown decode unit type");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mcFlags = 0;
|
|
||||||
|
|
||||||
if ((decodeUnit.getFlags() & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
|
||||||
LimeLog.info("Codec config");
|
|
||||||
mcFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
|
||||||
}
|
|
||||||
if ((decodeUnit.getFlags() & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
|
|
||||||
LimeLog.info("Sync frame");
|
|
||||||
mcFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||||
if (inputIndex >= 0)
|
if (inputIndex >= 0)
|
||||||
{
|
{
|
||||||
ByteBuffer buf = videoDecoderInputBuffers[inputIndex];
|
ByteBuffer buf = videoDecoderInputBuffers[inputIndex];
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 300) {
|
||||||
|
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear old input data
|
// Clear old input data
|
||||||
buf.clear();
|
buf.clear();
|
||||||
|
|
||||||
if (needsSpsBitstreamFixup || needsSpsNumRefFixup) {
|
int codecFlags = 0;
|
||||||
|
int decodeUnitFlags = decodeUnit.getFlags();
|
||||||
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||||
|
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||||
|
}
|
||||||
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
|
||||||
|
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0 &&
|
||||||
|
(needsSpsBitstreamFixup || needsSpsNumRefFixup)) {
|
||||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
||||||
if (header.data[header.offset+4] == 0x67) {
|
if (header.data[header.offset+4] == 0x67) {
|
||||||
|
byte last = header.data[header.length+header.offset-1];
|
||||||
|
|
||||||
// TI OMAP4 requires a reference frame count of 1 to decode successfully
|
// TI OMAP4 requires a reference frame count of 1 to decode successfully
|
||||||
if (needsSpsNumRefFixup) {
|
if (needsSpsNumRefFixup) {
|
||||||
LimeLog.info("Fixing up num ref frames");
|
LimeLog.info("Fixing up num ref frames");
|
||||||
@@ -279,31 +284,41 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// We manually modify the SPS here to speed-up decoding if the decoder was flagged as needing it.
|
// We manually modify the SPS here to speed-up decoding if the decoder was flagged as needing it.
|
||||||
int spsLength;
|
int spsLength;
|
||||||
if (needsSpsBitstreamFixup) {
|
if (needsSpsBitstreamFixup) {
|
||||||
switch (header.length) {
|
if (!needsSpsNumRefFixup) {
|
||||||
case 26:
|
switch (header.length) {
|
||||||
LimeLog.info("Adding bitstream restrictions to SPS (26)");
|
case 26:
|
||||||
buf.put(header.data, header.offset, 24);
|
LimeLog.info("Adding bitstream restrictions to SPS (26)");
|
||||||
buf.put((byte) 0x11);
|
buf.put(header.data, header.offset, 24);
|
||||||
buf.put((byte) 0xe3);
|
buf.put((byte) 0x11);
|
||||||
buf.put((byte) 0x06);
|
buf.put((byte) 0xe3);
|
||||||
buf.put((byte) 0x50);
|
buf.put((byte) 0x06);
|
||||||
spsLength = header.length + 2;
|
buf.put((byte) 0x50);
|
||||||
break;
|
spsLength = header.length + 2;
|
||||||
case 27:
|
break;
|
||||||
LimeLog.info("Adding bitstream restrictions to SPS (27)");
|
case 27:
|
||||||
buf.put(header.data, header.offset, 25);
|
LimeLog.info("Adding bitstream restrictions to SPS (27)");
|
||||||
buf.put((byte) 0x04);
|
buf.put(header.data, header.offset, 25);
|
||||||
buf.put((byte) 0x78);
|
buf.put((byte) 0x04);
|
||||||
buf.put((byte) 0xc1);
|
buf.put((byte) 0x78);
|
||||||
buf.put((byte) 0x94);
|
buf.put((byte) 0xc1);
|
||||||
spsLength = header.length + 2;
|
buf.put((byte) 0x94);
|
||||||
break;
|
spsLength = header.length + 2;
|
||||||
default:
|
break;
|
||||||
LimeLog.warning("Unknown SPS of length "+header.length);
|
default:
|
||||||
|
LimeLog.warning("Unknown SPS of length "+header.length);
|
||||||
|
buf.put(header.data, header.offset, header.length);
|
||||||
|
spsLength = header.length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set bitstream restrictions to only buffer single frame
|
||||||
|
// (starts 9 bits before stop bit and 6 bits earlier because of the shortening above)
|
||||||
|
this.replace(header, header.length*8+Integer.numberOfLeadingZeros(last & - last)%8-9-6, 2, BITSTREAM_RESTRICTIONS, 3*8);
|
||||||
buf.put(header.data, header.offset, header.length);
|
buf.put(header.data, header.offset, header.length);
|
||||||
spsLength = header.length;
|
spsLength = header.length;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
buf.put(header.data, header.offset, header.length);
|
buf.put(header.data, header.offset, header.length);
|
||||||
@@ -312,7 +327,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
videoDecoder.queueInputBuffer(inputIndex,
|
videoDecoder.queueInputBuffer(inputIndex,
|
||||||
0, spsLength,
|
0, spsLength,
|
||||||
0, mcFlags);
|
currentTime * 1000, codecFlags);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,7 +340,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
videoDecoder.queueInputBuffer(inputIndex,
|
videoDecoder.queueInputBuffer(inputIndex,
|
||||||
0, decodeUnit.getDataLength(),
|
0, decodeUnit.getDataLength(),
|
||||||
0, mcFlags);
|
currentTime * 1000, codecFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -333,7 +348,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return fastInputQueueing ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,4 +429,20 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
source.offset = offset;
|
source.offset = offset;
|
||||||
source.length = length;
|
source.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
if (totalFrames == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)(decoderTimeMs / totalFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (totalFrames == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)(totalTimeMs / totalFrames);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.renderscript.Allocation;
|
|
||||||
import android.renderscript.Element;
|
|
||||||
import android.renderscript.RenderScript;
|
|
||||||
import android.renderscript.Type;
|
|
||||||
import android.view.Surface;
|
|
||||||
|
|
||||||
public class RsRenderer {
|
|
||||||
private RenderScript rs;
|
|
||||||
private Allocation renderBuffer;
|
|
||||||
|
|
||||||
public RsRenderer(Context context, int width, int height, Surface renderTarget) {
|
|
||||||
rs = RenderScript.create(context);
|
|
||||||
|
|
||||||
Type.Builder tb = new Type.Builder(rs, Element.RGBA_8888(rs));
|
|
||||||
tb.setX(width);
|
|
||||||
tb.setY(height);
|
|
||||||
Type bufferType = tb.create();
|
|
||||||
|
|
||||||
renderBuffer = Allocation.createTyped(rs, bufferType, Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
|
|
||||||
renderBuffer.setSurface(renderTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() {
|
|
||||||
renderBuffer.setSurface(null);
|
|
||||||
renderBuffer.destroy();
|
|
||||||
rs.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void render(byte[] rgbData) {
|
|
||||||
renderBuffer.copyFrom(rgbData);
|
|
||||||
renderBuffer.ioSend();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
|
public class ComputerDatabaseManager {
|
||||||
|
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||||
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
|
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||||
|
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||||
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
|
|
||||||
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
|
public ComputerDatabaseManager(Context c) {
|
||||||
|
try {
|
||||||
|
// Create or open an existing DB
|
||||||
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
// Delete the DB and try again
|
||||||
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
|
}
|
||||||
|
computerDb.enableWriteAheadLogging();
|
||||||
|
initializeDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
computerDb.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeDb() {
|
||||||
|
// Create tables if they aren't already there
|
||||||
|
computerDb.execSQL(String.format("CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||||
|
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||||
|
COMPUTER_TABLE_NAME,
|
||||||
|
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||||
|
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteComputer(String name) {
|
||||||
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
|
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||||
|
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||||
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ComputerDetails> getAllComputers() {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
|
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, skip the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
details.macAddress == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
computerList.add(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return computerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerDetails getComputerByName(String name) {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
if (!c.moveToFirst()) {
|
||||||
|
// No matching computer
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, delete the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
details.macAddress == null) {
|
||||||
|
deleteComputer(details.name);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
public interface ComputerManagerListener {
|
||||||
|
public void notifyComputerUpdated(ComputerDetails details);
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InterfaceAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.discovery.DiscoveryService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
public class ComputerManagerService extends Service {
|
||||||
|
private static final int MAX_CONCURRENT_REQUESTS = 4;
|
||||||
|
private static final int POLLING_PERIOD_MS = 5000;
|
||||||
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
|
||||||
|
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
|
private ComputerDatabaseManager dbManager;
|
||||||
|
private IdentityManager idManager;
|
||||||
|
private ThreadPoolExecutor pollingPool;
|
||||||
|
private Timer pollingTimer;
|
||||||
|
private ComputerManagerListener listener = null;
|
||||||
|
|
||||||
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
|
private ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
synchronized (discoveryServiceConnection) {
|
||||||
|
discoveryBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||||
|
|
||||||
|
// Set us as the event listener
|
||||||
|
discoveryBinder.setListener(createDiscoveryListener());
|
||||||
|
|
||||||
|
// Signal a possible waiter that we're all setup
|
||||||
|
discoveryServiceConnection.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
discoveryBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public class ComputerManagerBinder extends Binder {
|
||||||
|
public void startPolling(ComputerManagerListener listener) {
|
||||||
|
// Set the listener
|
||||||
|
ComputerManagerService.this.listener = listener;
|
||||||
|
|
||||||
|
// Start mDNS autodiscovery too
|
||||||
|
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||||
|
|
||||||
|
// Start polling known machines
|
||||||
|
pollingTimer = new Timer();
|
||||||
|
pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitForReady() {
|
||||||
|
synchronized (discoveryServiceConnection) {
|
||||||
|
try {
|
||||||
|
while (discoveryBinder == null) {
|
||||||
|
// Wait for the bind notification
|
||||||
|
discoveryServiceConnection.wait(1000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
|
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
ComputerManagerService.this.addComputer(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
ComputerManagerService.this.removeComputer(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopPolling() {
|
||||||
|
// Just call the unbind handler to cleanup
|
||||||
|
ComputerManagerService.this.onUnbind(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUniqueId() {
|
||||||
|
return idManager.getUniqueId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
// Stop mDNS autodiscovery
|
||||||
|
discoveryBinder.stopDiscovery();
|
||||||
|
|
||||||
|
// Stop polling
|
||||||
|
if (pollingTimer != null) {
|
||||||
|
pollingTimer.cancel();
|
||||||
|
pollingTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the listener
|
||||||
|
listener = null;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||||
|
return new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
// Kick off a serverinfo poll on this machine
|
||||||
|
addComputer(computer.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
|
LimeLog.severe("mDNS discovery failed");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
|
// Put it in the thread pool to process later
|
||||||
|
pollingPool.execute(getPollingRunnable(fakeDetails));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
|
// Block while we try to fill the details
|
||||||
|
getPollingRunnable(fakeDetails).run();
|
||||||
|
|
||||||
|
// If the machine is reachable, it was successful
|
||||||
|
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
// Remove it from the database
|
||||||
|
dbManager.deleteComputer(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimerTask getTimerTask() {
|
||||||
|
return new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
||||||
|
for (ComputerDetails computer : computerList) {
|
||||||
|
pollingPool.execute(getPollingRunnable(computer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getActiveNetworkType() {
|
||||||
|
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||||
|
if (activeNetworkInfo == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeNetworkInfo.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InterfaceAddress getActiveInterfaceAddress() {
|
||||||
|
String matchingPrefix;
|
||||||
|
|
||||||
|
switch (getActiveNetworkType())
|
||||||
|
{
|
||||||
|
case ConnectivityManager.TYPE_ETHERNET:
|
||||||
|
matchingPrefix = "eth";
|
||||||
|
break;
|
||||||
|
case ConnectivityManager.TYPE_WIFI:
|
||||||
|
matchingPrefix = "wlan";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Must be on Ethernet or Wifi to consider that we can send large packets
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the interface that corresponds to the active network
|
||||||
|
try {
|
||||||
|
Enumeration<NetworkInterface> ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (ifaceList.hasMoreElements()) {
|
||||||
|
NetworkInterface iface = ifaceList.nextElement();
|
||||||
|
|
||||||
|
// Look for an interface that matches the prefix we expect
|
||||||
|
if (iface.isUp() && iface.getName().startsWith(matchingPrefix)) {
|
||||||
|
// Find the IPv4 address for the interface
|
||||||
|
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
||||||
|
if (!(addr.getAddress() instanceof Inet4Address)) {
|
||||||
|
// Skip non-IPv4 addresses
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found the right address on the right interface
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SocketException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We didn't find the interface or something else went wrong
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOnSameSubnet(InetAddress targetAddress, InetAddress localAddress, short networkPrefixLength) {
|
||||||
|
byte[] targetBytes = targetAddress.getAddress();
|
||||||
|
byte[] localBytes = localAddress.getAddress();
|
||||||
|
|
||||||
|
for (int byteIndex = 0; networkPrefixLength > 0; byteIndex++) {
|
||||||
|
byte target = targetBytes[byteIndex];
|
||||||
|
byte local = localBytes[byteIndex];
|
||||||
|
|
||||||
|
if (networkPrefixLength >= 8) {
|
||||||
|
// Do a full byte comparison
|
||||||
|
if (target != local) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkPrefixLength -= 8;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
target &= (byte)(0xFF << (8 - networkPrefixLength));
|
||||||
|
local &= (byte)(0xFF << (8 - networkPrefixLength));
|
||||||
|
|
||||||
|
// Do a masked comparison
|
||||||
|
if (target != local) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkPrefixLength = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComputerDetails tryPollIp(InetAddress ipAddr) {
|
||||||
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||||
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
|
return http.getComputerDetails();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||||
|
ComputerDetails polledDetails;
|
||||||
|
|
||||||
|
if (localFirst) {
|
||||||
|
polledDetails = tryPollIp(details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polledDetails == null) {
|
||||||
|
// Failed, so let's try the fallback
|
||||||
|
if (!localFirst) {
|
||||||
|
polledDetails = tryPollIp(details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fallback poll worked
|
||||||
|
if (polledDetails != null) {
|
||||||
|
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
|
ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
|
ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine was unreachable both tries
|
||||||
|
if (polledDetails == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, it's reachable
|
||||||
|
details.update(polledDetails);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean doPollMachine(ComputerDetails details) {
|
||||||
|
// Get the network type
|
||||||
|
int networkType = getActiveNetworkType();
|
||||||
|
switch (networkType) {
|
||||||
|
// We'll check local first on these if we find
|
||||||
|
// we're on the same subnet
|
||||||
|
case ConnectivityManager.TYPE_ETHERNET:
|
||||||
|
case ConnectivityManager.TYPE_WIFI:
|
||||||
|
InterfaceAddress ifaceAddr = getActiveInterfaceAddress();
|
||||||
|
if (ifaceAddr != null) {
|
||||||
|
if (isOnSameSubnet(details.localIp, ifaceAddr.getAddress(), ifaceAddr.getNetworkPrefixLength())) {
|
||||||
|
// It's on the same subnet, so poll local first
|
||||||
|
LimeLog.info("Machine looks local; trying local IP first");
|
||||||
|
return pollComputer(details, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through to remote first
|
||||||
|
default:
|
||||||
|
LimeLog.info("Machine looks remote; trying remote IP first");
|
||||||
|
return pollComputer(details, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable getPollingRunnable(final ComputerDetails details) {
|
||||||
|
return new Runnable() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean newPc = (details.name == null);
|
||||||
|
|
||||||
|
// Poll the machine
|
||||||
|
if (!doPollMachine(details)) {
|
||||||
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
|
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's online, update our persistent state
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
if (!newPc) {
|
||||||
|
// Check if it's in the database because it could have been
|
||||||
|
// removed after this was issued
|
||||||
|
if (dbManager.getComputerByName(details.name) == null) {
|
||||||
|
// It's gone
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbManager.updateComputer(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update anyone listening
|
||||||
|
if (listener != null) {
|
||||||
|
listener.notifyComputerUpdated(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
// Bind to the discovery service
|
||||||
|
bindService(new Intent(this, DiscoveryService.class),
|
||||||
|
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
// Create the thread pool for updating computer state
|
||||||
|
pollingPool = new ThreadPoolExecutor(1, MAX_CONCURRENT_REQUESTS, Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(), new ThreadPoolExecutor.DiscardPolicy());
|
||||||
|
|
||||||
|
// Lookup or generate this device's UID
|
||||||
|
idManager = new IdentityManager(this);
|
||||||
|
|
||||||
|
// Initialize the DB
|
||||||
|
dbManager = new ComputerDatabaseManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (discoveryBinder != null) {
|
||||||
|
// Unbind from the discovery service
|
||||||
|
unbindService(discoveryServiceConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the thread pool
|
||||||
|
pollingPool.shutdownNow();
|
||||||
|
try {
|
||||||
|
pollingPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
|
||||||
|
// Close the DB
|
||||||
|
dbManager.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class IdentityManager {
|
||||||
|
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
||||||
|
private static final int UID_SIZE_IN_BYTES = 8;
|
||||||
|
|
||||||
|
private String uniqueId;
|
||||||
|
|
||||||
|
public IdentityManager(Context c) {
|
||||||
|
uniqueId = loadUniqueId(c);
|
||||||
|
if (uniqueId == null) {
|
||||||
|
uniqueId = generateNewUniqueId(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("UID is now: "+uniqueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUniqueId() {
|
||||||
|
return uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadUniqueId(Context c) {
|
||||||
|
// 2 Hex digits per byte
|
||||||
|
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||||
|
InputStreamReader reader = null;
|
||||||
|
LimeLog.info("Reading UID from disk");
|
||||||
|
try {
|
||||||
|
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||||
|
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||||
|
{
|
||||||
|
LimeLog.severe("UID file data is truncated");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new String(uid);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
LimeLog.info("No UID file found");
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
LimeLog.severe("Error while reading UID file");
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
reader.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateNewUniqueId(Context c) {
|
||||||
|
// Generate a new UID hex string
|
||||||
|
LimeLog.info("Generating new UID");
|
||||||
|
String uidStr = String.format("%016x", new Random().nextLong());
|
||||||
|
|
||||||
|
OutputStreamWriter writer = null;
|
||||||
|
try {
|
||||||
|
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||||
|
writer.write(uidStr);
|
||||||
|
LimeLog.info("UID written to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LimeLog.severe("Error while writing UID file");
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (writer != null) {
|
||||||
|
try {
|
||||||
|
writer.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can return a UID even if I/O fails
|
||||||
|
return uidStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.limelight.discovery;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.net.wifi.WifiManager.MulticastLock;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
public class DiscoveryService extends Service {
|
||||||
|
|
||||||
|
private MdnsDiscoveryAgent discoveryAgent;
|
||||||
|
private MdnsDiscoveryListener boundListener;
|
||||||
|
private MulticastLock multicastLock;
|
||||||
|
|
||||||
|
public class DiscoveryBinder extends Binder {
|
||||||
|
public void setListener(MdnsDiscoveryListener listener) {
|
||||||
|
boundListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startDiscovery(int queryIntervalMs) {
|
||||||
|
multicastLock.acquire();
|
||||||
|
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopDiscovery() {
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
|
multicastLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MdnsComputer> getComputerSet() {
|
||||||
|
return discoveryAgent.getComputerSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
|
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||||
|
multicastLock.setReferenceCounted(false);
|
||||||
|
|
||||||
|
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerAdded(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerRemoved(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyDiscoveryFailure(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
// Stop any discovery session
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
|
multicastLock.release();
|
||||||
|
|
||||||
|
// Unbind the listener
|
||||||
|
boundListener = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,11 @@ public class Dialog implements Runnable {
|
|||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
{
|
{
|
||||||
for (Dialog d : rundownDialogs)
|
for (Dialog d : rundownDialogs) {
|
||||||
d.alert.dismiss();
|
if (d.alert.isShowing()) {
|
||||||
|
d.alert.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rundownDialogs.clear();
|
rundownDialogs.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
{
|
{
|
||||||
for (SpinnerDialog d : rundownDialogs)
|
for (SpinnerDialog d : rundownDialogs) {
|
||||||
d.progress.dismiss();
|
if (d.progress.isShowing()) {
|
||||||
|
d.progress.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rundownDialogs.clear();
|
rundownDialogs.clear();
|
||||||
}
|
}
|
||||||
@@ -86,7 +89,9 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
progress.dismiss();
|
if (progress.isShowing()) {
|
||||||
|
progress.dismiss();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user