Compare commits

...

51 Commits

Author SHA1 Message Date
Cameron Gutman
bc2f2de6c2 Update common 2014-07-12 14:07:41 -07:00
Cameron Gutman
efec35afa6 Update version to 2.5.0.2 2014-07-12 14:00:52 -07:00
Cameron Gutman
993711ccfe Don't report decoding time for software decoder because it generally doesn't capture the information we want due to CPU overhead 2014-07-12 13:46:39 -07:00
Cameron Gutman
d1ef912984 Remove the remaining allocations in the AV paths 2014-07-12 13:38:28 -07:00
Cameron Gutman
d2f8ee8b81 Add timekeeping stats to CPU decoder 2014-07-10 18:59:05 -07:00
Cameron Gutman
91e68c0580 Include rendering time in the total decoding time 2014-07-10 18:47:30 -07:00
Cameron Gutman
a591dcec48 Disable write-ahead logging on the computer database 2014-07-10 18:42:42 -07:00
Cameron Gutman
1898fcd741 Prevent crashing if the user exits the AddComputerManually activity before the job completes 2014-07-06 15:55:39 -07:00
Cameron Gutman
8bc8f14c64 Protect from accessing the ComputerManagerService before initialization has completed 2014-07-06 12:49:57 -07:00
Cameron Gutman
09cf5d23ea Small update to fix SecurityException on some older devices 2014-07-05 21:31:21 -07:00
Cameron Gutman
bec5cfe4a6 Update readme 2014-07-05 13:40:40 -07:00
Cameron Gutman
59f7595050 Bump version to 2.5 and make wifi optional 2014-07-05 13:25:17 -07:00
Cameron Gutman
9e20d25093 Fix list view bugs (mostly Ouya related) 2014-07-04 18:57:03 -07:00
Cameron Gutman
155432b4b8 Remove the now unnecessary controllers.json file 2014-07-04 16:57:54 -07:00
Cameron Gutman
0c614516fb The computer details object must always update, even if the text doesn't change 2014-07-04 15:24:01 -07:00
Cameron Gutman
1734d21b8e Update placeholder text in PC view 2014-07-04 15:12:37 -07:00
Cameron Gutman
825233309d Remove init hack in DiscoveryService 2014-07-04 15:11:31 -07:00
Cameron Gutman
0f4e5a4585 Make remote discovery faster. Fix the bitrate slider. Change some toast text. 2014-07-04 14:51:57 -07:00
Cameron Gutman
9be149942a Update with common fixes 2014-07-04 12:16:53 -07:00
Cameron Gutman
2ab14cdbf5 Suppress a Lint warning and remove a trace 2014-07-04 12:12:12 -07:00
Cameron Gutman
b79fd8860c Warn the user if they've entered nothing to add 2014-07-04 11:52:56 -07:00
Cameron Gutman
6f6eb1fb95 Remove a trace 2014-07-04 11:50:13 -07:00
Cameron Gutman
c50ec21f39 Fix crash when tapping the placeholder 2014-07-04 11:31:03 -07:00
Cameron Gutman
3176ee72fe Major update to v2.5 2014-07-03 23:33:14 -07:00
Cameron Gutman
cc37da9a56 Merge branch 'v2.5'
Conflicts:
	libs/limelight-common.jar
