Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3b649fe9 | ||
|
|
7223efb9f8 | ||
|
|
c3296cce3d | ||
|
|
5ef20aba21 | ||
|
|
54eaee3f79 | ||
|
|
4c82da1f5c | ||
|
|
080dc01c21 | ||
|
|
f09fbf4ba6 | ||
|
|
ad10413714 | ||
|
|
c9014da186 | ||
|
|
c025f9f02b | ||
|
|
b737acedb0 | ||
|
|
f15bfe3038 | ||
|
|
8938f51292 | ||
|
|
4b92b8f714 | ||
|
|
5f13b9bca4 | ||
|
|
2f219aac6f | ||
|
|
1d9efb30e2 | ||
|
|
ed7be00881 | ||
|
|
a6003f6bff | ||
|
|
4619045375 | ||
|
|
e61b8f1b34 | ||
|
|
79b6ec839a | ||
|
|
fd12e30c53 | ||
|
|
87a9ca4318 | ||
|
|
3f64411174 | ||
|
|
57b0da1a3a | ||
|
|
7d3e74a67f | ||
|
|
d704e322df | ||
|
|
f598153818 | ||
|
|
f395a0c170 | ||
|
|
654b33d27f | ||
|
|
6c12da96c9 | ||
|
|
1a6f639b81 | ||
|
|
59a00a38c9 | ||
|
|
2beee168e3 | ||
|
|
a92bbc7e5a | ||
|
|
fbc921dd07 | ||
|
|
59c6c3d777 | ||
|
|
e7ab61c8d0 | ||
|
|
7023760782 | ||
|
|
932ce435b5 | ||
|
|
af384d88f7 | ||
|
|
792846ddad | ||
|
|
1187d9c78c | ||
|
|
37db9ab072 | ||
|
|
fb40060560 | ||
|
|
a4f4887647 | ||
|
|
f1d7f556fd | ||
|
|
1e70e1d329 | ||
|
|
e02a009635 | ||
|
|
bd6ff35603 | ||
|
|
1cb7727dc7 | ||
|
|
0c73e3d0ae | ||
|
|
6371d364e1 | ||
|
|
ded9c9140d | ||
|
|
7c8a108e28 | ||
|
|
2a18ffcdba | ||
|
|
381d0d5e81 | ||
|
|
be126acfd1 | ||
|
|
fc2f5cfe4d | ||
|
|
9878902a89 | ||
|
|
f1230d46f3 | ||
|
|
d8822392f1 | ||
|
|
1d9cf71517 | ||
|
|
2160e87fef | ||
|
|
88249ba8aa | ||
|
|
2856617fb3 | ||
|
|
d822980d5a | ||
|
|
b5ba59b413 | ||
|
|
1148e0163c | ||
|
|
cf36c7adb1 |
No files matched your search
@@ -1,14 +1,14 @@
|
|||||||
#Limelight
|
#Moonlight
|
||||||
|
|
||||||
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](http://moonlight-stream.com) 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.
|
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 games from your Windows PC to your Android device,
|
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether 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 and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
|
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
##Features
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for
|
|||||||
|
|
||||||
##Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Limelight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
##Requirements
|
##Requirements
|
||||||
@@ -33,7 +33,7 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for
|
|||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Limelight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
@@ -46,8 +46,6 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
Check out our [website](http://limelight-stream.com) for project links and information.
|
|
||||||
|
|
||||||
##Authors
|
##Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
@@ -55,5 +53,5 @@ Check out our [website](http://limelight-stream.com) for project links and infor
|
|||||||
* [Aaron Neyer](https://github.com/Aaronneyer)
|
* [Aaron Neyer](https://github.com/Aaronneyer)
|
||||||
* [Andrew Hennessy](https://github.com/yetanothername)
|
* [Andrew Hennessy](https://github.com/yetanothername)
|
||||||
|
|
||||||
Limelight is the work of students at [Case Western](http://case.edu) and was
|
Moonlight is the work of students at [Case Western](http://case.edu) and was
|
||||||
started as a project at [MHacks](http://mhacks.org).
|
started as a project at [MHacks](http://mhacks.org).
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||||
<component name="FacetManager">
|
<component name="FacetManager">
|
||||||
<facet type="android-gradle" name="Android-Gradle">
|
<facet type="android-gradle" name="Android-Gradle">
|
||||||
<configuration>
|
<configuration>
|
||||||
@@ -13,8 +13,11 @@
|
|||||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
||||||
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
|
||||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
|
<afterSyncTasks>
|
||||||
|
<task>generateNonRootDebugAndroidTestSources</task>
|
||||||
|
<task>generateNonRootDebugSources</task>
|
||||||
|
</afterSyncTasks>
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||||
@@ -23,7 +26,7 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
</facet>
|
</facet>
|
||||||
</component>
|
</component>
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
@@ -33,7 +36,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
||||||
@@ -46,7 +49,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||||
@@ -103,16 +106,15 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
|
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
|
||||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||||
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
|
||||||
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
<orderEntry type="library" exported="" name="jcodec-0.1.9-patched" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 22
|
compileSdkVersion 23
|
||||||
buildToolsVersion "21.1.2"
|
buildToolsVersion "23.0.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 22
|
targetSdkVersion 23
|
||||||
|
|
||||||
versionName "3.1.3"
|
versionName "3.1.12"
|
||||||
versionCode = 58
|
versionCode = 69
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
@@ -62,15 +62,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
|
||||||
|
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
|
||||||
|
|
||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
|
||||||
|
|
||||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
|
compile files('libs/jmdns-3.4.2.jar')
|
||||||
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
|
||||||
|
|
||||||
compile files('libs/jmdns-fixed.jar')
|
|
||||||
compile files('libs/limelight-common.jar')
|
compile files('libs/limelight-common.jar')
|
||||||
compile files('libs/tinyrtsp.jar')
|
compile files('libs/tinyrtsp.jar')
|
||||||
|
compile files('libs/jcodec-0.1.9-patched.jar')
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,10 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
|
|
||||||
|
<!-- Samsung multi-window support -->
|
||||||
|
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
||||||
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
|
|
||||||
<!-- Launcher for traditional devices -->
|
<!-- Launcher for traditional devices -->
|
||||||
<activity
|
<activity
|
||||||
@@ -24,6 +28,7 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.AppGridAdapter;
|
import com.limelight.grid.AppGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
@@ -26,10 +21,8 @@ import com.limelight.utils.SpinnerDialog;
|
|||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
@@ -64,6 +63,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
private long threeFingerDownTime = 0;
|
private long threeFingerDownTime = 0;
|
||||||
|
|
||||||
|
private static final double REFERENCE_HORIZ_RES = 1280;
|
||||||
|
private static final double REFERENCE_VERT_RES = 720;
|
||||||
|
|
||||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
private ControllerHandler controllerHandler;
|
||||||
@@ -77,6 +79,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean displayedFailureDialog = false;
|
private boolean displayedFailureDialog = false;
|
||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
private boolean deferredSurfaceResize = false;
|
||||||
|
|
||||||
private EvdevWatcher evdevWatcher;
|
private EvdevWatcher evdevWatcher;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
@@ -207,17 +210,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||||
|
|
||||||
|
boolean aspectRatioMatch = false;
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
||||||
|
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
||||||
|
// setFixedSize so we can handle 4K properly. The only known devices that have
|
||||||
|
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||||
|
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||||
|
// 4K devices run 6.0 at the moment.
|
||||||
|
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||||
|
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||||
|
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||||
|
LimeLog.info("Stream has compatible aspect ratio with output display");
|
||||||
|
aspectRatioMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SurfaceHolder sh = sv.getHolder();
|
SurfaceHolder sh = sv.getHolder();
|
||||||
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated() || aspectRatioMatch) {
|
||||||
// Set the surface to the size of the video
|
// Set the surface to the size of the video
|
||||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
deferredSurfaceResize = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i,
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
((double)prefConfig.width / (double)screenSize.x),
|
(REFERENCE_HORIZ_RES / (double)screenSize.x),
|
||||||
((double)prefConfig.height / (double)screenSize.y));
|
(REFERENCE_VERT_RES / (double)screenSize.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LimelightBuildProps.ROOT_BUILD) {
|
if (LimelightBuildProps.ROOT_BUILD) {
|
||||||
@@ -520,9 +542,56 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||||
{
|
{
|
||||||
|
// This case is for mice
|
||||||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
||||||
|
{
|
||||||
|
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||||
|
|
||||||
|
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||||
|
// Send the vertical scroll packet
|
||||||
|
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||||
|
conn.sendMouseScroll(vScrollClicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First process the history
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
|
||||||
|
lastButtonState = event.getButtonState();
|
||||||
|
}
|
||||||
// This case is for touch-based input devices
|
// This case is for touch-based input devices
|
||||||
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
|
else
|
||||||
event.getSource() == InputDevice.SOURCE_STYLUS)
|
|
||||||
{
|
{
|
||||||
int actionIndex = event.getActionIndex();
|
int actionIndex = event.getActionIndex();
|
||||||
|
|
||||||
@@ -601,59 +670,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This case is for mice
|
|
||||||
else if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
|
||||||
{
|
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
|
||||||
|
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
|
||||||
// Send the vertical scroll packet
|
|
||||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
|
||||||
conn.sendMouseScroll(vScrollClicks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// First process the history
|
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
|
||||||
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process the current values
|
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Unknown source
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handled a known source
|
// Handled a known source
|
||||||
return true;
|
return true;
|
||||||
@@ -687,8 +703,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Scale the deltas if the device resolution is different
|
// Scale the deltas if the device resolution is different
|
||||||
// than the stream resolution
|
// than the stream resolution
|
||||||
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
|
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)screenSize.x));
|
||||||
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
|
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)screenSize.y));
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
@@ -806,7 +822,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Resize the surface to match the aspect ratio of the video
|
// Resize the surface to match the aspect ratio of the video
|
||||||
// This must be done after the surface is created.
|
// This must be done after the surface is created.
|
||||||
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
|
if (deferredSurfaceResize) {
|
||||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
||||||
prefConfig.width, prefConfig.height);
|
prefConfig.width, prefConfig.height);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private RelativeLayout noPcFoundLayout;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling;
|
private boolean freezeUpdates, runningPolling, hasResumed;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
@@ -215,6 +215,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
hasResumed = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +223,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
|
hasResumed = false;
|
||||||
stopComputerUpdates(false);
|
stopComputerUpdates(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,13 +243,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
|
||||||
startComputerUpdates();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
||||||
|
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
@@ -271,7 +270,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
startComputerUpdates();
|
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
||||||
|
// We don't want to start computer updates again, so we need to keep track of whether we're paused.
|
||||||
|
if (hasResumed) {
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
@@ -306,6 +309,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
addr = computer.remoteIp;
|
addr = computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
@@ -374,7 +381,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doWakeOnLan(final ComputerDetails computer) {
|
private void doWakeOnLan(final ComputerDetails computer) {
|
||||||
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
if (computer.state == ComputerDetails.State.ONLINE) {
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -431,6 +438,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
addr = computer.remoteIp;
|
addr = computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
@@ -606,10 +617,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||||
// Do nothing
|
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
// Open the context menu if a PC is offline or refreshing
|
||||||
// Open the context menu if a PC is offline
|
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else if (computer.details.pairState != PairState.PAIRED) {
|
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
// Pair an unpaired machine by default
|
// Pair an unpaired machine by default
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
|||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
private static final int FRAME_SIZE = 960;
|
|
||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean streamInitialized(int channelCount, int sampleRate) {
|
public boolean streamInitialized(int channelCount, int channelMask, int samplesPerFrame, int sampleRate) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
int bufferSize;
|
||||||
|
int bytesPerFrame = (samplesPerFrame * 2);
|
||||||
|
|
||||||
switch (channelCount)
|
switch (channelCount)
|
||||||
{
|
{
|
||||||
@@ -26,6 +25,12 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
case 2:
|
case 2:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
break;
|
break;
|
||||||
|
case 4:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
return false;
|
return false;
|
||||||
@@ -38,7 +43,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
try {
|
try {
|
||||||
// Buffer two frames of audio if possible
|
// Buffer two frames of audio if possible
|
||||||
bufferSize = FRAME_SIZE * 2;
|
bufferSize = bytesPerFrame * 2;
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
@@ -59,10 +64,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
FRAME_SIZE * 2);
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -31,7 +29,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
|
|
||||||
private final Vector2d inputVector = new Vector2d();
|
private final Vector2d inputVector = new Vector2d();
|
||||||
|
|
||||||
private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
|
private final SparseArray<ControllerContext> contexts = new SparseArray<ControllerContext>();
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final double stickDeadzone;
|
private final double stickDeadzone;
|
||||||
@@ -52,8 +50,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
deadzonePercentage = 10;
|
deadzonePercentage = 10;
|
||||||
|
|
||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int i = 0; i < ids.length; i++) {
|
for (int id : ids) {
|
||||||
InputDevice dev = InputDevice.getDevice(ids[i]);
|
InputDevice dev = InputDevice.getDevice(id);
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
||||||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
||||||
// This looks like a gamepad, but we'll check X and Y to be sure
|
// This looks like a gamepad, but we'll check X and Y to be sure
|
||||||
@@ -97,18 +95,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
private short assignNewControllerNumber() {
|
|
||||||
for (short i = 0; i < 4; i++) {
|
|
||||||
if ((currentControllers & (1 << i)) == 0) {
|
|
||||||
// Found an unused controller value
|
|
||||||
currentControllers |= (1 << i);
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInputDeviceAdded(int deviceId) {
|
public void onInputDeviceAdded(int deviceId) {
|
||||||
// Nothing happening here yet
|
// Nothing happening here yet
|
||||||
@@ -116,13 +102,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInputDeviceRemoved(int deviceId) {
|
public void onInputDeviceRemoved(int deviceId) {
|
||||||
for (Map.Entry<String, ControllerContext> device : contexts.entrySet()) {
|
ControllerContext context = contexts.get(deviceId);
|
||||||
if (device.getValue().id == deviceId) {
|
if (context != null) {
|
||||||
LimeLog.info("Removed controller: "+device.getValue().name);
|
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
|
||||||
releaseControllerNumber(device.getValue().controllerNumber);
|
releaseControllerNumber(context);
|
||||||
contexts.remove(device.getKey());
|
contexts.remove(deviceId);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +117,47 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
onInputDeviceAdded(deviceId);
|
onInputDeviceAdded(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseControllerNumber(int controllerNumber) {
|
private void releaseControllerNumber(ControllerContext context) {
|
||||||
LimeLog.info("Controller number "+controllerNumber+" is now available");
|
if (context.reservedControllerNumber) {
|
||||||
currentControllers &= ~(1 << controllerNumber);
|
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
|
||||||
|
currentControllers &= ~(1 << context.controllerNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called before sending input but after we've determined that this
|
||||||
|
// is definitely a controller (not a keyboard, mouse, or something else)
|
||||||
|
private void assignControllerNumberIfNeeded(ControllerContext context) {
|
||||||
|
if (context.assignedControllerNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info(context.name+" ("+context.id+") needs a controller number assigned");
|
||||||
|
if (context.name != null && context.name.contains("gpio-keys")) {
|
||||||
|
// This is the back button on Shield portable consoles
|
||||||
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
|
context.controllerNumber = 0;
|
||||||
|
}
|
||||||
|
else if (multiControllerEnabled && context.hasJoystickAxes) {
|
||||||
|
context.controllerNumber = 0;
|
||||||
|
|
||||||
|
LimeLog.info("Reserving the next available controller number");
|
||||||
|
for (short i = 0; i < 4; i++) {
|
||||||
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
|
// Found an unused controller value
|
||||||
|
currentControllers |= (1 << i);
|
||||||
|
context.controllerNumber = i;
|
||||||
|
context.reservedControllerNumber = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Not reserving a controller number");
|
||||||
|
context.controllerNumber = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Assigned as controller "+context.controllerNumber);
|
||||||
|
context.assignedControllerNumber = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControllerContext createContextForDevice(InputDevice dev) {
|
private ControllerContext createContextForDevice(InputDevice dev) {
|
||||||
@@ -251,6 +273,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore the back buttonn if a controller has both buttons
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
||||||
|
if (hasSelectKey[0] && hasSelectKey[1]) {
|
||||||
|
LimeLog.info("Ignoring back button because select is present");
|
||||||
|
context.ignoreBack = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (devName != null) {
|
if (devName != null) {
|
||||||
// For the Nexus Player (and probably other ATV devices), we should
|
// For the Nexus Player (and probably other ATV devices), we should
|
||||||
// use the back button as start since it doesn't have a start/menu button
|
// use the back button as start since it doesn't have a start/menu button
|
||||||
@@ -274,7 +305,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
||||||
// It's only a remote if it doesn't any sticks
|
// It's only a remote if it doesn't any sticks
|
||||||
if (!context.hasJoystickAxes) {
|
if (!context.hasJoystickAxes) {
|
||||||
context.isRemote = true;
|
context.ignoreBack = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SHIELD controllers will use small stick deadzones
|
// SHIELD controllers will use small stick deadzones
|
||||||
@@ -282,23 +313,22 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
context.leftStickDeadzoneRadius = 0.07f;
|
context.leftStickDeadzoneRadius = 0.07f;
|
||||||
context.rightStickDeadzoneRadius = 0.07f;
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
}
|
}
|
||||||
|
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
||||||
|
// back presses on this device
|
||||||
|
else if (devName.equals("sec_touchscreen")) {
|
||||||
|
context.ignoreBack = true;
|
||||||
|
}
|
||||||
|
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||||
|
// a back button which we want to ignore since there's already a select button.
|
||||||
|
else if (devName.contains("Razer Serval")) {
|
||||||
|
context.isServal = true;
|
||||||
|
context.ignoreBack = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
||||||
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
|
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
|
||||||
|
|
||||||
if (devName != null && devName.equals("gpio-keys")) {
|
|
||||||
// This is the back button on Shield portable consoles
|
|
||||||
context.controllerNumber = 0;
|
|
||||||
}
|
|
||||||
else if (multiControllerEnabled && context.hasJoystickAxes) {
|
|
||||||
context.controllerNumber = assignNewControllerNumber();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
context.controllerNumber = 0;
|
|
||||||
}
|
|
||||||
LimeLog.info("Assigned as controller "+context.controllerNumber);
|
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,22 +338,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
return defaultContext;
|
return defaultContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
String descriptor = dev.getDescriptor();
|
|
||||||
|
|
||||||
// Return the existing context if it exists
|
// Return the existing context if it exists
|
||||||
ControllerContext context = contexts.get(descriptor);
|
ControllerContext context = contexts.get(dev.getId());
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise create a new context
|
// Otherwise create a new context
|
||||||
context = createContextForDevice(dev);
|
context = createContextForDevice(dev);
|
||||||
contexts.put(descriptor, context);
|
contexts.put(dev.getId(), context);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendControllerInputPacket(ControllerContext context) {
|
private void sendControllerInputPacket(ControllerContext context) {
|
||||||
|
assignControllerNumberIfNeeded(context);
|
||||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
||||||
context.leftTrigger, context.rightTrigger,
|
context.leftTrigger, context.rightTrigger,
|
||||||
context.leftStickX, context.leftStickY,
|
context.leftStickX, context.leftStickY,
|
||||||
@@ -333,8 +362,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||||
// Device MAY BE NULL
|
// Device MAY BE NULL
|
||||||
private int handleRemapping(ControllerContext context, KeyEvent event) {
|
private int handleRemapping(ControllerContext context, KeyEvent event) {
|
||||||
// For remotes, don't capture the back button
|
// Don't capture the back button if configured
|
||||||
if (context.isRemote) {
|
if (context.ignoreBack) {
|
||||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -378,6 +407,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If this is a Serval controller sending an unknown key code, it's probably
|
||||||
|
// the start and select buttons
|
||||||
|
else if (context.isServal && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||||
|
switch (event.getScanCode()) {
|
||||||
|
case 314:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
|
case 315:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
||||||
switch (event.getKeyCode()) {
|
switch (event.getKeyCode()) {
|
||||||
@@ -799,11 +838,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
|
|
||||||
public boolean isDualShock4;
|
public boolean isDualShock4;
|
||||||
public boolean isXboxController;
|
public boolean isXboxController;
|
||||||
|
public boolean isServal;
|
||||||
public boolean backIsStart;
|
public boolean backIsStart;
|
||||||
public boolean modeIsSelect;
|
public boolean modeIsSelect;
|
||||||
public boolean isRemote;
|
public boolean ignoreBack;
|
||||||
public boolean hasJoystickAxes;
|
public boolean hasJoystickAxes;
|
||||||
|
|
||||||
|
public boolean assignedControllerNumber;
|
||||||
|
public boolean reservedControllerNumber;
|
||||||
public short controllerNumber;
|
public short controllerNumber;
|
||||||
|
|
||||||
public short inputMap = 0x0000;
|
public short inputMap = 0x0000;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package com.limelight.binding.input;
|
|||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class TouchContext {
|
public class TouchContext {
|
||||||
private int lastTouchX = 0;
|
private int lastTouchX = 0;
|
||||||
private int lastTouchY = 0;
|
private int lastTouchY = 0;
|
||||||
@@ -10,14 +13,20 @@ public class TouchContext {
|
|||||||
private int originalTouchY = 0;
|
private int originalTouchY = 0;
|
||||||
private long originalTouchTime = 0;
|
private long originalTouchTime = 0;
|
||||||
private boolean cancelled;
|
private boolean cancelled;
|
||||||
|
private boolean confirmedMove;
|
||||||
|
private boolean confirmedDrag;
|
||||||
|
private Timer dragTimer;
|
||||||
|
private double distanceMoved;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final int actionIndex;
|
private final int actionIndex;
|
||||||
private final double xFactor;
|
private final double xFactor;
|
||||||
private final double yFactor;
|
private final double yFactor;
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||||
|
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||||
{
|
{
|
||||||
@@ -32,15 +41,19 @@ public class TouchContext {
|
|||||||
return actionIndex;
|
return actionIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isWithinTapBounds(int touchX, int touchY)
|
||||||
|
{
|
||||||
|
int xDelta = Math.abs(touchX - originalTouchX);
|
||||||
|
int yDelta = Math.abs(touchY - originalTouchY);
|
||||||
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
yDelta <= TAP_MOVEMENT_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTap()
|
private boolean isTap()
|
||||||
{
|
{
|
||||||
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
|
||||||
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||||
|
|
||||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
|
||||||
timeDelta <= TAP_TIME_THRESHOLD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte getMouseButtonIndex()
|
private byte getMouseButtonIndex()
|
||||||
@@ -58,7 +71,13 @@ public class TouchContext {
|
|||||||
originalTouchX = lastTouchX = eventX;
|
originalTouchX = lastTouchX = eventX;
|
||||||
originalTouchY = lastTouchY = eventY;
|
originalTouchY = lastTouchY = eventY;
|
||||||
originalTouchTime = System.currentTimeMillis();
|
originalTouchTime = System.currentTimeMillis();
|
||||||
cancelled = false;
|
cancelled = confirmedDrag = confirmedMove = false;
|
||||||
|
distanceMoved = 0;
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Start the timer for engaging a drag
|
||||||
|
startDragTimer();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -69,10 +88,17 @@ public class TouchContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTap())
|
// Cancel the drag timer
|
||||||
{
|
cancelDragTimer();
|
||||||
byte buttonIndex = getMouseButtonIndex();
|
|
||||||
|
|
||||||
|
byte buttonIndex = getMouseButtonIndex();
|
||||||
|
|
||||||
|
if (confirmedDrag) {
|
||||||
|
// Raise the button after a drag
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
else if (isTap())
|
||||||
|
{
|
||||||
// Lower the mouse button
|
// Lower the mouse button
|
||||||
conn.sendMouseButtonDown(buttonIndex);
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
|
||||||
@@ -87,24 +113,101 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized void startDragTimer() {
|
||||||
|
dragTimer = new Timer(true);
|
||||||
|
dragTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (TouchContext.this) {
|
||||||
|
// Check if someone already set move
|
||||||
|
if (confirmedMove) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (dragTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
dragTimer = null;
|
||||||
|
|
||||||
|
// We haven't been cancelled before the timer expired so begin dragging
|
||||||
|
confirmedDrag = true;
|
||||||
|
conn.sendMouseButtonDown(getMouseButtonIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, DRAG_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelDragTimer() {
|
||||||
|
if (dragTimer != null) {
|
||||||
|
dragTimer.cancel();
|
||||||
|
dragTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
|
||||||
|
// If we've already confirmed something, get out now
|
||||||
|
if (confirmedMove || confirmedDrag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it leaves the tap bounds before the drag time expires, it's a move.
|
||||||
|
if (!isWithinTapBounds(eventX, eventY)) {
|
||||||
|
confirmedMove = true;
|
||||||
|
cancelDragTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the maximum distance moved
|
||||||
|
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
|
||||||
|
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
|
||||||
|
confirmedMove = true;
|
||||||
|
cancelDragTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
{
|
{
|
||||||
// We only send moves for the primary touch point
|
// We only send moves and drags for the primary touch point
|
||||||
if (actionIndex == 0) {
|
if (actionIndex == 0) {
|
||||||
|
checkForConfirmedMove(eventX, eventY);
|
||||||
|
|
||||||
int deltaX = eventX - lastTouchX;
|
int deltaX = eventX - lastTouchX;
|
||||||
int deltaY = eventY - lastTouchY;
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
// Scale the deltas based on the factors passed to our constructor
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
deltaX = (int)Math.round((double)deltaX * xFactor);
|
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
||||||
deltaY = (int)Math.round((double)deltaY * yFactor);
|
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
||||||
|
|
||||||
|
// Fix up the signs
|
||||||
|
if (eventX < lastTouchX) {
|
||||||
|
deltaX = -deltaX;
|
||||||
|
}
|
||||||
|
if (eventY < lastTouchY) {
|
||||||
|
deltaY = -deltaY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
||||||
|
// non-zero to update lastTouch that way devices that report small touch events often
|
||||||
|
// will work correctly
|
||||||
|
if (deltaX != 0) {
|
||||||
|
lastTouchX = eventX;
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
lastTouchY = eventY;
|
||||||
|
}
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
lastTouchX = eventX;
|
lastTouchX = eventX;
|
||||||
lastTouchY = eventY;
|
lastTouchY = eventY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -112,6 +215,14 @@ public class TouchContext {
|
|||||||
|
|
||||||
public void cancelTouch() {
|
public void cancelTouch() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
||||||
|
// Cancel the drag timer
|
||||||
|
cancelDragTimer();
|
||||||
|
|
||||||
|
// If it was a confirmed drag, we'll need to raise the button now
|
||||||
|
if (confirmedDrag) {
|
||||||
|
conn.sendMouseButtonUp(getMouseButtonIndex());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCancelled() {
|
public boolean isCancelled() {
|
||||||
|
|||||||
@@ -177,10 +177,10 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
if (diff > WAIT_CEILING_MS) {
|
||||||
try {
|
try {
|
||||||
@@ -203,7 +203,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private long computePresentationTimeMs(int frameRate) {
|
private long computePresentationTimeMs(int frameRate) {
|
||||||
return System.currentTimeMillis() + (1000 / frameRate);
|
return MediaCodecHelper.getMonotonicMillis() + (1000 / frameRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -251,7 +251,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||||
if (success) {
|
if (success) {
|
||||||
long timeAfterDecode = System.currentTimeMillis();
|
long timeAfterDecode = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
import org.jcodec.codecs.h264.H264Utils;
|
||||||
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ import com.limelight.nvstream.av.DecodeUnit;
|
|||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
@@ -22,15 +22,17 @@ import android.media.MediaCodec.CodecException;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
// Used on versions < 5.0
|
||||||
|
private ByteBuffer[] legacyInputBuffers;
|
||||||
|
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private final boolean needsSpsBitstreamFixup, isExynos4;
|
private final boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private VideoDepacketizer depacketizer;
|
private VideoDepacketizer depacketizer;
|
||||||
private final boolean adaptivePlayback, directSubmit;
|
private final boolean adaptivePlayback, directSubmit;
|
||||||
|
private final boolean constrainedHighProfile;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
@@ -46,7 +48,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
private int numIframeIn;
|
private int numIframeIn;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
public MediaCodecDecoderRenderer() {
|
public MediaCodecDecoderRenderer() {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
// This case is handled later in setup()
|
// This case is handled later in setup()
|
||||||
needsSpsBitstreamFixup = isExynos4 =
|
needsSpsBitstreamFixup = isExynos4 =
|
||||||
adaptivePlayback = directSubmit = false;
|
adaptivePlayback = directSubmit =
|
||||||
|
constrainedHighProfile = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
||||||
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
||||||
|
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(decoderName, decoder);
|
||||||
isExynos4 = MediaCodecHelper.isExynos4Device();
|
isExynos4 = MediaCodecHelper.isExynos4Device();
|
||||||
if (needsSpsBitstreamFixup) {
|
if (needsSpsBitstreamFixup) {
|
||||||
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
||||||
@@ -75,6 +78,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (needsBaselineSpsHack) {
|
if (needsBaselineSpsHack) {
|
||||||
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
|
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
|
||||||
}
|
}
|
||||||
|
if (constrainedHighProfile) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" needs constrained high profile");
|
||||||
|
}
|
||||||
if (isExynos4) {
|
if (isExynos4) {
|
||||||
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
||||||
}
|
}
|
||||||
@@ -118,7 +124,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
|
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (e instanceof CodecException) {
|
if (e instanceof CodecException) {
|
||||||
@@ -134,18 +139,20 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buf != null || codecFlags != 0) {
|
// Only throw if this happens at the beginning of a stream
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
if (totalFrames < 60) {
|
||||||
}
|
if (buf != null || codecFlags != 0) {
|
||||||
else {
|
throw new RendererException(this, e, buf, codecFlags);
|
||||||
throw new RendererException(this, e);
|
}
|
||||||
|
else {
|
||||||
|
throw new RendererException(this, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startDirectSubmitRendererThread()
|
private void startDirectSubmitRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
@@ -169,7 +176,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = System.currentTimeMillis() - (presentationTimeUs / 1000);
|
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||||
if (delta >= 0 && delta < 1000) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
decoderTimeMs += delta;
|
decoderTimeMs += delta;
|
||||||
totalTimeMs += delta;
|
totalTimeMs += delta;
|
||||||
@@ -178,9 +185,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
switch (outIndex) {
|
switch (outIndex) {
|
||||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||||
break;
|
break;
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
|
||||||
LimeLog.info("Output buffers changed");
|
|
||||||
break;
|
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
LimeLog.info("Output format changed");
|
LimeLog.info("Output format changed");
|
||||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
@@ -204,17 +208,17 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
int index;
|
int index;
|
||||||
long startTime, queueTime;
|
long startTime, queueTime;
|
||||||
|
|
||||||
startTime = System.currentTimeMillis();
|
startTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
|
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
queueTime = System.currentTimeMillis();
|
queueTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
if (queueTime - startTime >= 20) {
|
if (queueTime - startTime >= 20) {
|
||||||
LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms");
|
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
@@ -223,7 +227,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private void startLegacyRendererThread()
|
private void startLegacyRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
@@ -240,7 +243,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
inputIndex = dequeueInputBuffer(false, false);
|
inputIndex = dequeueInputBuffer(false, false);
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
if (du != null) {
|
if (du != null) {
|
||||||
lastDuDequeueTime = System.currentTimeMillis();
|
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
notifyDuReceived(du);
|
notifyDuReceived(du);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +252,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, inputIndex);
|
||||||
|
|
||||||
du = null;
|
du = null;
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
@@ -282,7 +285,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (du == null) {
|
if (du == null) {
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
if (du != null) {
|
if (du != null) {
|
||||||
lastDuDequeueTime = System.currentTimeMillis();
|
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
notifyDuReceived(du);
|
notifyDuReceived(du);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,12 +293,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// If we've got both a decode unit and an input buffer, we'll
|
// If we've got both a decode unit and an input buffer, we'll
|
||||||
// submit now. Otherwise, we wait until we have one.
|
// submit now. Otherwise, we wait until we have one.
|
||||||
if (du != null && inputIndex >= 0) {
|
if (du != null && inputIndex >= 0) {
|
||||||
long submissionTime = System.currentTimeMillis();
|
long submissionTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
if (submissionTime - lastDuDequeueTime >= 20) {
|
if (submissionTime - lastDuDequeueTime >= 20) {
|
||||||
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
|
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, inputIndex);
|
||||||
|
|
||||||
// DU and input buffer have both been consumed
|
// DU and input buffer have both been consumed
|
||||||
du = null;
|
du = null;
|
||||||
@@ -321,7 +324,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
|
long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000);
|
||||||
if (delta >= 0 && delta < 1000) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
decoderTimeMs += delta;
|
decoderTimeMs += delta;
|
||||||
totalTimeMs += delta;
|
totalTimeMs += delta;
|
||||||
@@ -335,9 +338,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
LockSupport.parkNanos(1);
|
LockSupport.parkNanos(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
|
||||||
LimeLog.info("Output buffers changed");
|
|
||||||
break;
|
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
LimeLog.info("Output format changed");
|
LimeLog.info("Output format changed");
|
||||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
@@ -365,7 +365,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
|
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
legacyInputBuffers = videoDecoder.getInputBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
if (directSubmit) {
|
if (directSubmit) {
|
||||||
startDirectSubmitRendererThread();
|
startDirectSubmitRendererThread();
|
||||||
@@ -406,7 +408,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
for (i = 0; i < 25; i++) {
|
for (i = 0; i < 25; i++) {
|
||||||
try {
|
try {
|
||||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||||
0, length,
|
offset, length,
|
||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -420,9 +422,44 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Using the new getInputBuffer() API on Lollipop allows
|
||||||
|
// the framework to do some performance optimizations for us
|
||||||
|
private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) {
|
||||||
|
ByteBuffer buf;
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
buf = videoDecoder.getInputBuffer(inputBufferIndex);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buf = legacyInputBuffers[inputBufferIndex];
|
||||||
|
|
||||||
|
// Clear old input data pre-Lollipop
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doProfileSpecificSpsPatching(SeqParameterSet sps) {
|
||||||
|
// Some devices benefit from setting constraint flags 4 & 5 to make this Constrained
|
||||||
|
// High Profile which allows the decoder to assume there will be no B-frames and
|
||||||
|
// reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't
|
||||||
|
// like it so we only set them on devices that are confirmed to benefit from it.
|
||||||
|
if (sps.profile_idc == 100 && constrainedHighProfile) {
|
||||||
|
LimeLog.info("Setting constraint set flags for constrained high profile");
|
||||||
|
sps.constraint_set_4_flag = true;
|
||||||
|
sps.constraint_set_5_flag = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Force the constraints unset otherwise (some may be set by default)
|
||||||
|
sps.constraint_set_4_flag = false;
|
||||||
|
sps.constraint_set_5_flag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) {
|
||||||
long timestampUs = System.currentTimeMillis() * 1000;
|
long timestampUs = System.nanoTime() / 1000;
|
||||||
if (timestampUs <= lastTimestampUs) {
|
if (timestampUs <= lastTimestampUs) {
|
||||||
// We can't submit multiple buffers with the same timestamp
|
// We can't submit multiple buffers with the same timestamp
|
||||||
// so bump it up by one before queuing
|
// so bump it up by one before queuing
|
||||||
@@ -430,8 +467,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
lastTimestampUs = timestampUs;
|
lastTimestampUs = timestampUs;
|
||||||
|
|
||||||
// Clear old input data
|
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
buf.clear();
|
|
||||||
|
|
||||||
int codecFlags = 0;
|
int codecFlags = 0;
|
||||||
int decodeUnitFlags = decodeUnit.getFlags();
|
int decodeUnitFlags = decodeUnit.getFlags();
|
||||||
@@ -455,7 +491,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Skip to the start of the NALU data
|
// Skip to the start of the NALU data
|
||||||
spsBuf.position(header.offset+5);
|
spsBuf.position(header.offset+5);
|
||||||
|
|
||||||
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
|
// The H264Utils.readSPS function safely handles
|
||||||
|
// Annex B NALUs (including NALUs with escape sequences)
|
||||||
|
SeqParameterSet sps = H264Utils.readSPS(spsBuf);
|
||||||
|
|
||||||
// Some decoders rely on H264 level to decide how many buffers are needed
|
// Some decoders rely on H264 level to decide how many buffers are needed
|
||||||
// Since we only need one frame buffered, we'll set the level as low as we can
|
// Since we only need one frame buffered, we'll set the level as low as we can
|
||||||
@@ -483,19 +521,44 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
LimeLog.info("Patching num_ref_frames in SPS");
|
LimeLog.info("Patching num_ref_frames in SPS");
|
||||||
sps.num_ref_frames = 1;
|
sps.num_ref_frames = 1;
|
||||||
|
|
||||||
|
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||||
|
// Some devices don't like these so we remove them here.
|
||||||
|
sps.vuiParams.video_signal_type_present_flag = false;
|
||||||
|
sps.vuiParams.colour_description_present_flag = false;
|
||||||
|
sps.vuiParams.chroma_loc_info_present_flag = false;
|
||||||
|
|
||||||
if (needsSpsBitstreamFixup || isExynos4) {
|
if (needsSpsBitstreamFixup || isExynos4) {
|
||||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||||
LimeLog.info("Adding bitstream restrictions");
|
|
||||||
|
|
||||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
// GFE 2.5.11 started sending bitstream restrictions
|
||||||
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
if (sps.vuiParams.bitstreamRestriction == null) {
|
||||||
|
LimeLog.info("Adding bitstream restrictions");
|
||||||
|
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||||
|
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
||||||
|
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
||||||
|
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
||||||
|
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Patching bitstream restrictions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
|
||||||
|
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
|
||||||
|
|
||||||
|
// These values are the defaults for the fields, but they are more aggressive
|
||||||
|
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
|
||||||
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
||||||
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
||||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
// conservative values by GFE 2.5.11. We'll let those values stand.
|
||||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
|
}
|
||||||
|
else {
|
||||||
|
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
|
||||||
|
// will continue to not receive them now
|
||||||
|
sps.vuiParams.bitstreamRestriction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we need to hack this SPS to say we're baseline, do so now
|
// If we need to hack this SPS to say we're baseline, do so now
|
||||||
@@ -505,11 +568,16 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
savedSps = sps;
|
savedSps = sps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch the SPS constraint flags
|
||||||
|
doProfileSpecificSpsPatching(sps);
|
||||||
|
|
||||||
// Write the annex B header
|
// Write the annex B header
|
||||||
buf.put(header.data, header.offset, 5);
|
buf.put(header.data, header.offset, 5);
|
||||||
|
|
||||||
// Write the modified SPS to the input buffer
|
// The H264Utils.writeSPS function safely handles
|
||||||
sps.write(buf);
|
// Annex B NALUs (including NALUs with escape sequences)
|
||||||
|
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, header.length);
|
||||||
|
buf.put(escapedNalu);
|
||||||
|
|
||||||
queueInputBuffer(inputBufferIndex,
|
queueInputBuffer(inputBufferIndex,
|
||||||
0, buf.position(),
|
0, buf.position(),
|
||||||
@@ -549,9 +617,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
private void replaySps() {
|
private void replaySps() {
|
||||||
int inputIndex = dequeueInputBuffer(true, true);
|
int inputIndex = dequeueInputBuffer(true, true);
|
||||||
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
|
||||||
|
|
||||||
inputBuffer.clear();
|
|
||||||
|
|
||||||
// Write the Annex B header
|
// Write the Annex B header
|
||||||
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||||
@@ -559,6 +625,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Switch the H264 profile back to high
|
// Switch the H264 profile back to high
|
||||||
savedSps.profile_idc = 100;
|
savedSps.profile_idc = 100;
|
||||||
|
|
||||||
|
// Patch the SPS constraint flags
|
||||||
|
doProfileSpecificSpsPatching(savedSps);
|
||||||
|
|
||||||
// Write the SPS data
|
// Write the SPS data
|
||||||
savedSps.write(inputBuffer);
|
savedSps.write(inputBuffer);
|
||||||
|
|
||||||
@@ -568,7 +637,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Queue the new SPS
|
// Queue the new SPS
|
||||||
queueInputBuffer(inputIndex,
|
queueInputBuffer(inputIndex,
|
||||||
0, inputBuffer.position(),
|
0, inputBuffer.position(),
|
||||||
System.currentTimeMillis() * 1000,
|
System.nanoTime() / 1000,
|
||||||
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||||
|
|
||||||
LimeLog.info("SPS replay complete");
|
LimeLog.info("SPS replay complete");
|
||||||
@@ -609,7 +678,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void notifyDuReceived(DecodeUnit du) {
|
private void notifyDuReceived(DecodeUnit du) {
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
long delta = currentTime-du.getReceiveTimestamp();
|
long delta = currentTime-du.getReceiveTimestamp();
|
||||||
if (delta >= 0 && delta < 1000) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
totalTimeMs += currentTime-du.getReceiveTimestamp();
|
totalTimeMs += currentTime-du.getReceiveTimestamp();
|
||||||
@@ -633,7 +702,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inputIndex >= 0) {
|
if (inputIndex >= 0) {
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, inputIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,6 +733,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||||
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
|
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
|
||||||
str += "Total frames: "+renderer.totalFrames+"\n";
|
str += "Total frames: "+renderer.totalFrames+"\n";
|
||||||
|
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
|
||||||
|
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
|
||||||
|
|
||||||
if (currentBuffer != null) {
|
if (currentBuffer != null) {
|
||||||
str += "Current buffer: ";
|
str += "Current buffer: ";
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class MediaCodecHelper {
|
|||||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
private static final List<String> baselineProfileHackPrefixes;
|
||||||
private static final List<String> directSubmitPrefixes;
|
private static final List<String> directSubmitPrefixes;
|
||||||
|
private static final List<String> constrainedHighProfilePrefixes;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
directSubmitPrefixes = new LinkedList<String>();
|
directSubmitPrefixes = new LinkedList<String>();
|
||||||
@@ -39,6 +40,7 @@ public class MediaCodecHelper {
|
|||||||
directSubmitPrefixes.add("omx.intel");
|
directSubmitPrefixes.add("omx.intel");
|
||||||
directSubmitPrefixes.add("omx.brcm");
|
directSubmitPrefixes.add("omx.brcm");
|
||||||
directSubmitPrefixes.add("omx.TI");
|
directSubmitPrefixes.add("omx.TI");
|
||||||
|
directSubmitPrefixes.add("omx.arc");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -57,7 +59,6 @@ public class MediaCodecHelper {
|
|||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||||
|
|
||||||
baselineProfileHackPrefixes = new LinkedList<String>();
|
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||||
@@ -68,6 +69,9 @@ public class MediaCodecHelper {
|
|||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
|
|
||||||
|
constrainedHighProfilePrefixes = new LinkedList<String>();
|
||||||
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
@@ -82,6 +86,10 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long getMonotonicMillis() {
|
||||||
|
return System.nanoTime() / 1000000L;
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
@@ -112,6 +120,10 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderNeedsConstrainedHighProfile(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ import android.os.IBinder;
|
|||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
public class ComputerManagerService extends Service {
|
public class ComputerManagerService extends Service {
|
||||||
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
|
private static final int SERVERINFO_POLLING_PERIOD_MS = 1500;
|
||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 500;
|
private static final int FAST_POLL_TIMEOUT = 500;
|
||||||
private static final int OFFLINE_POLL_TRIES = 3;
|
private static final int OFFLINE_POLL_TRIES = 5;
|
||||||
|
|
||||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (!pollComputer(details)) {
|
if (!pollComputer(details)) {
|
||||||
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
||||||
// Return without calling the listener
|
// Return without calling the listener
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +120,7 @@ public class ComputerManagerService extends Service {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Thread createPollingThread(final ComputerDetails details) {
|
private Thread createPollingThread(final PollingTuple tuple) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -126,24 +128,26 @@ public class ComputerManagerService extends Service {
|
|||||||
int offlineCount = 0;
|
int offlineCount = 0;
|
||||||
while (!isInterrupted() && pollingActive) {
|
while (!isInterrupted() && pollingActive) {
|
||||||
try {
|
try {
|
||||||
// Check if this poll has modified the details
|
// Only allow one request to the machine at a time
|
||||||
if (!runPoll(details, false, offlineCount)) {
|
synchronized (tuple.networkLock) {
|
||||||
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
|
// Check if this poll has modified the details
|
||||||
offlineCount++;
|
if (!runPoll(tuple.computer, false, offlineCount)) {
|
||||||
}
|
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
||||||
else {
|
offlineCount++;
|
||||||
offlineCount = 0;
|
} else {
|
||||||
|
offlineCount = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until the next polling interval
|
// Wait until the next polling interval
|
||||||
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for "+details.localIp.getHostAddress());
|
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +169,7 @@ public class ComputerManagerService extends Service {
|
|||||||
// Report this computer initially
|
// Report this computer initially
|
||||||
listener.notifyComputerUpdated(tuple.computer);
|
listener.notifyComputerUpdated(tuple.computer);
|
||||||
|
|
||||||
tuple.thread = createPollingThread(tuple.computer);
|
tuple.thread = createPollingThread(tuple);
|
||||||
tuple.thread.start();
|
tuple.thread.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +286,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// Start a polling thread if polling is active
|
// Start a polling thread if polling is active
|
||||||
if (pollingActive && tuple.thread == null) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
tuple.thread = createPollingThread(details);
|
tuple.thread = createPollingThread(tuple);
|
||||||
tuple.thread.start();
|
tuple.thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +296,10 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we got here, we didn't find an entry
|
// If we got here, we didn't find an entry
|
||||||
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
|
PollingTuple tuple = new PollingTuple(details, null);
|
||||||
|
if (pollingActive) {
|
||||||
|
tuple.thread = createPollingThread(tuple);
|
||||||
|
}
|
||||||
pollingTuples.add(tuple);
|
pollingTuples.add(tuple);
|
||||||
if (tuple.thread != null) {
|
if (tuple.thread != null) {
|
||||||
tuple.thread.start();
|
tuple.thread.start();
|
||||||
@@ -367,6 +374,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
||||||
|
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||||
|
if (!fastPollIp(ipAddr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
@@ -392,7 +404,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private boolean fastPollIp(InetAddress addr) {
|
private boolean fastPollIp(InetAddress addr) {
|
||||||
Socket s = new Socket();
|
Socket s = new Socket();
|
||||||
try {
|
try {
|
||||||
s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT);
|
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
s.close();
|
s.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -450,7 +462,7 @@ public class ComputerManagerService extends Service {
|
|||||||
return ComputerDetails.Reachability.OFFLINE;
|
return ComputerDetails.Reachability.OFFLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||||
ComputerDetails polledDetails;
|
ComputerDetails polledDetails;
|
||||||
ComputerDetails.Reachability reachability;
|
ComputerDetails.Reachability reachability;
|
||||||
|
|
||||||
@@ -464,11 +476,11 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||||
}
|
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
return false;
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
@@ -480,6 +492,7 @@ public class ComputerManagerService extends Service {
|
|||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InetAddress reachableAddr = null;
|
||||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||||
// Failed, so let's try the fallback
|
// Failed, so let's try the fallback
|
||||||
if (!localFirst) {
|
if (!localFirst) {
|
||||||
@@ -489,27 +502,59 @@ public class ComputerManagerService extends Service {
|
|||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fallback poll worked
|
|
||||||
if (polledDetails != null) {
|
if (polledDetails != null) {
|
||||||
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
// The fallback poll worked
|
||||||
ComputerDetails.Reachability.REMOTE;
|
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (polledDetails != null) {
|
else if (polledDetails != null) {
|
||||||
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
||||||
ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Machine was unreachable both tries
|
if (reachableAddr == null) {
|
||||||
if (polledDetails == null) {
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
||||||
|
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
else if (polledDetails.localIp.equals(reachableAddr)) {
|
||||||
|
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReachabilityTuple(polledDetails, reachableAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
|
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
||||||
|
if (initialReachTuple == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
|
// Neither IP address reported in the serverinfo response was the one we used.
|
||||||
|
// Poll again to see if we can contact this machine on either of its reported addresses.
|
||||||
|
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||||
|
if (confirmationReachTuple == null) {
|
||||||
|
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||||
|
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
||||||
|
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We got it on one of the returned addresses; replace the original reach tuple
|
||||||
|
// with the new one
|
||||||
|
initialReachTuple = confirmationReachTuple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save the old MAC address
|
// Save the old MAC address
|
||||||
String savedMacAddress = details.macAddress;
|
String savedMacAddress = details.macAddress;
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(polledDetails);
|
details.update(initialReachTuple.computer);
|
||||||
|
|
||||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||||
@@ -568,6 +613,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private Thread thread;
|
private Thread thread;
|
||||||
private final ComputerDetails computer;
|
private final ComputerDetails computer;
|
||||||
private final Object pollEvent = new Object();
|
private final Object pollEvent = new Object();
|
||||||
|
private boolean receivedAppList = false;
|
||||||
|
|
||||||
public ApplistPoller(ComputerDetails computer) {
|
public ApplistPoller(ComputerDetails computer) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
@@ -582,7 +628,15 @@ public class ComputerManagerService extends Service {
|
|||||||
private boolean waitPollingDelay() {
|
private boolean waitPollingDelay() {
|
||||||
try {
|
try {
|
||||||
synchronized (pollEvent) {
|
synchronized (pollEvent) {
|
||||||
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
|
if (receivedAppList) {
|
||||||
|
// If we've already reported an app list successfully,
|
||||||
|
// wait the full polling period
|
||||||
|
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we've failed to get an app list so far, retry much earlier
|
||||||
|
pollEvent.wait(APPLIST_FAILED_POLLING_RETRY_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
return false;
|
return false;
|
||||||
@@ -591,6 +645,18 @@ public class ComputerManagerService extends Service {
|
|||||||
return thread != null && !thread.isInterrupted();
|
return thread != null && !thread.isInterrupted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PollingTuple getPollingTuple(ComputerDetails details) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (details.uuid.equals(tuple.computer.uuid)) {
|
||||||
|
return tuple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
thread = new Thread() {
|
thread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
@@ -621,9 +687,23 @@ public class ComputerManagerService extends Service {
|
|||||||
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
|
PollingTuple tuple = getPollingTuple(computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Query the app list from the server
|
String appList;
|
||||||
String appList = http.getAppListRaw();
|
if (tuple != null) {
|
||||||
|
// If we're polling this machine too, grab the network lock
|
||||||
|
// while doing the app list request to prevent other requests
|
||||||
|
// from being issued in the meantime.
|
||||||
|
synchronized (tuple.networkLock) {
|
||||||
|
appList = http.getAppListRaw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No polling is happening now, so we just call it directly
|
||||||
|
appList = http.getAppListRaw();
|
||||||
|
}
|
||||||
|
|
||||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
@@ -638,11 +718,12 @@ public class ComputerManagerService extends Service {
|
|||||||
if (cacheOut != null) {
|
if (cacheOut != null) {
|
||||||
cacheOut.close();
|
cacheOut.close();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the computer
|
// Update the computer
|
||||||
computer.rawAppList = appList;
|
computer.rawAppList = appList;
|
||||||
|
receivedAppList = true;
|
||||||
|
|
||||||
// Notify that the app list has been updated
|
// Notify that the app list has been updated
|
||||||
// and ensure that the thread is still active
|
// and ensure that the thread is still active
|
||||||
@@ -679,9 +760,21 @@ public class ComputerManagerService extends Service {
|
|||||||
class PollingTuple {
|
class PollingTuple {
|
||||||
public Thread thread;
|
public Thread thread;
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
|
public final Object networkLock;
|
||||||
|
|
||||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.thread = thread;
|
this.thread = thread;
|
||||||
|
this.networkLock = new Object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReachabilityTuple {
|
||||||
|
public final InetAddress reachableAddress;
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
|
||||||
|
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.reachableAddress = reachableAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
|
|
||||||
private final CachedAppAssetLoader loader;
|
private final CachedAppAssetLoader loader;
|
||||||
|
|
||||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
|
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||||
|
|
||||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||||
@@ -40,7 +40,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
dp = LARGE_WIDTH_DP;
|
dp = LARGE_WIDTH_DP;
|
||||||
}
|
}
|
||||||
|
|
||||||
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
|
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
||||||
if (scalingDivisor < 1.0) {
|
if (scalingDivisor < 1.0) {
|
||||||
// We don't want to make them bigger before draw-time
|
// We don't want to make them bigger before draw-time
|
||||||
scalingDivisor = 1.0;
|
scalingDivisor = 1.0;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.widget.ImageView;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
@@ -97,9 +98,18 @@ public class CachedAppAssetLoader {
|
|||||||
// Write the stream straight to disk
|
// Write the stream straight to disk
|
||||||
diskLoader.populateCacheWithStream(tuple, in);
|
diskLoader.populateCacheWithStream(tuple, in);
|
||||||
|
|
||||||
|
// Close the network input stream
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
// If there's a task associated with this load, we should return the bitmap
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp != null) {
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Otherwise it's a background load and we return nothing
|
// Otherwise it's a background load and we return nothing
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class DiskAssetLoader {
|
public class DiskAssetLoader {
|
||||||
|
// 5 MB
|
||||||
|
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
private final File cacheDir;
|
private final File cacheDir;
|
||||||
|
|
||||||
public DiskAssetLoader(File cacheDir) {
|
public DiskAssetLoader(File cacheDir) {
|
||||||
@@ -27,13 +30,19 @@ public class DiskAssetLoader {
|
|||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
Bitmap bmp = null;
|
Bitmap bmp = null;
|
||||||
try {
|
try {
|
||||||
|
// Make sure the cached asset doesn't exceed the maximum size
|
||||||
|
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
||||||
|
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||||
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
options.inSampleSize = sampleSize;
|
options.inSampleSize = sampleSize;
|
||||||
|
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||||
} catch (FileNotFoundException ignored) {
|
} catch (IOException ignored) {
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
} finally {
|
||||||
if (in != null) {
|
if (in != null) {
|
||||||
try {
|
try {
|
||||||
@@ -51,9 +60,11 @@ public class DiskAssetLoader {
|
|||||||
|
|
||||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
OutputStream out = null;
|
OutputStream out = null;
|
||||||
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
CacheHelper.writeInputStreamToOutputStream(input, out);
|
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||||
|
success = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -62,6 +73,11 @@ public class DiskAssetLoader {
|
|||||||
out.close();
|
out.close();
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||||
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ public class NetworkAssetLoader {
|
|||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
try {
|
try {
|
||||||
in = http.getBoxArt(tuple.app);
|
in = http.getBoxArt(tuple.app);
|
||||||
} catch (IOException e) {}
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
if (in != null) {
|
if (in != null) {
|
||||||
LimeLog.info("Network asset load complete: " + tuple);
|
LimeLog.info("Network asset load complete: " + tuple);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.limelight.preferences;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
@@ -69,9 +70,18 @@ public class PreferenceConfiguration {
|
|||||||
|
|
||||||
public static boolean getDefaultSmallMode(Context context) {
|
public static boolean getDefaultSmallMode(Context context) {
|
||||||
PackageManager manager = context.getPackageManager();
|
PackageManager manager = context.getPackageManager();
|
||||||
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
if (manager != null) {
|
||||||
// TVs shouldn't use small mode by default
|
// TVs shouldn't use small mode by default
|
||||||
return false;
|
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 21 uses LEANBACK instead of TELEVISION
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use small mode on anything smaller than a 7" tablet
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
@@ -80,24 +90,7 @@ public class PreferenceConfiguration {
|
|||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
|
||||||
if (str.equals("720p30")) {
|
|
||||||
return BITRATE_DEFAULT_720_30;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p60")) {
|
|
||||||
return BITRATE_DEFAULT_720_60;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p30")) {
|
|
||||||
return BITRATE_DEFAULT_1080_30;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p60")) {
|
|
||||||
return BITRATE_DEFAULT_1080_60;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
return DEFAULT_BITRATE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getDecoderValue(Context context) {
|
private static int getDecoderValue(Context context) {
|
||||||
|
|||||||
@@ -16,14 +16,17 @@ import com.limelight.utils.UiHelper;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
|
||||||
|
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
config.locale = new Locale(locale);
|
config.locale = new Locale(previousPrefs.language);
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +42,16 @@ public class StreamSettings extends Activity {
|
|||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
finish();
|
finish();
|
||||||
|
|
||||||
// Restart the PC view to apply UI changes
|
// Check for changes that require a UI reload to take effect
|
||||||
Intent intent = new Intent(this, PcView.class);
|
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
if (newPrefs.listMode != previousPrefs.listMode ||
|
||||||
startActivity(intent, null);
|
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
|
||||||
|
!newPrefs.language.equals(previousPrefs.language)) {
|
||||||
|
// Restart the PC view to apply UI changes
|
||||||
|
Intent intent = new Intent(this, PcView.class);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ public class CacheHelper {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long getFileSize(File root, String... path) {
|
||||||
|
return openPath(false, root, path).length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteCacheFile(File root, String... path) {
|
||||||
|
return openPath(false, root, path).delete();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean cacheFileExists(File root, String... path) {
|
public static boolean cacheFileExists(File root, String... path) {
|
||||||
return openPath(false, root, path).exists();
|
return openPath(false, root, path).exists();
|
||||||
}
|
}
|
||||||
@@ -42,11 +50,15 @@ public class CacheHelper {
|
|||||||
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
|
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out, long maxLength) throws IOException {
|
||||||
byte[] buf = new byte[4096];
|
byte[] buf = new byte[4096];
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
|
|
||||||
while ((bytesRead = in.read(buf)) != -1) {
|
while ((bytesRead = in.read(buf)) != -1) {
|
||||||
|
maxLength -= bytesRead;
|
||||||
|
if (maxLength <= 0) {
|
||||||
|
throw new IOException("Stream exceeded max size");
|
||||||
|
}
|
||||||
out.write(buf, 0, bytesRead);
|
out.write(buf, 0, bytesRead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,6 +73,10 @@ public class CacheHelper {
|
|||||||
sb.append(buf, 0, bytesRead);
|
sb.append(buf, 0, bytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <opus.h>
|
#include <opus_multistream.h>
|
||||||
#include "nv_opus_dec.h"
|
#include "nv_opus_dec.h"
|
||||||
|
|
||||||
OpusDecoder* decoder;
|
OpusMSDecoder* decoder;
|
||||||
|
|
||||||
// This function must be called before
|
// This function must be called before
|
||||||
// any other decoding functions
|
// any other decoding functions
|
||||||
int nv_opus_init(void) {
|
int nv_opus_init(int sampleRate, int channelCount, int streams,
|
||||||
|
int coupledStreams, const unsigned char *mapping) {
|
||||||
int err;
|
int err;
|
||||||
decoder = opus_decoder_create(
|
decoder = opus_multistream_decoder_create(
|
||||||
nv_opus_get_sample_rate(),
|
sampleRate,
|
||||||
nv_opus_get_channel_count(),
|
channelCount,
|
||||||
&err);
|
streams,
|
||||||
|
coupledStreams,
|
||||||
|
mapping,
|
||||||
|
&err);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,36 +23,20 @@ int nv_opus_init(void) {
|
|||||||
// decoding is finished
|
// decoding is finished
|
||||||
void nv_opus_destroy(void) {
|
void nv_opus_destroy(void) {
|
||||||
if (decoder != NULL) {
|
if (decoder != NULL) {
|
||||||
opus_decoder_destroy(decoder);
|
opus_multistream_decoder_destroy(decoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Opus stream is stereo
|
|
||||||
int nv_opus_get_channel_count(void) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This number assumes 16-bit samples at 48 KHz with 2.5 ms frames
|
|
||||||
int nv_opus_get_max_out_shorts(void) {
|
|
||||||
return 240*nv_opus_get_channel_count();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Opus stream is 48 KHz
|
|
||||||
int nv_opus_get_sample_rate(void) {
|
|
||||||
return 48000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// outpcmdata must be 5760*2 shorts in length
|
|
||||||
// packets must be decoded in order
|
// packets must be decoded in order
|
||||||
// a packet loss must call this function with NULL indata and 0 inlen
|
// a packet loss must call this function with NULL indata and 0 inlen
|
||||||
// returns the number of decoded samples
|
// returns the number of decoded samples
|
||||||
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata) {
|
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata, int framesize) {
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
// Decoding to 16-bit PCM with FEC off
|
// Decoding to 16-bit PCM with FEC off
|
||||||
// Maximum length assuming 48KHz sample rate
|
// Maximum length assuming 48KHz sample rate
|
||||||
err = opus_decode(decoder, indata, inlen,
|
err = opus_multistream_decode(decoder, indata, inlen,
|
||||||
outpcmdata, 512, 0);
|
outpcmdata, framesize, 0);
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
int nv_opus_init(void);
|
int nv_opus_init(int sampleRate, int channelCount, int streams,
|
||||||
|
int coupledStreams, const unsigned char *mapping);
|
||||||
void nv_opus_destroy(void);
|
void nv_opus_destroy(void);
|
||||||
int nv_opus_get_channel_count(void);
|
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata, int framesize);
|
||||||
int nv_opus_get_max_out_shorts(void);
|
|
||||||
int nv_opus_get_sample_rate(void);
|
|
||||||
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata);
|
|
||||||
@@ -3,11 +3,26 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
|
static int SamplesPerChannel;
|
||||||
|
static int ChannelCount;
|
||||||
|
|
||||||
// This function must be called before
|
// This function must be called before
|
||||||
// any other decoding functions
|
// any other decoding functions
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_init(JNIEnv *env, jobject this) {
|
Java_com_limelight_nvstream_av_audio_OpusDecoder_init(JNIEnv *env, jobject this, int sampleRate,
|
||||||
return nv_opus_init();
|
int samplesPerChannel, int channelCount, int streams,
|
||||||
|
int coupledStreams, jbyteArray mapping) {
|
||||||
|
jbyte* jni_mapping_data;
|
||||||
|
jint ret;
|
||||||
|
|
||||||
|
SamplesPerChannel = samplesPerChannel;
|
||||||
|
ChannelCount = channelCount;
|
||||||
|
|
||||||
|
jni_mapping_data = (*env)->GetByteArrayElements(env, mapping, 0);
|
||||||
|
ret = nv_opus_init(sampleRate, channelCount, streams, coupledStreams, jni_mapping_data);
|
||||||
|
(*env)->ReleaseByteArrayElements(env, mapping, jni_mapping_data, JNI_ABORT);
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function must be called after
|
// This function must be called after
|
||||||
@@ -17,28 +32,9 @@ Java_com_limelight_nvstream_av_audio_OpusDecoder_destroy(JNIEnv *env, jobject th
|
|||||||
nv_opus_destroy();
|
nv_opus_destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Opus stream is stereo
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_getChannelCount(JNIEnv *env, jobject this) {
|
|
||||||
return nv_opus_get_channel_count();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This number assumes 2 channels at 48 KHz
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_getMaxOutputShorts(JNIEnv *env, jobject this) {
|
|
||||||
return nv_opus_get_max_out_shorts();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Opus stream is 48 KHz
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_getSampleRate(JNIEnv *env, jobject this) {
|
|
||||||
return nv_opus_get_sample_rate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// outpcmdata must be 5760*2 shorts in length
|
|
||||||
// packets must be decoded in order
|
// packets must be decoded in order
|
||||||
// a packet loss must call this function with NULL indata and 0 inlen
|
// a packet loss must call this function with NULL indata and 0 inlen
|
||||||
// returns the number of decoded samples
|
// returns the number of decoded bytes
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
|
Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
|
||||||
JNIEnv *env, jobject this, // JNI parameters
|
JNIEnv *env, jobject this, // JNI parameters
|
||||||
@@ -53,13 +49,18 @@ Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
|
|||||||
if (indata != NULL) {
|
if (indata != NULL) {
|
||||||
jni_input_data = (*env)->GetByteArrayElements(env, indata, 0);
|
jni_input_data = (*env)->GetByteArrayElements(env, indata, 0);
|
||||||
|
|
||||||
ret = nv_opus_decode(&jni_input_data[inoff], inlen, (jshort*)jni_pcm_data);
|
ret = nv_opus_decode(&jni_input_data[inoff], inlen, (jshort*)jni_pcm_data, SamplesPerChannel);
|
||||||
|
|
||||||
// The input data isn't changed so it can be safely aborted
|
// The input data isn't changed so it can be safely aborted
|
||||||
(*env)->ReleaseByteArrayElements(env, indata, jni_input_data, JNI_ABORT);
|
(*env)->ReleaseByteArrayElements(env, indata, jni_input_data, JNI_ABORT);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ret = nv_opus_decode(NULL, 0, (jshort*)jni_pcm_data);
|
ret = nv_opus_decode(NULL, 0, (jshort*)jni_pcm_data, SamplesPerChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert samples (2 bytes) per channel to total bytes returned
|
||||||
|
if (ret > 0) {
|
||||||
|
ret *= ChannelCount * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
(*env)->ReleaseByteArrayElements(env, outpcmdata, jni_pcm_data, 0);
|
(*env)->ReleaseByteArrayElements(env, outpcmdata, jni_pcm_data, 0);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 65 KiB |
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
<string name="category_ui_settings">Impostazioni Interfaccia</string>
|
<string name="category_ui_settings">Impostazioni Interfaccia</string>
|
||||||
<string name="title_language_list">Lingua</string>
|
<string name="title_language_list">Lingua</string>
|
||||||
<string name="summary_language_list">Lingua da usare in Limelight</string>
|
<string name="summary_language_list">Lingua da usare in Moonlight</string>
|
||||||
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
|
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
|
||||||
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
|
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
|
||||||
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
|
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
<string name="category_ui_settings">UI Settings</string>
|
<string name="category_ui_settings">UI Settings</string>
|
||||||
<string name="title_language_list">Language</string>
|
<string name="title_language_list">Language</string>
|
||||||
<string name="summary_language_list">Language to use for Limelight</string>
|
<string name="summary_language_list">Language to use for Moonlight</string>
|
||||||
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
|
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
|
||||||
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
|
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
|
||||||
<string name="title_checkbox_small_icon_mode">Use small icons</string>
|
<string name="title_checkbox_small_icon_mode">Use small icons</string>
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- Non-root application name -->
|
<!-- Non-root application name -->
|
||||||
<application android:label="Limelight" />
|
<application android:label="Moonlight" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -5,5 +5,5 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
|
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
|
||||||
|
|
||||||
<!-- Root application name -->
|
<!-- Root application name -->
|
||||||
<application android:label="Limelight (Root)" />
|
<application android:label="Moonlight (Root)" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:1.1.0'
|
classpath 'com.android.tools.build:gradle:1.3.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ This file serves to document some of the decoder errata when using MediaCodec ha
|
|||||||
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
|
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
|
||||||
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
|
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
|
||||||
|
|
||||||
5. Some decoders will hang if max_dec_frame_buffering is not present
|
5. Some decoders will hang or crash if max_dec_frame_buffering is not present and level_idc is >= 50
|
||||||
- Affected decoders: MediaTek decoder in Fire HD 7 (2014)
|
- Affected decoders: MediaTek decoder in Fire HD 6/7 (2014)
|
||||||
|
|
||||||
6. Some decoders will hang if max_dec_frame_buffering IS present
|
6. Some decoders will hang if max_dec_frame_buffering IS present
|
||||||
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
|
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
|
||||||
@@ -21,5 +21,11 @@ This file serves to document some of the decoder errata when using MediaCodec ha
|
|||||||
7. Some decoders will not enter low latency mode if adaptive playback is enabled
|
7. Some decoders will not enter low latency mode if adaptive playback is enabled
|
||||||
- Affected decoders: Intel decoder in Nexus Player
|
- Affected decoders: Intel decoder in Nexus Player
|
||||||
|
|
||||||
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS.
|
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS because B-frames may be present.
|
||||||
- Affected decoders: Intel decoder in Nexus Player
|
- Affected decoders: Intel decoder in Nexus Player (prior to Android 6.0)
|
||||||
|
|
||||||
|
9. Some decoders will not enter low latency mode if the profile isn't constrained high profile because B-frames may be present.
|
||||||
|
- Affected decoders: Intel decoder in Nexus Player (after Android 6.0)
|
||||||
|
|
||||||
|
10. Some decoders actually suffer increased latency when max_dec_frame_buffering=1
|
||||||
|
- Affected decoders: MediaTek decoder in Fire TV 2015
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||||
<component name="FacetManager">
|
<component name="FacetManager">
|
||||||
<facet type="java-gradle" name="Java-Gradle">
|
<facet type="java-gradle" name="Java-Gradle">
|
||||||
<configuration>
|
<configuration>
|
||||||
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
||||||
|
<option name="BUILDABLE" value="false" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</facet>
|
</facet>
|
||||||
</component>
|
</component>
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||||
@@ -15,5 +16,4 @@
|
|||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||