mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 15:27:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eac6998e17 | ||
|
|
17afbffdb5 | ||
|
|
072a439c2d | ||
|
|
c533600983 | ||
|
|
5847fbb6b6 | ||
|
|
1876b30c1b | ||
|
|
5c71f55993 | ||
|
|
9c0960d03d | ||
|
|
29a395f3f4 | ||
|
|
a676b8d8e6 | ||
|
|
7ab0be3b62 | ||
|
|
115853fed2 | ||
|
|
60beb81ae4 | ||
|
|
5310375d42 | ||
|
|
7ce29e3a09 | ||
|
|
42c65f4f16 | ||
|
|
bf2cc2a4d5 | ||
|
|
6d6d7121f6 | ||
|
|
2ab67380d6 | ||
|
|
899387caa1 | ||
|
|
56c8a9e6fe | ||
|
|
896288a40b | ||
|
|
fc8ce5e4b9 |
No files matched your search
+1
-1
@@ -103,7 +103,7 @@
|
|||||||
<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 21 Platform" jdkType="Android SDK" />
|
<orderEntry type="jdk" jdkName="Android API 22 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.51" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
||||||
|
|||||||
+4
-4
@@ -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 21
|
compileSdkVersion 22
|
||||||
buildToolsVersion "21.1.2"
|
buildToolsVersion "21.1.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 21
|
targetSdkVersion 22
|
||||||
|
|
||||||
versionName "3.1.2"
|
versionName "3.1.3"
|
||||||
versionCode = 56
|
versionCode = 58
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
|
|||||||
Binary file not shown.
@@ -21,6 +21,7 @@ import com.limelight.ui.AdapterFragment;
|
|||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
@@ -54,6 +55,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private ComputerManagerService.ApplistPoller poller;
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
private String lastRawApplist;
|
private String lastRawApplist;
|
||||||
|
private int lastRunningAppId;
|
||||||
|
private boolean suspendGridUpdates;
|
||||||
|
|
||||||
private final static int START_OR_RESUME_ID = 1;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
@@ -111,11 +114,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private InetAddress getAddress() {
|
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp : computer.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
return;
|
return;
|
||||||
@@ -124,6 +122,11 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerUpdated(ComputerDetails details) {
|
public void notifyComputerUpdated(ComputerDetails details) {
|
||||||
|
// Do nothing if updates are suspended
|
||||||
|
if (suspendGridUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't care about other computers
|
// Don't care about other computers
|
||||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||||
return;
|
return;
|
||||||
@@ -143,13 +146,23 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App list is the same or empty; nothing to do
|
// App list is the same or empty
|
||||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
|
||||||
|
// Let's check if the running app ID changed
|
||||||
|
if (details.runningGameId != lastRunningAppId) {
|
||||||
|
// Update the currently running game using the app ID
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
lastRawApplist = details.rawAppList;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
lastRawApplist = details.rawAppList;
|
|
||||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
if (blockingLoadSpinner != null) {
|
||||||
@@ -291,33 +304,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
|
|
||||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
switch (which){
|
|
||||||
case DialogInterface.BUTTON_POSITIVE:
|
|
||||||
if (onYes != null) {
|
|
||||||
onYes.run();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DialogInterface.BUTTON_NEGATIVE:
|
|
||||||
if (onNo != null) {
|
|
||||||
onNo.run();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
||||||
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
|
|
||||||
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
|
|
||||||
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
@@ -325,25 +311,37 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case START_WTIH_QUIT:
|
case START_WTIH_QUIT:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
displayQuitConfirmationDialog(new Runnable() {
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
doStart(app.app);
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case START_OR_RESUME_ID:
|
case START_OR_RESUME_ID:
|
||||||
// Resume is the same as start for us
|
// Resume is the same as start for us
|
||||||
doStart(app.app);
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
displayQuitConfirmationDialog(new Runnable() {
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
doQuit(app.app);
|
suspendGridUpdates = true;
|
||||||
|
ServerHelper.doQuit(AppView.this,
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
|
app.app, managerBinder, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Trigger a poll immediately
|
||||||
|
suspendGridUpdates = false;
|
||||||
|
if (poller != null) {
|
||||||
|
poller.pollNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
@@ -356,6 +354,44 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateUiWithServerinfo(final ComputerDetails details) {
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean updated = false;
|
||||||
|
|
||||||
|
// Look through our current app list to tag the running app
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// There can only be one or zero apps running.
|
||||||
|
if (existingApp.app.getIsRunning() &&
|
||||||
|
existingApp.app.getAppId() == details.runningGameId) {
|
||||||
|
// This app was running and still is, so we're done now
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (existingApp.app.getAppId() == details.runningGameId) {
|
||||||
|
// This app wasn't running but now is
|
||||||
|
existingApp.app.setIsRunning(true);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
else if (existingApp.app.getIsRunning()) {
|
||||||
|
// This app was running but now isn't
|
||||||
|
existingApp.app.setIsRunning(false);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This app wasn't running and still isn't
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
appGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void updateUiWithAppList(final List<NvApp> appList) {
|
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -427,67 +463,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doStart(NvApp app) {
|
|
||||||
Intent intent = new Intent(this, Game.class);
|
|
||||||
intent.putExtra(Game.EXTRA_HOST,
|
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doQuit(final NvApp app) {
|
|
||||||
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
httpConn = new NvHTTP(getAddress(),
|
|
||||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
|
|
||||||
if (httpConn.quitApp()) {
|
|
||||||
message = getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
|
||||||
} else {
|
|
||||||
message = getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
|
||||||
}
|
|
||||||
} catch (GfeHttpResponseException e) {
|
|
||||||
if (e.getErrorCode() == 599) {
|
|
||||||
message = "This session wasn't started by this device," +
|
|
||||||
" so it cannot be quit. End streaming on the original " +
|
|
||||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = getResources().getString(R.string.error_unknown_host);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = getResources().getString(R.string.error_404);
|
|
||||||
} catch (Exception e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
} finally {
|
|
||||||
// Trigger a poll immediately
|
|
||||||
if (poller != null) {
|
|
||||||
poller.pollNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
@@ -508,7 +483,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (getRunningAppId() != -1) {
|
if (getRunningAppId() != -1) {
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else {
|
} else {
|
||||||
doStart(app.app);
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.limelight.computers.ComputerManagerListener;
|
|||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.PcGridAdapter;
|
import com.limelight.grid.PcGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
@@ -22,6 +23,7 @@ import com.limelight.preferences.StreamSettings;
|
|||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -93,6 +95,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
private final static int DELETE_ID = 5;
|
private final static int DELETE_ID = 5;
|
||||||
|
private final static int RESUME_ID = 6;
|
||||||
|
private final static int QUIT_ID = 7;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -252,11 +256,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
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));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
|
if (computer.details.runningGameId != 0) {
|
||||||
|
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
|
|
||||||
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
||||||
// it with delete which actually work
|
// it with delete which actually work
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,36 +486,61 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId()) {
|
||||||
{
|
case PAIR_ID:
|
||||||
case PAIR_ID:
|
doPair(computer.details);
|
||||||
doPair(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case UNPAIR_ID:
|
|
||||||
doUnpair(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case WOL_ID:
|
|
||||||
doWakeOnLan(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case DELETE_ID:
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
managerBinder.removeComputer(computer.details.name);
|
|
||||||
removeComputer(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case UNPAIR_ID:
|
||||||
doAppList(computer.details);
|
doUnpair(computer.details);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
case WOL_ID:
|
||||||
return super.onContextItemSelected(item);
|
doWakeOnLan(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case DELETE_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
managerBinder.removeComputer(computer.details.name);
|
||||||
|
removeComputer(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case APP_LIST_ID:
|
||||||
|
doAppList(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case RESUME_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case QUIT_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ServerHelper.doQuit(PcView.this,
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||||
|
new NvApp("app", 0), managerBinder, null);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,10 +277,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
context.isRemote = true;
|
context.isRemote = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// NYKO Playpad has a fake hat that mimics the left stick for some reason
|
// SHIELD controllers will use small stick deadzones
|
||||||
else if (devName.contains("NYKO PLAYPAD")) {
|
else if (devName.contains("SHIELD")) {
|
||||||
context.hatXAxis = -1;
|
context.leftStickDeadzoneRadius = 0.07f;
|
||||||
context.hatYAxis = -1;
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
// This is the back button on Shield portable consoles
|
// This is the back button on Shield portable consoles
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
}
|
}
|
||||||
else if (multiControllerEnabled) {
|
else if (multiControllerEnabled && context.hasJoystickAxes) {
|
||||||
context.controllerNumber = assignNewControllerNumber();
|
context.controllerNumber = assignNewControllerNumber();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -231,7 +231,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||||
decoderBuffer.clear();
|
decoderBuffer.clear();
|
||||||
|
|
||||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
||||||
|
bbd != null; bbd = bbd.nextDescriptor) {
|
||||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +242,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||||
|
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
||||||
|
bbd != null; bbd = bbd.nextDescriptor) {
|
||||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||||
offset += bbd.length;
|
offset += bbd.length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return decoderRenderer.getCapabilities();
|
return decoderRenderer.getCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void directSubmitDecodeUnit(DecodeUnit du) {
|
||||||
|
decoderRenderer.directSubmitDecodeUnit(du);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAverageDecoderLatency() {
|
public int getAverageDecoderLatency() {
|
||||||
if (decoderRenderer != null) {
|
if (decoderRenderer != null) {
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
|
|||||||
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
|
||||||
public abstract String getDecoderName();
|
public abstract String getDecoderName();
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private final boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private VideoDepacketizer depacketizer;
|
private VideoDepacketizer depacketizer;
|
||||||
private boolean adaptivePlayback;
|
private final boolean adaptivePlayback, directSubmit;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
@@ -46,8 +46,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
private int numIframeIn;
|
private int numIframeIn;
|
||||||
|
|
||||||
private static final boolean ENABLE_ASYNC_RENDERER = false;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public MediaCodecDecoderRenderer() {
|
public MediaCodecDecoderRenderer() {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
@@ -58,12 +56,15 @@ 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 =
|
||||||
|
adaptivePlayback = directSubmit = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoderName = decoder.getName();
|
decoderName = decoder.getName();
|
||||||
|
|
||||||
// Set decoder-specific attributes
|
// Set decoder-specific attributes
|
||||||
|
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
|
||||||
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);
|
||||||
@@ -77,9 +78,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (isExynos4) {
|
if (isExynos4) {
|
||||||
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
||||||
}
|
}
|
||||||
|
if (directSubmit) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" will use direct submit");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.initialWidth = width;
|
this.initialWidth = width;
|
||||||
@@ -107,52 +110,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
|
|
||||||
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
videoDecoder.setCallback(new MediaCodec.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
|
||||||
LimeLog.info("Output format changed");
|
|
||||||
LimeLog.info("New output Format: " + format);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOutputBufferAvailable(MediaCodec codec, int index,
|
|
||||||
BufferInfo info) {
|
|
||||||
try {
|
|
||||||
// FIXME: It looks like we can't frameskip here
|
|
||||||
codec.releaseOutputBuffer(index, true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
|
||||||
try {
|
|
||||||
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// What do we do here?
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(MediaCodec codec, CodecException e) {
|
|
||||||
if (e.isTransient()) {
|
|
||||||
LimeLog.warning(e.getDiagnosticInfo());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.severe(e.getDiagnosticInfo());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
@@ -162,7 +119,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
private void handleDecoderException(MediaCodecDecoderRenderer dr, 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) {
|
||||||
CodecException codecExc = (CodecException) e;
|
CodecException codecExc = (CodecException) e;
|
||||||
@@ -178,14 +135,92 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buf != null || codecFlags != 0) {
|
if (buf != null || codecFlags != 0) {
|
||||||
throw new RendererException(dr, e, buf, codecFlags);
|
throw new RendererException(this, e, buf, codecFlags);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new RendererException(dr, e);
|
throw new RendererException(this, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRendererThread()
|
private void startDirectSubmitRendererThread()
|
||||||
|
{
|
||||||
|
rendererThread = new Thread() {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
BufferInfo info = new BufferInfo();
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
// Try to output a frame
|
||||||
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
||||||
|
if (outIndex >= 0) {
|
||||||
|
long presentationTimeUs = info.presentationTimeUs;
|
||||||
|
int lastIndex = outIndex;
|
||||||
|
|
||||||
|
// Get the last output buffer in the queue
|
||||||
|
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||||
|
|
||||||
|
lastIndex = outIndex;
|
||||||
|
presentationTimeUs = info.presentationTimeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the last buffer
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
|
||||||
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
|
long delta = System.currentTimeMillis() - (presentationTimeUs / 1000);
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
decoderTimeMs += delta;
|
||||||
|
totalTimeMs += delta;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (outIndex) {
|
||||||
|
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||||
|
break;
|
||||||
|
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||||
|
LimeLog.info("Output buffers changed");
|
||||||
|
break;
|
||||||
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
|
LimeLog.info("Output format changed");
|
||||||
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||||
|
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
|
||||||
|
rendererThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
|
||||||
|
int index;
|
||||||
|
long startTime, queueTime;
|
||||||
|
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
|
||||||
|
if (index < 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (queueTime - startTime >= 20) {
|
||||||
|
LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startLegacyRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@@ -194,6 +229,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
DecodeUnit du = null;
|
DecodeUnit du = null;
|
||||||
int inputIndex = -1;
|
int inputIndex = -1;
|
||||||
|
long lastDuDequeueTime = 0;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
// In order to get as much data to the decoder as early as possible,
|
// In order to get as much data to the decoder as early as possible,
|
||||||
@@ -201,8 +237,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (inputIndex == -1 && du == null) {
|
if (inputIndex == -1 && du == null) {
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
inputIndex = dequeueInputBuffer(false, false);
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
lastDuDequeueTime = System.currentTimeMillis();
|
||||||
|
notifyDuReceived(du);
|
||||||
|
}
|
||||||
|
|
||||||
// Stop if we can't get a DU or input buffer
|
// Stop if we can't get a DU or input buffer
|
||||||
if (du == null || inputIndex == -1) {
|
if (du == null || inputIndex == -1) {
|
||||||
@@ -216,7 +256,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,21 +271,30 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// If we've got a DU waiting to be given to the decoder,
|
// If we've got a DU waiting to be given to the decoder,
|
||||||
// wait a full 3 ms for an input buffer. Otherwise
|
// wait a full 3 ms for an input buffer. Otherwise
|
||||||
// just see if we can get one immediately.
|
// just see if we can get one immediately.
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
inputIndex = dequeueInputBuffer(du != null, false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a decode unit if we don't have one already
|
// Grab a decode unit if we don't have one already
|
||||||
if (du == null) {
|
if (du == null) {
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
lastDuDequeueTime = System.currentTimeMillis();
|
||||||
|
notifyDuReceived(du);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
if (submissionTime - lastDuDequeueTime >= 20) {
|
||||||
|
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
|
||||||
|
}
|
||||||
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
|
||||||
// DU and input buffer have both been consumed
|
// DU and input buffer have both been consumed
|
||||||
@@ -279,26 +328,26 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (outIndex) {
|
switch (outIndex) {
|
||||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||||
// Getting an input buffer may already block
|
// Getting an input buffer may already block
|
||||||
// so don't park if we still need to do that
|
// so don't park if we still need to do that
|
||||||
if (inputIndex >= 0) {
|
if (inputIndex >= 0) {
|
||||||
LockSupport.parkNanos(1);
|
LockSupport.parkNanos(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||||
LimeLog.info("Output buffers changed");
|
LimeLog.info("Output buffers changed");
|
||||||
break;
|
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());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,11 +365,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
|
|
||||||
// On devices pre-Lollipop, we'll use a rendering thread
|
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||||
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
if (directSubmit) {
|
||||||
startRendererThread();
|
startDirectSubmitRendererThread();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
startLegacyRendererThread();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +410,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(this, e, null, codecFlags);
|
handleDecoderException(e, null, codecFlags);
|
||||||
lastException = e;
|
lastException = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,14 +422,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
||||||
long currentTime = System.currentTimeMillis();
|
long timestampUs = System.currentTimeMillis() * 1000;
|
||||||
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
|
||||||
if (delta >= 0 && delta < 1000) {
|
|
||||||
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
|
||||||
totalFrames++;
|
|
||||||
}
|
|
||||||
|
|
||||||
long timestampUs = currentTime * 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
|
||||||
@@ -400,7 +446,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
boolean needsSpsReplay = false;
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
ByteBufferDescriptor header = decodeUnit.getBufferHead();
|
||||||
if (header.data[header.offset+4] == 0x67) {
|
if (header.data[header.offset+4] == 0x67) {
|
||||||
numSpsIn++;
|
numSpsIn++;
|
||||||
|
|
||||||
@@ -485,8 +531,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy data from our buffer list into the input buffer
|
// Copy data from our buffer list into the input buffer
|
||||||
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
|
||||||
{
|
desc != null; desc = desc.nextDescriptor) {
|
||||||
buf.put(desc.data, desc.offset, desc.length);
|
buf.put(desc.data, desc.offset, desc.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +548,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void replaySps() {
|
private void replaySps() {
|
||||||
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
int inputIndex = dequeueInputBuffer(true, true);
|
||||||
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
||||||
|
|
||||||
inputBuffer.clear();
|
inputBuffer.clear();
|
||||||
@@ -530,8 +576,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return adaptivePlayback ?
|
int caps = 0;
|
||||||
|
|
||||||
|
caps |= adaptivePlayback ?
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
||||||
|
|
||||||
|
caps |= directSubmit ?
|
||||||
|
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
||||||
|
|
||||||
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -555,6 +608,35 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return decoderName;
|
return decoderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyDuReceived(DecodeUnit du) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long delta = currentTime-du.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
totalTimeMs += currentTime-du.getReceiveTimestamp();
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void directSubmitDecodeUnit(DecodeUnit du) {
|
||||||
|
int inputIndex;
|
||||||
|
|
||||||
|
notifyDuReceived(du);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
inputIndex = dequeueInputBuffer(true, true);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputIndex >= 0) {
|
||||||
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class RendererException extends RuntimeException {
|
public class RendererException extends RuntimeException {
|
||||||
private static final long serialVersionUID = 8985937536997012406L;
|
private static final long serialVersionUID = 8985937536997012406L;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ public class MediaCodecHelper {
|
|||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
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;
|
||||||
|
|
||||||
|
static {
|
||||||
|
directSubmitPrefixes = new LinkedList<String>();
|
||||||
|
|
||||||
|
// These decoders have low enough input buffer latency that they
|
||||||
|
// can be directly invoked from the receive thread
|
||||||
|
directSubmitPrefixes.add("omx.qcom");
|
||||||
|
directSubmitPrefixes.add("omx.sec");
|
||||||
|
directSubmitPrefixes.add("omx.exynos");
|
||||||
|
directSubmitPrefixes.add("omx.intel");
|
||||||
|
directSubmitPrefixes.add("omx.brcm");
|
||||||
|
directSubmitPrefixes.add("omx.TI");
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
preferredDecoders = new LinkedList<String>();
|
||||||
@@ -98,6 +112,10 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ 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 POLLING_PERIOD_MS = 3000;
|
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
|
||||||
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
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 = 3;
|
||||||
@@ -135,7 +136,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait until the next polling interval
|
// Wait until the next polling interval
|
||||||
Thread.sleep(POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -581,7 +582,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private boolean waitPollingDelay() {
|
private boolean waitPollingDelay() {
|
||||||
try {
|
try {
|
||||||
synchronized (pollEvent) {
|
synchronized (pollEvent) {
|
||||||
pollEvent.wait(POLLING_PERIOD_MS);
|
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.BitmapFactory;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@@ -14,27 +14,18 @@ import com.limelight.grid.assets.MemoryAssetLoader;
|
|||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
private final Activity activity;
|
|
||||||
|
|
||||||
private static final int ART_WIDTH_PX = 300;
|
private static final int ART_WIDTH_PX = 300;
|
||||||
private static final int SMALL_WIDTH_DP = 100;
|
private static final int SMALL_WIDTH_DP = 100;
|
||||||
private static final int LARGE_WIDTH_DP = 150;
|
private static final int LARGE_WIDTH_DP = 150;
|
||||||
|
|
||||||
private final CachedAppAssetLoader loader;
|
private final CachedAppAssetLoader loader;
|
||||||
private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
|
|
||||||
private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
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) throws KeyManagementException, NoSuchAlgorithmException {
|
||||||
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);
|
||||||
@@ -56,26 +47,20 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
this.activity = activity;
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inSampleSize = (int) scalingDivisor;
|
||||||
|
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
|
new MemoryAssetLoader(),
|
||||||
}
|
new DiskAssetLoader(context.getCacheDir()),
|
||||||
|
BitmapFactory.decodeResource(activity.getResources(),
|
||||||
private static void cancelTuples(ConcurrentHashMap<?, CachedAppAssetLoader.LoaderTuple> map) {
|
R.drawable.image_loading, options));
|
||||||
Collection<CachedAppAssetLoader.LoaderTuple> tuples = map.values();
|
|
||||||
|
|
||||||
for (CachedAppAssetLoader.LoaderTuple tuple : tuples) {
|
|
||||||
tuple.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
map.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public void cancelQueuedOperations() {
|
||||||
cancelTuples(loadingTuples);
|
loader.cancelForegroundLoads();
|
||||||
cancelTuples(backgroundLoadingTuples);
|
loader.cancelBackgroundLoads();
|
||||||
|
|
||||||
loader.freeCacheMemory();
|
loader.freeCacheMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,14 +74,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
public void addApp(AppView.AppObject app) {
|
||||||
// Queue a request to fetch this bitmap in the background
|
// Queue a request to fetch this bitmap into cache
|
||||||
Object tupleKey = new Object();
|
loader.queueCacheLoad(app.app);
|
||||||
CachedAppAssetLoader.LoaderTuple tuple =
|
|
||||||
loader.loadBitmapWithContextInBackground(app.app, tupleKey, backgroundLoadListener);
|
|
||||||
if (tuple != null) {
|
|
||||||
backgroundLoadingTuples.put(tupleKey, tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Add the app to our sorted list
|
||||||
itemList.add(app);
|
itemList.add(app);
|
||||||
sortList();
|
sortList();
|
||||||
}
|
}
|
||||||
@@ -105,100 +86,9 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
itemList.remove(app);
|
itemList.remove(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() {
|
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
||||||
@Override
|
// Let the cached asset loader handle it
|
||||||
public void notifyLongLoad(Object object) {
|
loader.populateImageView(obj.app, imgView);
|
||||||
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
|
||||||
|
|
||||||
// If the view isn't there anymore, don't bother scheduling on the UI thread
|
|
||||||
if (viewRef.get() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ImageView view = viewRef.get();
|
|
||||||
if (view != null) {
|
|
||||||
view.setImageResource(R.drawable.image_loading);
|
|
||||||
fadeInImage(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
|
||||||
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
|
||||||
|
|
||||||
loadingTuples.remove(viewRef);
|
|
||||||
|
|
||||||
// Just leave the loading icon in place
|
|
||||||
if (bitmap == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the view isn't there anymore, don't bother scheduling on the UI thread
|
|
||||||
if (viewRef.get() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ImageView view = viewRef.get();
|
|
||||||
if (view != null) {
|
|
||||||
view.setImageBitmap(bitmap);
|
|
||||||
fadeInImage(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final CachedAppAssetLoader.LoadListener backgroundLoadListener = new CachedAppAssetLoader.LoadListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyLongLoad(Object object) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
|
||||||
backgroundLoadingTuples.remove(object);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void reapLoaderTuples(ImageView view) {
|
|
||||||
// Poor HashMap doesn't deserve this...
|
|
||||||
Iterator<Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple>> i = loadingTuples.entrySet().iterator();
|
|
||||||
while (i.hasNext()) {
|
|
||||||
Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> entry = i.next();
|
|
||||||
ImageView imageView = entry.getKey().get();
|
|
||||||
|
|
||||||
// Remove tuples that refer to this view or no view
|
|
||||||
if (imageView == null || imageView == view) {
|
|
||||||
// FIXME: There's a small chance that this can race if we've already gone down
|
|
||||||
// the path to notification but haven't been notified yet
|
|
||||||
entry.getValue().cancel();
|
|
||||||
|
|
||||||
// Remove it from the tuple list
|
|
||||||
i.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
|
||||||
// Cancel pending loads on this image view
|
|
||||||
reapLoaderTuples(imgView);
|
|
||||||
|
|
||||||
// Clear existing contents of the image view
|
|
||||||
imgView.setAlpha(0.0f);
|
|
||||||
|
|
||||||
// Start loading the bitmap
|
|
||||||
WeakReference<ImageView> viewRef = new WeakReference<>(imgView);
|
|
||||||
CachedAppAssetLoader.LoaderTuple tuple = loader.loadBitmapWithContext(obj.app, viewRef, imageViewLoadListener);
|
|
||||||
if (tuple != null) {
|
|
||||||
// The load was issued asynchronously
|
|
||||||
loadingTuples.put(viewRef, tuple);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +112,4 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
// No overlay
|
// No overlay
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fadeInImage(ImageView view) {
|
|
||||||
view.animate().alpha(1.0f).setDuration(100).start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,148 +1,322 @@
|
|||||||
package com.limelight.grid.assets;
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
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.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class CachedAppAssetLoader {
|
public class CachedAppAssetLoader {
|
||||||
|
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
||||||
|
|
||||||
|
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
||||||
|
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
||||||
|
private static final int MAX_PENDING_DISK_LOADS = 40;
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
private final ComputerDetails computer;
|
private final ComputerDetails computer;
|
||||||
private final double scalingDivider;
|
private final double scalingDivider;
|
||||||
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
|
||||||
private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
|
||||||
private final NetworkAssetLoader networkLoader;
|
private final NetworkAssetLoader networkLoader;
|
||||||
private final MemoryAssetLoader memoryLoader;
|
private final MemoryAssetLoader memoryLoader;
|
||||||
private final DiskAssetLoader diskLoader;
|
private final DiskAssetLoader diskLoader;
|
||||||
|
private final Bitmap placeholderBitmap;
|
||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
DiskAssetLoader diskLoader) {
|
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.scalingDivider = scalingDivider;
|
this.scalingDivider = scalingDivider;
|
||||||
|
|
||||||
this.networkLoader = networkLoader;
|
this.networkLoader = networkLoader;
|
||||||
this.memoryLoader = memoryLoader;
|
this.memoryLoader = memoryLoader;
|
||||||
this.diskLoader = diskLoader;
|
this.diskLoader = diskLoader;
|
||||||
|
this.placeholderBitmap = placeholderBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelBackgroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
||||||
|
cacheExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelForegroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
|
||||||
|
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
||||||
|
foregroundExecutor.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((r = networkExecutor.getQueue().poll()) != null) {
|
||||||
|
networkExecutor.remove(r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void freeCacheMemory() {
|
public void freeCacheMemory() {
|
||||||
memoryLoader.clearCache();
|
memoryLoader.clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) {
|
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||||
return new Runnable() {
|
// Try 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
// Check again whether we've been cancelled or the image view is gone
|
||||||
|
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||||
|
if (in != null) {
|
||||||
|
// Write the stream straight to disk
|
||||||
|
diskLoader.populateCacheWithStream(tuple, in);
|
||||||
|
|
||||||
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
|
if (task != null) {
|
||||||
|
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise it's a background load and we return nothing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second with a bit of fuzz
|
||||||
|
try {
|
||||||
|
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||||
|
private final WeakReference<ImageView> imageViewRef;
|
||||||
|
private final boolean diskOnly;
|
||||||
|
|
||||||
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
|
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
||||||
|
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
||||||
|
this.diskOnly = diskOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||||
|
tuple = params[0];
|
||||||
|
|
||||||
|
// Check whether it has been cancelled or the image view is gone
|
||||||
|
if (isCancelled() || imageViewRef.get() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp == null) {
|
||||||
|
if (!diskOnly) {
|
||||||
|
// Try to load the asset from the network
|
||||||
|
bmp = doNetworkAssetLoad(tuple, this);
|
||||||
|
} else {
|
||||||
|
// Report progress to display the placeholder and spin
|
||||||
|
// off the network-capable task
|
||||||
|
publishProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the bitmap
|
||||||
|
if (bmp != null) {
|
||||||
|
memoryLoader.populateCache(tuple, bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Void... nothing) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current loader task for this view isn't us, do nothing
|
||||||
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Set off another loader task on the network executor
|
||||||
|
LoaderTask task = new LoaderTask(imageView, false);
|
||||||
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||||
|
imageView.setAlpha(1.0f);
|
||||||
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Set the bitmap
|
||||||
|
if (bitmap != null) {
|
||||||
|
imageView.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the view
|
||||||
|
imageView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class AsyncDrawable extends BitmapDrawable {
|
||||||
|
private final WeakReference<LoaderTask> loaderTaskReference;
|
||||||
|
|
||||||
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
|
LoaderTask loaderTask) {
|
||||||
|
super(res, bitmap);
|
||||||
|
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderTask getLoaderTask() {
|
||||||
|
return loaderTaskReference.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoaderTask getLoaderTask(ImageView imageView) {
|
||||||
|
if (imageView == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Drawable drawable = imageView.getDrawable();
|
||||||
|
|
||||||
|
// If our drawable is in play, get the loader task
|
||||||
|
if (drawable instanceof AsyncDrawable) {
|
||||||
|
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||||
|
return asyncDrawable.getLoaderTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
||||||
|
final LoaderTask loaderTask = getLoaderTask(imageView);
|
||||||
|
|
||||||
|
// Check if any task was pending for this image view
|
||||||
|
if (loaderTask != null && !loaderTask.isCancelled()) {
|
||||||
|
final LoaderTuple taskTuple = loaderTask.tuple;
|
||||||
|
|
||||||
|
// Cancel the task if it's not already loading the same data
|
||||||
|
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
||||||
|
loaderTask.cancel(true);
|
||||||
|
} else {
|
||||||
|
// It's already loading what we want
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the load to proceed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queueCacheLoad(NvApp app) {
|
||||||
|
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
||||||
|
// It's in memory which means it must also be on disk
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue a fetch in the cache executor
|
||||||
|
cacheExecutor.execute(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Abort if we've been cancelled
|
// Check if the image is cached on disk
|
||||||
if (tuple.cancelled) {
|
if (diskLoader.checkCacheExists(tuple)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
// Try to load the asset from the network and cache result on disk
|
||||||
if (bmp == null) {
|
doNetworkAssetLoad(tuple, null);
|
||||||
// Notify the listener that this may take a while
|
|
||||||
listener.notifyLongLoad(context);
|
|
||||||
|
|
||||||
// Try 5 times maximum
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
// Check again whether we've been cancelled
|
|
||||||
if (tuple.cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream in = networkLoader.getBitmapStream(tuple);
|
|
||||||
if (in != null) {
|
|
||||||
// Write the stream straight to disk
|
|
||||||
diskLoader.populateCacheWithStream(tuple, in);
|
|
||||||
|
|
||||||
// Read it back scaled
|
|
||||||
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
if (bmp != null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait 1 second with a bit of fuzz
|
|
||||||
try {
|
|
||||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bmp != null) {
|
|
||||||
// Populate the memory cache
|
|
||||||
memoryLoader.populateCache(tuple, bmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check one last time whether we've been cancelled
|
|
||||||
synchronized (tuple) {
|
|
||||||
if (tuple.cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
tuple.notified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the load complete callback (possible with a null bitmap)
|
|
||||||
listener.notifyLoadComplete(context, bmp);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener) {
|
public void populateImageView(NvApp app, ImageView view) {
|
||||||
return loadBitmapWithContext(app, context, listener, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoaderTuple loadBitmapWithContextInBackground(NvApp app, Object context, LoadListener listener) {
|
|
||||||
return loadBitmapWithContext(app, context, listener, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) {
|
|
||||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
// If there's already a task in progress for this view,
|
||||||
|
// cancel it. If the task is already loading the same image,
|
||||||
|
// we return and let that load finish.
|
||||||
|
if (!cancelPendingLoad(tuple, view)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// First, try the memory cache in the current context
|
// First, try the memory cache in the current context
|
||||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
// The caller never sees our tuple in this case
|
// Show the bitmap immediately
|
||||||
listener.notifyLoadComplete(context, bmp);
|
view.setAlpha(1.0f);
|
||||||
return null;
|
view.setImageBitmap(bmp);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's not in memory, throw this in our executor
|
// If it's not in memory, create an async task to load it. This task will be attached
|
||||||
if (background) {
|
// via AsyncDrawable to this view.
|
||||||
backgroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
|
final LoaderTask task = new LoaderTask(view, true);
|
||||||
}
|
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
|
||||||
else {
|
view.setAlpha(0.0f);
|
||||||
foregroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
|
view.setImageDrawable(asyncDrawable);
|
||||||
}
|
|
||||||
return tuple;
|
// Run the task on our foreground executor
|
||||||
|
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LoaderTuple {
|
public class LoaderTuple {
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
|
|
||||||
public boolean notified;
|
|
||||||
public boolean cancelled;
|
|
||||||
|
|
||||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean cancel() {
|
@Override
|
||||||
synchronized (this) {
|
public boolean equals(Object o) {
|
||||||
cancelled = true;
|
if (!(o instanceof LoaderTuple)) {
|
||||||
return !notified;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoaderTuple other = (LoaderTuple) o;
|
||||||
|
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -150,13 +324,4 @@ public class CachedAppAssetLoader {
|
|||||||
return "("+computer.uuid+", "+app.getAppId()+")";
|
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface LoadListener {
|
|
||||||
// Notifies that the load didn't hit any cache and is about to be dispatched
|
|
||||||
// over the network
|
|
||||||
public void notifyLongLoad(Object context);
|
|
||||||
|
|
||||||
// Bitmap may be null if the load failed
|
|
||||||
public void notifyLoadComplete(Object context, Bitmap bitmap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -18,6 +19,10 @@ public class DiskAssetLoader {
|
|||||||
this.cacheDir = cacheDir;
|
this.cacheDir = cacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
Bitmap bmp = null;
|
Bitmap bmp = null;
|
||||||
@@ -26,6 +31,7 @@ public class DiskAssetLoader {
|
|||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
options.inSampleSize = sampleSize;
|
options.inSampleSize = sampleSize;
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.limelight.LimeLog;
|
|||||||
|
|
||||||
public class MemoryAssetLoader {
|
public class MemoryAssetLoader {
|
||||||
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 12) {
|
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
||||||
@Override
|
@Override
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
protected int sizeOf(String key, Bitmap bitmap) {
|
||||||
// Sizeof returns kilobytes
|
// Sizeof returns kilobytes
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import com.limelight.nvstream.http.NvHTTP;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
public class NetworkAssetLoader {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ public class CacheHelper {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean cacheFileExists(File root, String... path) {
|
||||||
|
return openPath(false, root, path).exists();
|
||||||
|
}
|
||||||
|
|
||||||
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||||
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.Game;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
public class ServerHelper {
|
||||||
|
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp : computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
Intent intent = new Intent(parent, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
|
parent.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doQuit(final Activity parent,
|
||||||
|
final InetAddress address,
|
||||||
|
final NvApp app,
|
||||||
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
|
final Runnable onComplete) {
|
||||||
|
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(address,
|
||||||
|
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||||
|
} else {
|
||||||
|
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 599) {
|
||||||
|
message = "This session wasn't started by this device," +
|
||||||
|
" so it cannot be quit. End streaming on the original " +
|
||||||
|
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = parent.getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
} finally {
|
||||||
|
if (onComplete != null) {
|
||||||
|
onComplete.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
parent.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
@@ -28,4 +32,31 @@ public class UiHelper {
|
|||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||||
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||||
|
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
|
||||||
|
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
<string name="conn_establishing_title">Establishing Connection</string>
|
<string name="conn_establishing_title">Establishing Connection</string>
|
||||||
<string name="conn_establishing_msg">Starting connection</string>
|
<string name="conn_establishing_msg">Starting connection</string>
|
||||||
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
||||||
<string name="conn_client_latency">Average client-side frame latency:</string>
|
<string name="conn_client_latency">Average frame decoding latency:</string>
|
||||||
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
||||||
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
|
<string name="conn_hardware_latency">Average hardware decoding latency:</string>
|
||||||
<string name="conn_starting">Starting</string>
|
<string name="conn_starting">Starting</string>
|
||||||
<string name="conn_error_title">Connection Error</string>
|
<string name="conn_error_title">Connection Error</string>
|
||||||
<string name="conn_error_msg">Failed to start</string>
|
<string name="conn_error_msg">Failed to start</string>
|
||||||
|
|||||||
Reference in new issue
Block a user