2014-07-03 16:30:10 -07:00
Cameron Gutman
87572bb083 Change latency toasts a bit 2014-07-03 13:35:36 -07:00
Cameron Gutman
a74de39879 Update latency tracking code 2014-07-02 08:16:54 -07:00
Cameron Gutman
1d27309e53 Revert "Disable latency tracking due to performance problems"
This reverts commit 3dd57e9f38.
2014-07-01 19:51:45 -07:00
Cameron Gutman
3f00885d2c Protect against a null pointer exception if setup() fails 2014-07-01 07:27:32 -07:00
Cameron Gutman
ea3b1a7438 Bump to 2.5 2014-06-30 21:25:30 -07:00
Cameron Gutman
3dd57e9f38 Disable latency tracking due to performance problems 2014-06-30 21:23:45 -07:00
Cameron Gutman
a639143e94 Add a latency toast after the stream stops 2014-06-30 21:14:27 -07:00
Cameron Gutman
b3c78ce1b1 Submit codec config frames with the corresponding flag to work around an TI OMAP 4 errata. Also add a file that documents the currently known codec errata. 2014-06-30 21:05:10 -07:00
Cameron Gutman
ddf9284ece Add a Quit Steam button 2014-06-30 01:14:46 -07:00
Cameron Gutman
438be4b8e5 Add a mDNS auto-discovery service 2014-06-30 00:28:32 -07:00
Cameron Gutman
8f43b95129 Remove the RenderScript renderer 2014-06-29 23:51:59 -07:00
Cameron Gutman
faa2be431f Replace dnsjava with jmdns and remove a redundant classpath entry 2014-06-29 23:50:32 -07:00
Cameron Gutman
16de523e78 Ignore Lint error because Bouncy Castle calls API that don't exist in Android (some javax packages). We don't call these paths so we're okay now, but we need to remember that this can be a problem later on. 2014-06-29 12:49:10 -07:00
Cameron Gutman
b24abc6ddd Update common for the app launch support 2014-06-29 12:40:22 -07:00
Cameron Gutman
a450cd5b01 Remove deprecated crypto API usage based on irtimmer's changes to Limelight-PC 2014-06-29 12:39:36 -07:00
Cameron Gutman
2e3b7a2c09 Ignore Lint accessibility warning for onTouch handler 2014-06-29 12:32:10 -07:00
Cameron Gutman
22bba877d7 Ignore Lint warning for Amazon and Ouya assets 2014-06-29 12:12:11 -07:00
Cameron Gutman
7142db3fac Fixes for Android L and some weird codec exceptions 2014-06-29 11:49:42 -07:00
Cameron Gutman
37cf572c0c Increase version number 2014-06-26 20:39:31 -07:00
Cameron Gutman
33c5254d6f Yet again raise the minimum button down time based on testing on Ouya 2014-06-26 20:38:37 -07:00
Cameron Gutman
faa82ca9d6 Fixup num_ref_frames in SPS on Qualcomm devices to (hopefully) fix the crashing video bug on the Galaxy S3 after Android 4.3 2014-06-26 20:03:12 -07:00
Cameron Gutman
0d35ea5207 Enable max Ethernet MTU sized packets when the remote PC is on the subnet 2014-06-22 17:08:22 -07:00
Cameron Gutman
579645c07c Make the pairing toast shorter. Display a nice message when we get a 404 instead of a huge URL. 2014-06-22 13:24:14 -07:00
Cameron Gutman
869cbe2e81 Frame latency and jitter improvements 2014-06-19 19:13:16 -07:00
Cameron Gutman
329a938bf8 Hold a high performance Wi-Fi lock while streaming 2014-06-19 18:26:33 -07:00
Cameron Gutman
411931cc27 Increment version and update common jar 2014-06-15 21:10:42 -07:00
37 changed files with 2667 additions and 700 deletions

View File

@@ -5,6 +5,5 @@
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="lib" path="libs/limelight-common.jar"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight"
android:versionCode="16"
android:versionName="2.2.1.2" >
android:versionCode="22"
android:versionName="2.5.0.2" >
<uses-sdk
android:minSdkVersion="16"
@@ -10,8 +10,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<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-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<application
android:allowBackup="true"
@@ -19,7 +23,8 @@
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<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" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -27,6 +32,35 @@
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
</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
android:name="com.limelight.Game"
android:screenOrientation="sensorLandscape"
@@ -38,6 +72,12 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.Connection" />
</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>
</manifest>

View File

