doublesymmetry / react-native-track-player

A fully fledged audio module created for music apps. Provides audio playback, external media controls, background mode and more!
https://rntp.dev/
Apache License 2.0
3.18k stars 981 forks source link

Android Doze Mode kills audio in player #2198

Closed justinhandley closed 3 months ago

justinhandley commented 8 months ago

Describe the Bug Since Android 12, on certain devices, the audio playback just stops on android devices after about 5 or 10 minutes when the device puts the app into doze mode.

Steps To Reproduce Build an app, deploy to android, play long audios.

Code To Reproduce This is a systematic / config question therefore I don't think code samples are relevant.

Replicable on Example App? Can you replicate this bug in the React Native Track Player Example App?

Not sure - will work on this and report back.

Environment Info: Paste the results of npx react-native info Paste the exact react-native-track-player version you are using Real device? Or simulator? What OS are you running?

info Fetching system and libraries information... System: OS: macOS 14.0 CPU: (16) x64 Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz Memory: 67.90 GB / 128.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 18.12.0 - ~/.asdf/installs/nodejs/18.12.0/bin/node Yarn: 1.22.19 - ~/IdeaProjects/tmi-nx-stack/node_modules/.bin/yarn npm: 8.19.2 - ~/.asdf/plugins/nodejs/shims/npm Watchman: 2023.10.23.00 - /usr/local/bin/watchman Managers: CocoaPods: 1.13.0 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: DriverKit 23.0, iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0 Android SDK: Not Found IDEs: Android Studio: 2022.3 AI-223.8836.35.2231.10811636 Xcode: 15.0.1/15A507 - /usr/bin/xcodebuild Languages: Java: 17.0.9 - /usr/bin/javac npmPackages: @react-native-community/cli: 10.2.2 => 10.2.2 react: 18.2.0 => 18.2.0 react-native: 0.71.8 => 0.71.8 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

This only happens on real devices as far as we have seen, can't get it to happen on a simulator., and it seems to have variable performance across different devices, maybe due to battery life or settings per device

We are on 3.2.0

How I can Help What can you do to help resolve this? If this is a real issue and other people are experiencing it, we might be able to fix and do a pull request.

Have you investigated the underlying JS or Swift/Android code causing this bug? Yes, but it is just about background mode - basically, when you hit play on an audio you should probably put the system in a 'partial wake lock' - at least that is the workaround we are considering.

Can you create a Pull Request with a fix? Maybe, but just trying to make sure that this isn't just a config setting I'm missing.

dcvz commented 8 months ago

Is this happening to you during a track change? And during that track change are you loading new audio?

justinhandley commented 8 months ago

No - this happens about 5 to 10 minutes into a track (some of our tracks are over an hour) on Android only when the phone is in background mode. We also have downloads, and it doesn't happen on downloads. From what we can tell, it relates to the Doze mode - https://developer.android.com/training/monitoring-device-state/doze-standby - where part of Doze is that it cuts the internet connection. I'm wondering if this is something you guys haven't run into because maybe if people are primarily working with shorter tracks and when they change track, it 'wakes' the app up for a second, so doze mode doesn't occur.

I would assume you guys have some way of stopping the app from going offline from streaming audio - or that music apps in general (spotify) handle this somehow - I guess I'm just wondering if this is something you have handled and our version or our setup just needs to be tweaked somehow, or if this is something you aren't seeing and we need to do our own partial-wake solution. Also, if we do that, should we do it as part of your library, or just in our app (i.e. when someone hits play we put them in partial wake mode, when they hit stop we remove it)

lovegaoshi commented 8 months ago

you might be experiencing a version of https://github.com/doublesymmetry/react-native-track-player/issues/2159 android battery optimization has been a problem for the longest time. I personally use a Samsung S21 and just never saw my app killed; it won't even kill itself on app close, with AppKilledPlaybackBehavior set to stop and remove notif and battery optmization on. which is... weird but works. I'm assuming setting AppKilledPlaybackBehavior to continue playback won't magically work for you, and realistically everyone else has been just asking users to disable battery optimization of their apps, i'm afraid your options might also be limited to that?

uzegonemad commented 7 months ago

We're running into this with our app as well. I would set AppKilledPlaybackBehavior to ContinuePlayback but I also have a periodic API request that is called via the PlaybackProgressUpdated event, and that event obviously isn't called once the app is killed.

It would be great if RNTP included a way to periodically call native code when the PlaybackProgressUpdated event is called (or at some other interval), in order to work around that issue.

lovegaoshi commented 7 months ago

Appkilledbehavior.continueplayback only keeps the musicService to be alive (ie not killed), I do believe the event still emits but nothing is receiving it if your RNactivity is dead. Same goes for any of the remote events. Imo RNTP is severely limited when the activity is killed (not rntps fault, but a limitation to RN) You prob need to customize storing your business logic passed to native, or better yet just ditch RN all together

On Tue, Nov 21, 2023, 8:17 AM Benjamin Uzelac @.***> wrote:

