mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 18:40:46 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc2f2de6c2 | ||
|
|
efec35afa6 | ||
|
|
993711ccfe | ||
|
|
d1ef912984 | ||
|
|
d2f8ee8b81 | ||
|
|
91e68c0580 | ||
|
|
a591dcec48 | ||
|
|
1898fcd741 | ||
|
|
8bc8f14c64 | ||
|
|
09cf5d23ea | ||
|
|
bec5cfe4a6 | ||
|
|
59f7595050 | ||
|
|
9e20d25093 | ||
|
|
155432b4b8 | ||
|
|
0c614516fb | ||
|
|
1734d21b8e | ||
|
|
825233309d | ||
|
|
0f4e5a4585 | ||
|
|
9be149942a | ||
|
|
2ab14cdbf5 | ||
|
|
b79fd8860c | ||
|
|
6f6eb1fb95 | ||
|
|
c50ec21f39 | ||
|
|
3176ee72fe | ||
|
|
cc37da9a56 | ||
|
|
87572bb083 | ||
|
|
a74de39879 | ||
|
|
1d27309e53 | ||
|
|
3f00885d2c | ||
|
|
ea3b1a7438 | ||
|
|
3dd57e9f38 | ||
|
|
a639143e94 | ||
|
|
b3c78ce1b1 | ||
|
|
ddf9284ece | ||
|
|
438be4b8e5 | ||
|
|
8f43b95129 | ||
|
|
faa2be431f | ||
|
|
16de523e78 | ||
|
|
b24abc6ddd | ||
|
|
a450cd5b01 | ||
|
|
2e3b7a2c09 | ||
|
|
22bba877d7 | ||
|
|
7142db3fac | ||
|
|
37cf572c0c | ||
|
|
33c5254d6f | ||
|
|
faa82ca9d6 | ||
|
|
0d35ea5207 | ||
|
|
579645c07c | ||
|
|
869cbe2e81 | ||
|
|
329a938bf8 | ||
|
|
411931cc27 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
18
README.md
18
README.md
@@ -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
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"SupportedControllers" : {
|
||||
"Gamepad" : {},
|
||||
"Remote" : "false",
|
||||
"SecondScreen" : {
|
||||
"DPad" : "false",
|
||||
"AnalogSticks" : "0",
|
||||
"DigitalButtons" : "0",
|
||||
"Mouse" : "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
decoder-errata.txt
Normal file
10
decoder-errata.txt
Normal 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
|
||||
@@ -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
BIN
libs/jmdns-fixed.jar
Normal file
Binary file not shown.
Binary file not shown.
16
lint.xml
Normal file
16
lint.xml
Normal 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>
|
||||
7
res/drawable/list_view_unselected.xml
Normal file
7
res/drawable/list_view_unselected.xml
Normal 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>
|
||||
34
res/layout/activity_add_computer_manually.xml
Normal file
34
res/layout/activity_add_computer_manually.xml
Normal 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>
|
||||
62
res/layout/activity_advanced_settings.xml
Normal file
62
res/layout/activity_advanced_settings.xml
Normal 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>
|
||||
36
res/layout/activity_app_view.xml
Normal file
36
res/layout/activity_app_view.xml
Normal 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>
|
||||
@@ -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>
|
||||
45
res/layout/activity_pc_view.xml
Normal file
45
res/layout/activity_pc_view.xml
Normal 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>
|
||||
72
res/layout/activity_stream_settings.xml
Normal file
72
res/layout/activity_stream_settings.xml
Normal 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
17
res/layout/simplerow.xml
Normal 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>
|
||||
149
src/com/limelight/AddComputerManually.java
Normal file
149
src/com/limelight/AddComputerManually.java
Normal 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
131
src/com/limelight/AdvancedSettings.java
Normal file
131
src/com/limelight/AdvancedSettings.java
Normal 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");
|
||||
}
|
||||
}
|
||||
256
src/com/limelight/AppView.java
Normal file
256
src/com/limelight/AppView.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
570
src/com/limelight/PcView.java
Normal file
570
src/com/limelight/PcView.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/com/limelight/StreamSettings.java
Normal file
123
src/com/limelight/StreamSettings.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
158
src/com/limelight/computers/ComputerDatabaseManager.java
Normal file
158
src/com/limelight/computers/ComputerDatabaseManager.java
Normal 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;
|
||||
}
|
||||
}
|
||||
7
src/com/limelight/computers/ComputerManagerListener.java
Normal file
7
src/com/limelight/computers/ComputerManagerListener.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public interface ComputerManagerListener {
|
||||
public void notifyComputerUpdated(ComputerDetails details);
|
||||
}
|
||||
424
src/com/limelight/computers/ComputerManagerService.java
Normal file
424
src/com/limelight/computers/ComputerManagerService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
85
src/com/limelight/computers/IdentityManager.java
Normal file
85
src/com/limelight/computers/IdentityManager.java
Normal 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;
|
||||
}
|
||||
}
|
||||
90
src/com/limelight/discovery/DiscoveryService.java
Normal file
90
src/com/limelight/discovery/DiscoveryService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user