@@ -3,43 +3,41 @@
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software, and created a version that can be run on any Android device.
Limelight will allow you to stream your full collection of Steam games from your Windows PC to your Android device,
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
in your own home, or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows Phone](https://github.com/limelight-stream/limelight-wp) are also in development.
##Features
* Streams Steam and all of your games from your PC to your Android device
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
##Features in development
* Use mDNS to scan for compatible GeForce Experience (GFE) machines on the network
* Choose from the list of available games instead of just launching Steam
* Keyboard input
##Installation
* Download and install Limelight for Android from
[XDA](http://forum.xda-developers.com/showthread.php?t=2505510)
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
##Requirements
* [GFE compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
* Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended)
* Exynos/Snapdragon SoC __OR__ Quad-Core 1.4 GHz Cortex-A9 or higher (Tegra 3)
##Usage
* Turn on Shield Streaming in the GFE settings
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* In Limelight, enter your PC's IP or Hostname and click "Pair"
* When on the same network as your PC, open Limelight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* In Limelight, click "Start Streaming"
* Tap your PC again to view the list of apps to stream
* Play games!
##Contribute

View File

@@ -1,12 +0,0 @@
{
"SupportedControllers" : {
"Gamepad" : {},
"Remote" : "false",
"SecondScreen" : {
"DPad" : "false",
"AnalogSticks" : "0",
"DigitalButtons" : "0",
"Mouse" : "false"
}
}
}

10
decoder-errata.txt Normal file
View File

@@ -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

View File

@@ -34,28 +34,40 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
public static final class drawable {
public static final int app_icon=0x7f020000;
public static final int ic_launcher=0x7f020001;
public static final int ouya_icon=0x7f020002;
public static final int list_view_unselected=0x7f020002;
public static final int ouya_icon=0x7f020003;
}
public static final class id {
public static final int autoDec=0x7f080006;
public static final int bitrateLabel=0x7f08000c;
public static final int bitrateSeekBar=0x7f08000d;
public static final int config1080p30Selected=0x7f08000a;
public static final int config1080p60Selected=0x7f08000b;
public static final int config720p30Selected=0x7f080008;
public static final int config720p60Selected=0x7f080009;
public static final int decoderConfigGroup=0x7f080003;
public static final int hardwareDec=0x7f080007;
public static final int addPc=0x7f080001;
public static final int advancedSettingsButton=0x7f080012;
public static final int appListText=0x7f080009;
public static final int autoDec=0x7f080004;
public static final int bitrateLabel=0x7f080006;
public static final int bitrateSeekBar=0x7f080007;
public static final int config1080p30Selected=0x7f080010;
public static final int config1080p60Selected=0x7f080011;
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 pairButton=0x7f080002;
public static final int softwareDec=0x7f080005;
public static final int statusButton=0x7f080001;
public static final int streamConfigGroup=0x7f080004;
public static final int surfaceView=0x7f08000e;
public static final int manuallyAddPc=0x7f080013;
public static final int pcListView=0x7f080008;
public static final int rowTextView=0x7f080014;
public static final int settingsButton=0x7f08000c;
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 int activity_connection=0x7f030000;
public static final int activity_game=0x7f030001;
public static final int activity_add_computer_manually=0x7f030000;
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 int app_name=0x7f060000;

Binary file not shown.

BIN
libs/jmdns-fixed.jar Normal file

Binary file not shown.

Binary file not shown.

16
lint.xml Normal file
View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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="10dp"
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="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>
<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>

View File

@@ -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>

View File

@@ -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>

17
res/layout/simplerow.xml Normal file
View File

@@ -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>

View File

@@ -0,0 +1,149 @@
package com.limelight;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.LinkedBlockingQueue;
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 ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
private void doAddPc(String host) {
String msg;
boolean finish = false;
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.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() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException e) {}
addThread = null;
}
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@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);
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
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();
computersToAdd.add(hostText.getText().toString());
}
});
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,325 +0,0 @@
package com.limelight;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.utils.Dialog;
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 onStop() {
super.onStop();
Dialog.closeDialogs();
}
@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;
}
// Ensure that the bitrate preference is up to date before
// starting the game activity
prefs.edit().
putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress()).
commit();
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(), PlatformBinding.getCryptoProvider(Connection.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(Connection.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 e1) {
message = "Failed to resolve host";
} catch (Exception e) {
message = e.getMessage();
}
Dialog.closeDialogs();
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("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
}
}