We're running into this with our app as well. I would set AppKilledPlaybackBehavior to ContinuePlayback but I also have a periodic API request that is called via the PlaybackProgressUpdated event, and that event obviously isn't called once the app is killed.

It would be great if RNTP included a way to periodically call native code when the PlaybackProgressUpdated event is called (or at some other interval), in order to work around that issue.

— Reply to this email directly, view it on GitHub https://github.com/doublesymmetry/react-native-track-player/issues/2198#issuecomment-1821235773, or unsubscribe https://github.com/notifications/unsubscribe-auth/AZMOVVS7QVIBAYE7LRKF363YFTHYZAVCNFSM6AAAAAA7FDQZLWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMRRGIZTKNZXGM . You are receiving this because you commented.Message ID: @.*** com>

ALamm commented 6 months ago

Hey @justinhandley - I'm wondering if you found a way around this issue? We are considering switching to RNTP (from expo-av) but we also have long audio tracks and this issue is giving us pause.

Have you tested whether setting ‘Battery optimization > Never sleep’ for your app fixes this issue? Thanks for any insight you can provide.

justinhandley commented 6 months ago

OK, @ALamm - we did get this working. One of my main questions is, can this be built into RNTP or is it a hack every single person has to apply to their app - I don't know that much about android / kotlin / permissions when they live in plugins - I guess maybe it is something that could be at least provided in the readme / install docs if it can't actually be built it.

In the same folder as your MainApplication.java make a NetworkService.kt (obviously replace all com.yourpackagename with your package name

package com.yourpackagename;

import android.app.Service
import android.content.Intent
import androidx.core.content.ContextCompat
import android.os.IBinder
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import android.app.PendingIntent
import android.content.Context
import com.yourpackagename.R

class NetworkService : Service() {

    private val channelId = "network_service_channel"

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        val notification = createNotification()
        startForeground(1, notification)
        // Add your network handling code here
        return START_STICKY
    }

    private fun createNotification(): Notification {
        val notificationIntent = Intent(this, com.yourpackagename.MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)

        return Notification.Builder(this, channelId)
                .setContentTitle("Network Service")
                .setContentText("Running...")
                .setSmallIcon(R.drawable.ic_notification) // Replace with your icon
                .setContentIntent(pendingIntent)
                .build()
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                channelId,
                "Network Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(serviceChannel)
        }
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }
}

and a NetworkWakeLockModule.kt

package com.yourpackagename // replace with your actual package name
import android.content.Intent
import androidx.core.content.ContextCompat

import android.content.Context
import android.os.PowerManager
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.uimanager.ViewManager
import com.yourpackagename.NetworkService

class NetworkWakeLockPackage : ReactPackage {

    override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
        return listOf(NetworkWakeLockModule(reactContext))
    }

    override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
        return emptyList() // If you are not adding any view managers
    }
}

class NetworkWakeLockModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
    private var wakeLock: PowerManager.WakeLock? = null

    override fun getName(): String {
        return "NetworkWakeLockModule"
    }

    @ReactMethod
    fun acquireWakeLock(tag: String) {
        val powerManager = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
        if (wakeLock == null) {
            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag)
            wakeLock?.acquire()
        }
    }

    @ReactMethod
    fun releaseWakeLock() {
        if (wakeLock?.isHeld == true) {
            wakeLock?.release()
            wakeLock = null
        }
    }

    @ReactMethod
    fun startNetworkService() {
        val serviceIntent = Intent(reactApplicationContext, NetworkService::class.java)
        ContextCompat.startForegroundService(reactApplicationContext, serviceIntent)
    }

    @ReactMethod
    fun stopNetworkService() {
        val serviceIntent = Intent(reactApplicationContext, NetworkService::class.java)
        reactApplicationContext.stopService(serviceIntent)
    }

}

in your app/build.gradle add

apply plugin: 'kotlin-android'

in your AndroidManifest.xml

<service android:name="com.yourpackagename.NetworkService" android:exported="false"> </service>

in your MainApplication.java

import com.yourpackagename.NetworkWakeLockPackage;

and before 'return packages' add

packages.add(new NetworkWakeLockPackage());

I THINK that is everything - if that doesn't work for you let me know - the commit with that change had some other stuff in it that I believe is irrelevant and I don't want to dump unnecessary changes on your app.

Essentially, when you play an audio file, we are applying a 'wake lock' on the phone that stops it from cutting off the apps internet connection, etc. This feature is still in beta and not in the wild in our app, but so far all testing seems to point to that it works without asking people to change their battery settings.

Oh, also I think you need to add an icon where it says put your icon... I'd love to know if this works for you, and if it doesn't I'm happy to help figure out what is missing until it does, and ideally we can get this boiled down into a nice replicatable set of steps (or even better the rntp folks can understand this and incorporate it in their project.

github-actions[bot] commented 3 months ago

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 7 days.

github-actions[bot] commented 3 months ago

This issue was closed because it has been stalled for 7 days with no activity.