2

I have an app that can stream local media content to a Chromecast receiver. This mostly works, except that when the device is asleep and not on external power the session will die/disconnect after about 5 minutes (measured from when the screen goes blank).

I've already had a look at this question here:

How do I keep a ChromeCast route alive when my app is in the background on battery power?

...and implemented both of the suggested answers.

Specifically, in my app's manifest, I have:

<uses-permission android:name="android.permission.WAKE_LOCK" />

And then in the embedded HTTP server that I use to stream media content to the Chromecast receiver, I'm doing:

PowerManager powerManager = (PowerManager)Environment.getApplicationContext().getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "cast-server-cpu");
wakeLock.acquire();

WifiManager wifiManager = (WifiManager)Environment.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "cast-server-net");
wifiLock.acquire();

That code executes when the server thread starts (occurs when the Chromecast route is selected), and the locks aren't released until the server is stopped. That happens in my MediaRouter.Callback implementation, which (incorporating the accepted answer from the question above) looks like this:

this.mediaRouterCallback = new MediaRouter.Callback() {
    private MediaRouter.RouteInfo autoconnectRoute = null;

    @Override
    public void onRouteSelected(MediaRouter mediaRouter, MediaRouter.RouteInfo routeInfo) {
        if (autoconnectRoute == null) {
            if (isPlaying()) {
                stop();
                playButton.setImageResource(R.drawable.icon_play);
            }

            castDevice = CastDevice.getFromBundle(routeInfo.getExtras());
            if (castServer != null) {
                castServer.stop();
            }

            //start the Chromecast file server
            castServer = new CastHttpFileServer();
            new Thread(castServer).start();
        }

        initCastClientListener();
        initRemoteMediaPlayer();
        launchReceiver();
    }

    @Override
    public void onRouteUnselected(MediaRouter mediaRouter, MediaRouter.RouteInfo routeInfo) {
        if (! doesRouterContainRoute(mediaRouter, routeInfo)) {
            //XXX:  do not disconnect in this case (the unselect was not initiated by a user action); see https://stackoverflow.com/questions/18967879/how-do-i-keep-a-chromecast-route-alive-when-my-app-is-in-the-background-on-batte
            autoconnectRoute = routeInfo;
            return;
        }

        //user disconnected, teardown and stop playback
        if (isPlaying()) {
            stop();
            playButton.setImageResource(R.drawable.icon_play);
        }
        if (castServer != null) {
            castServer.stop();
            castServer = null;
        }

        teardown();
        castDevice = null;
    }

    @Override
    public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
        super.onRouteAdded(router, route);
        if (autoconnectRoute != null && route.getId().equals(autoconnectRoute.getId())) {
            this.onRouteSelected(router, route);
        }
    }

    private boolean doesRouterContainRoute(MediaRouter router, MediaRouter.RouteInfo route) {
        if (router == null || route == null) {
            return false;
        }

        for (MediaRouter.RouteInfo info : router.getRoutes()) {
            if (info.getId().equals(route.getId())) {
                return true;
            }
        }

        return false;
    }
};

What more needs to be done in order to keep the Chromecast session alive when the device is asleep and not connected to external power?

Also, considering that this issue only reproduces when the device is not connected to USB, what's an effective way to debug in this case?

Edit

Here's the stacktrace that's reported when the session dies (from the embedded HTTP server):

java.net.SocketException: Software caused connection abort
 at java.net.SocketOutputStream.socketWrite0(Native Method)
 at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:112)
 at java.net.SocketOutputStream.write(SocketOutputStream.java:157)
 at com.myapp.CastHttpFileServer.writeResponse(CastHttpFileServer.java:124)
 at com.myapp.CastHttpFileServer.run(CastHttpFileServer.java:180)

And I've also noticed that I can capture this event in my RemoteMediaPlayer.OnStatusUpdatedListener implementation, like:

if (lastStatus == MediaStatus.PLAYER_STATE_PLAYING && mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_BUFFERING) {
    //buffer underrun/receiver went away because the wifi died; how do we fix this?
}
Community
  • 1
  • 1
aroth
  • 54,026
  • 20
  • 135
  • 176
  • Check the answer provided in this [SO post](http://stackoverflow.com/questions/18967879/how-do-i-keep-a-chromecast-route-alive-when-my-app-is-in-the-background-on-batte) where you check to see if route exists when it's unselected. – ReyAnthonyRenacia May 22 '17 at 15:16
  • 2
    "what's an effective way to debug in this case?" -- try `adb` over WiFi, perhaps, using `adb connect`. If this is an Android 6.0+ device, this may be Doze mode or the equivalent, in which case your app needs to be added to the battery optimization whitelist. – CommonsWare May 22 '17 at 16:25

1 Answers1

2

CommonsWare had the right of it in his comment. I was testing on a device running Android 7.1.2, and Doze mode was causing the network to drop after about 5 minutes of inactivity and irrespective of the wake lock(s) my app was holding.

The fix was to add the following permission in the manifest:

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

And then to update my onRouteSelected() callback implementation to request that Doze mode be disabled if/when a ChromeCast route is selected:

if (Build.VERSION.SDK_INT >= 23) {
    String packageName = context.getPackageName();
    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    if (! pm.isIgnoringBatteryOptimizations(packageName)) {
        //reguest that Doze mode be disabled
        Intent intent = new Intent();
        intent.setAction(
            Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
        intent.setData(Uri.parse("package:" + packageName));

        context.startActivity(intent);
    }

}

As long as the user selects 'Yes' when prompted, the app's wake locks are respected and the ChromeCast session stays alive even when the device is not charging.

aroth
  • 54,026
  • 20
  • 135
  • 176
  • Did you have an issue with google approving your app for the play store? I'm in a similar situation where I stream video over HTTP from the device to the receiver. Doze mode is killing the network access and the cast session hangs. – masterwok Dec 03 '17 at 23:14
  • 1
    @masterwok - Yes indeed I did. The app was automatically rejected for attempting to bypass Doze mode, and though I tried several times to appeal Google has so far rejected each one. Ultimately what I did was build two versions of the app; one for the app store that's crippled by Doze mode, and one that bypasses Doze mode which people can install by direct-download (i.e. outside of Google Play). You can [read all about it](http://codethink.no-ip.org/wordpress/chromecast-and-playlister) if you want. – aroth Dec 04 '17 at 00:41
  • Thanks for getting back to me. I'm planning on using the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS intent to take the user directly to settings after showing an instruction dialog. Doing this does not require the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permissions and should prevent the app from being rejected. – masterwok Dec 04 '17 at 01:54
  • 1
    Thanks for the tip. In case you haven't tried this yet, I can confirm that using `ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS` causes no issues with Play Store approval. Works well enough; the process the user has to go through to toggle the setting is a bit involved (6 taps and some scrolling, by my count), but I suppose they only have to do it once. – aroth Dec 19 '17 at 11:29
  • Nice, thanks. On a side note, I was looking at FuTorrent and they seem to have a battery optimization setting in app called, "keep CPU awake". Also, their companion app DivX doesn't seem to request battery optimization settings at all (which is strange as it must act as an HTTP server). Maybe there's another solution here? – masterwok Dec 19 '17 at 16:23