View File

@@ -1,5 +1,6 @@
package com.limelight;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
@@ -19,6 +20,7 @@ import android.content.SharedPreferences;
import android.graphics.Point;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.view.Display;
@@ -56,8 +58,17 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
private boolean connecting = false;
private boolean connected = false;
private ConfigurableDecoderRenderer decoderRenderer;
private WifiManager.WifiLock wifiLock;
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 WIDTH_PREF_STRING = "ResH";
@@ -66,18 +77,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
public static final String DECODER_PREF_STRING = "Decoder";
public static final String BITRATE_PREF_STRING = "Bitrate";
public static final int BITRATE_FLOOR_720_30 = 2;
public static final int BITRATE_FLOOR_720_60 = 4;
public static final int BITRATE_FLOOR_1080_30 = 4;
public static final int BITRATE_FLOOR_1080_60 = 10;
public static final int BITRATE_DEFAULT_720_30 = 5;
public static final int BITRATE_DEFAULT_720_60 = 10;
public static final int BITRATE_DEFAULT_1080_30 = 10;
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_HEIGHT = 720;
public static final int DEFAULT_REFRESH_RATE = 60;
@@ -152,12 +156,26 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
// Warn the user if they're on a metered connection
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
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
new StreamConfiguration(width, height, refreshRate, bitrate * 1000), PlatformBinding.getCryptoProvider(this));
conn = new NvConnection(host, uniqueId, Game.this,
new StreamConfiguration(app, width, height, refreshRate, bitrate * 1000,
enableLargePackets ? 1460 : 1024), PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn);
decoderRenderer = new ConfigurableDecoderRenderer();
// The connection will be started when the surface gets created
sh.addCallback(this);
@@ -211,9 +229,33 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
displayedFailureDialog = true;
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();
}
@Override
protected void onDestroy() {
super.onDestroy();
wifiLock.release();
}
private static byte getModifierState(KeyEvent event) {
byte modifier = 0;
if (event.isShiftPressed()) {
@@ -439,6 +481,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
return onGenericMotionEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
// Send it to the activity's touch event handler
@@ -520,7 +563,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
if (!connected && !connecting) {
connecting = true;
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
PlatformBinding.getAudioRenderer(), new ConfigurableDecoderRenderer());
PlatformBinding.getAudioRenderer(), decoderRenderer);
}
}

View File

@@ -0,0 +1,570 @@
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() {
if (managerBinder != null) {
if (!runningPolling) {
return;
}
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;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", 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;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).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;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).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) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return true;
}
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;
}
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -14,7 +14,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
private AudioTrack track;
@Override
public void streamInitialized(int channelCount, int sampleRate) {
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
@@ -27,7 +27,8 @@ public class AndroidAudioRenderer implements AudioRenderer {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
throw new IllegalArgumentException("Decoder returned unhandled channel count");
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
@@ -47,6 +48,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
AudioTrack.MODE_STREAM);
track.play();
return true;
}
@Override

View File

@@ -21,13 +21,19 @@ 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 javax.security.auth.x500.X500Principal;
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.x509.X509V3CertificateGenerator;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -36,7 +42,6 @@ import android.util.Base64;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.LimelightCryptoProvider;
@SuppressWarnings("deprecation")
public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile;
@@ -108,15 +113,11 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
return false;
}
LimeLog.info("Loaded key pair from disk");
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
X500Principal principalName = new X500Principal("CN=NVIDIA GameStream Client");
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
@@ -136,21 +137,24 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
}
Date now = new Date();
Date expirationDate = new Date();
// Expires in 20 years
expirationDate.setYear(expirationDate.getYear() + 20);
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
certGenerator.setSerialNumber(new BigInteger(snBytes).abs());
certGenerator.setIssuerDN(principalName);
certGenerator.setNotBefore(now);
certGenerator.setNotAfter(expirationDate);
certGenerator.setSubjectDN(principalName);
certGenerator.setPublicKey(keyPair.getPublic());
certGenerator.setSignatureAlgorithm("SHA1withRSA");
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 {
cert = certGenerator.generate(keyPair.getPrivate(), "BC");
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

View File

@@ -29,7 +29,7 @@ public class ControllerHandler {
private long lastRbUpTime = 0;
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 10;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1;
private static final int EMULATING_SELECT = 0x2;

View File

@@ -13,6 +13,7 @@ import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
@@ -30,6 +31,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3;
private int totalFrames;
private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors();
private int findOptimalPerformanceLevel() {
@@ -80,7 +84,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
}
@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;
int perfLevel = findOptimalPerformanceLevel();
@@ -138,25 +142,29 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override
public void start() {
public boolean start(final VideoDepacketizer depacketizer) {
rendererThread = new Thread() {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted())
{
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) {
try {
Thread.sleep(diff);
} catch (InterruptedException e) {
return;
}
continue;
}
nextFrameTime = computePresentationTimeMs(targetFps);
@@ -165,7 +173,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
private long computePresentationTimeMs(int frameRate) {
@@ -186,8 +196,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
AvcDecoder.destroy();
}
@Override
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
byte[] data;
// Use the reserved decoder buffer if this decode unit will fit
@@ -210,11 +219,36 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
}
}
return (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += delta;
totalFrames++;
}
}
return success;
}
@Override
public int getCapabilities() {
return 0;
}
@Override
public int getAverageDecoderLatency() {
return 0;
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
}

View File

@@ -1,7 +1,7 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
@@ -9,11 +9,13 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
@Override
public void release() {
decoderRenderer.release();
if (decoderRenderer != null) {
decoderRenderer.release();
}
}
@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 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecDecoderRenderer.findSafeDecoder() != null)) {
@@ -22,12 +24,12 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
}
@Override
public void start() {
decoderRenderer.start();
public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer);
}
@Override
@@ -35,14 +37,28 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
decoderRenderer.stop();
}
@Override
public boolean submitDecodeUnit(DecodeUnit du) {
return decoderRenderer.submitDecodeUnit(du);
}
@Override
public int 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;
}
}
}

View File

@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
@@ -23,15 +24,19 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private int redrawRate;
private boolean needsSpsBitstreamFixup;
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> spsFixupBitsreamFixupDecoderPrefixes;
public static final List<String> spsFixupNumRefFixupDecoderPrefixes;
public static final List<String> fastInputQueueingPrefixes;
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
@@ -45,11 +50,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
spsFixupNumRefFixupDecoderPrefixes = new LinkedList<String>();
spsFixupNumRefFixupDecoderPrefixes.add("omx.TI");
}
static {
fastInputQueueingPrefixes = new LinkedList<String>();
fastInputQueueingPrefixes.add("omx.nvidia");
spsFixupNumRefFixupDecoderPrefixes.add("omx.qcom");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
@@ -124,32 +125,32 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
}
@Override
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.redrawRate = redrawRate;
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
//dumpDecoders();
MediaCodecInfo safeDecoder = findSafeDecoder();
if (safeDecoder != null) {
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");
// It's nasty to put all this in a try-catch block,
// but codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems
try {
MediaCodecInfo safeDecoder = findSafeDecoder();
if (safeDecoder != null) {
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) {
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
else {
videoDecoder = MediaCodec.createDecoderByType("video/avc");
needsSpsBitstreamFixup = false;
needsSpsNumRefFixup = false;
}
fastInputQueueing = isDecoderInList(fastInputQueueingPrefixes, safeDecoder.getName());
if (fastInputQueueing) {
LimeLog.info("Decoder "+safeDecoder.getName()+" supports fast input queueing");
}
}
else {
videoDecoder = MediaCodec.createDecoderByType("video/avc");
needsSpsBitstreamFixup = false;
needsSpsNumRefFixup = false;
fastInputQueueing = false;
} catch (Exception e) {
return false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
@@ -160,6 +161,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
LimeLog.info("Using hardware decoding");
return true;
}
private void startRendererThread()
@@ -167,66 +170,69 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
rendererThread = new Thread() {
@Override
public void run() {
long nextFrameTimeUs = 0;
BufferInfo info = new BufferInfo();
DecodeUnit du;
while (!isInterrupted())
{
// Block for a maximum of 100 ms
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100000);
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;
}
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
boolean render = false;
if (currentTimeUs() >= nextFrameTimeUs) {
render = true;
nextFrameTimeUs = computePresentationTime(redrawRate);
}
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Render that buffer if it's time for the next frame
videoDecoder.releaseOutputBuffer(lastIndex, render);
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta > 5 && delta < 300) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} 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.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
}
private static long currentTimeUs() {
return System.nanoTime() / 1000;
}
private long computePresentationTime(int frameRate) {
return currentTimeUs() + (1000000 / frameRate);
}
@Override
public void start() {
public boolean start(VideoDepacketizer depacketizer) {
this.depacketizer = depacketizer;
startRendererThread();
return true;
}
@Override
public void stop() {
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException e) { }
@@ -239,35 +245,37 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
}
}
@Override
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;
}
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
if (inputIndex >= 0)
{
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
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);
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
if (needsSpsNumRefFixup) {
LimeLog.info("Fixing up num ref frames");
@@ -279,31 +287,41 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
// We manually modify the SPS here to speed-up decoding if the decoder was flagged as needing it.
int spsLength;
if (needsSpsBitstreamFixup) {
switch (header.length) {
case 26:
LimeLog.info("Adding bitstream restrictions to SPS (26)");
buf.put(header.data, header.offset, 24);
buf.put((byte) 0x11);
buf.put((byte) 0xe3);
buf.put((byte) 0x06);
buf.put((byte) 0x50);
spsLength = header.length + 2;
break;
case 27:
LimeLog.info("Adding bitstream restrictions to SPS (27)");
buf.put(header.data, header.offset, 25);
buf.put((byte) 0x04);
buf.put((byte) 0x78);
buf.put((byte) 0xc1);
buf.put((byte) 0x94);
spsLength = header.length + 2;
break;
default:
LimeLog.warning("Unknown SPS of length "+header.length);
if (!needsSpsNumRefFixup) {
switch (header.length) {
case 26:
LimeLog.info("Adding bitstream restrictions to SPS (26)");
buf.put(header.data, header.offset, 24);
buf.put((byte) 0x11);
buf.put((byte) 0xe3);
buf.put((byte) 0x06);
buf.put((byte) 0x50);
spsLength = header.length + 2;
break;
case 27:
LimeLog.info("Adding bitstream restrictions to SPS (27)");
buf.put(header.data, header.offset, 25);
buf.put((byte) 0x04);
buf.put((byte) 0x78);
buf.put((byte) 0xc1);
buf.put((byte) 0x94);
spsLength = header.length + 2;
break;
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);
spsLength = header.length;
break;
}
}
else {
buf.put(header.data, header.offset, header.length);
@@ -312,7 +330,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoder.queueInputBuffer(inputIndex,
0, spsLength,
0, mcFlags);
currentTime * 1000, codecFlags);
return true;
}
}
@@ -325,7 +343,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoder.queueInputBuffer(inputIndex,
0, decodeUnit.getDataLength(),
0, mcFlags);
currentTime * 1000, codecFlags);
}
return true;
@@ -333,7 +351,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
@Override
public int getCapabilities() {
return fastInputQueueing ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return 0;
}
/**
@@ -414,4 +432,20 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
source.offset = offset;
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);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,158 @@
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);
}
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;
}
}

View File

@@ -0,0 +1,7 @@
package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}