React-native-track-player: Link media buttons directly to the playback

Created on 13 Dec 2017  路  22Comments  路  Source: react-native-kit/react-native-track-player

At the moment, all media controls fire events. Users have to listen to all events and then manually manipulate the player, like so:

if(event.type == 'remote-play') {
    TrackPlayer.play();
} else if(event.type == 'remove-pause') {
    TrackPlayer.pause();
} ....

When I decided to design the API to work like this, I thought that someone might implement a different behavior to the media controls, but I don't think anyone has implemented anything different, it ended up being an annoying boilerplate code that every music app needs to implement the exactly same way.

The biggest problem with that are the issues that it has been causing:

  • Being slow to process the action, since it has to go from the native code, to the event emitter, to the event listener, and finally back to the native code again. That can not only cause a small delay, but can also slow down the whole app, since JS runs in a single thread.
  • In Android, when the app is in background, it needs to create a new service, start the JS engine, send the event, wait for the JS module to connect to the native player and then finally trigger the action. That might slow down the whole device, mainly when the device is low on memory.

I don't see any reason to keep them. If you really think these events shouldn't be removed, please, let me know why, I would really appreciate some feedback.

Enhancement

Most helpful comment

I agree with the others. Please keep the background functionality. It is critical.

All 22 comments

@dcvz @anderslemke @jasongrishkoff @egorkhmelev Any thoughts? I'd love to see some feedback on this

I would much prefer the media to be manipulated directly. I also don't see a downside to it unless our users are performing other actions upon these presses (analytics, etc). However, we could leave the events for users who wish to do other actions based on presses but leave the playback manipulation to the library -- where it will be much faster to process.

If there is really a reason to keep them, we should definitely go with your idea, but even though someone might want to perform background actions (such as analytics), they can still use the playback events.

I think we should provide what most audio apps need in the most efficient and bug free way. If someone really needs an unusual feature, they can fork the project and implement themselves. We can also move it to an addon (the same thing we'll be doing with adaptive streaming and chromecast), so it's completely optional

Let's remove them and let the lib manage playback changes 馃憤 If we start hearing from our users that they're missing something we can think about how we can help them.

I'll just try to explain our use case. We've done podcast app using react-native-video as a player with react-native-music-control. It was pretty much fine on iOS but complete disaster on Android (nevertheless we launch it to production and still getting awful feedback). We were seeking for a solution and suddenly this lib come up. I just want to say thats an amazing piece of work and missing link for any music player out there. So lets move to what we currently need in our app.

First of all we need granular control of what being played, for how long (to obtain usage statistics and present it to the user) and how far (app tends to crash, battery dies and so on). We need to store position inside the track and being able to restart that track from given position. So we cannot just fill the queue with tracks and relax, second or third track in the queue might start from the middle.

Moreover we have premium tracks which might not be available when we fill the queue but might become available later when user buy a subscription (the same true for opposite, track might become unavailable). So we keep all the tracks in own queue and decide what to play next when current track ends (and skip premium one for example).

Next, we need to mark tracks as played when current track ends or user skips that track at the end with some threshold.

And things gets more complicated when we introduced a sleep timers. Which have two options: sleep in some predefined intervals or after current track finish. So we need to seek that as well.

Not to mention that we send all usage analytics. When something starts playing, ends or progress events.

For now what I've found works for us is to keep only one track in the lib's queue and listen for a queue end event to decide what to play next, then reset the queue and add next track to it, seek to right position if needed and start playing. With unbind functionality for service (which keeps js context running even when user exit the app) I'm pretty much happy with the result and user experience. I'm not really happy with track switching as when I reset the queue, notification disappear and then reappear again with a new track (and also its impossible to preload next track in this case). But I couldn't find a better solution, as updating the queue and switching tracks is a bit buggy right now. I ended up in a situation when native part reports that it was playing one track but I was heard another one.

So, it would be great to have those buttons in notification works natively but I don't really understand how. I can think of some sort of modes for this lib, manual or automatic. With manual one lib can just delegate all the decisions to js context. Automatic on can be some sort of addon to the lib, which controls everything natively. Or there might be some sort of mix between the two. For example lib can control play/pause natively but also propagate those events to js. In my current implementation everything gets a way easier when all the decisions was moved to one single place (js context).

Also, I think that queue paradigm is a bit overcomplicated for such lib, which shouldn't assume how people gonna use it and dictate one single pattern/use case (playing number of music tracks in a row). But I understand why it was built that way.

So, keep doing this! I appreciate it.

Hi @Guichaguri . Let me give my thoughts on your proposal of 1) Linking controls to the media, and 2) removing the remote events.

I do see your point on the delay and general performance, which would be solved by linking directly to the player. That is hard to argue against.

I also agree that all functionality probably could be inferred from the playback-events, but I see those as the reactive part, not revealing any intention.

The intention of the user is only communicated by the remote events, and we actually use that intention to guide our error handling.

@anderslemke so you'd be more for an option where we move the playback to be handled by the lib but still emit the remote events as I proposed earlier?

Yes. That sounds reasonable to me.

how do you feel about leaving the events @Guichaguri ; after this last message?

The biggest problem is that audio-based apps in Android should run their players in a service, and react-native is not built to work with services. Sometimes Android kills the app when events are fired in background due to a high memory/CPU usage. I'm trying to find ways to fix this problem since this project started, and that's why I moved to that special event handler, added a queue (reduce business logic being done in events, but I don't think it really helped), and now I'm trying to completely remove media button events.

I'm not out of ideas though, there is a few things I can do:

  • I can keep the events, but only through an option, if the option is not set, you will not receive any events and everything will be handled by the module. This is not a real fix, but it reduce the amount of users affected.
  • I can enqueue all events or stats in background and send them as soon as possible.
  • I can move the player from the service to the activity, it would only work when the app is open (the same behavior that iOS apps have), and all of those problems would vanish. It's not recommended for performance and UX reasons, but it works. We could also go back into using the react-native EventEmitter, which is way nicer and easier. This is the same thing that modules like react-native-sound do.
  • I can keep everything as is. Most users haven't experienced problems like this anyway.

The only way a react-native app could work perfectly with the player running in a service, is if the player code is 100% native (event handling, track selection logic).

@Guichaguri what about headless task? From my tests it will keep running if you don鈥檛 resolve promise returned by js. Coupled with unbind functionality from service its a way to keep js context running without much overhead and system not gonna kill it.

@egorkhmelev You're not supposed to never resolve a promise, they have that name for a reason.

I'm not saying that running the headless task uses a lot of memory/CPU, initializing the headless task is what really uses because it initializes a new service, initializes the JS engine, loads the JS code, interprets it and finally runs it.

And still, you are keeping it running for a single event. Other events will still initialize the headless task again.

Even though the whole point of this module was to be the "most native" and performant audio module, I think moving the player from the service to the activity would be the best solution. It's the safest and most predictable one. As I've said before, react native is not built to work with services.

Since this project started, I thought running the playback in a service was be the best way to implement it, and most devs would prefer using it because of that. But it actually ended up being a problem to work around, making me change the API many times (it must have been annoying for app developers to reflect those changes, and mainly for @dcvz that had to make those changes in iOS). I don't think I should keep trying to fix problems that can't be fixed by design, so I decided to get rid of the service.

Pros

  • Same behavior for Android, iOS and Windows: when the app stops, the playback stops. I've added an option for that before, and most devs seem to use it (you can see in their code in #101, #77, #67, #105).
  • Faster to setup the player as I don't need to initialize the service anymore.
  • We can use the react native event emitter. Easier to integrate, better performance
  • You don't need to worry about the player already being initialized when the app starts anymore
  • Easier to maintain, predictable code which should be bug free

Cons

  • Worse playback performance, but it shouldn't be noticeable
  • Bad UX as most of other audio apps on Android continue playing after the app is closing (e.g. Spotify, SoundCloud, Google Play Music), but in the other side, users might expect the app to be completely gone when they close it.
  • No support for track and playlist selection through other devices (like Android Wear or Android Auto). Which means no support for Android Auto anymore (it was never finished anyway)

About the remote events, I agree with @anderslemke and I think we should keep them at least for now.
But I think it would be nice to have an option to enable the module to natively handle them.

@Guichaguri whats the issue with keeping of not resolved promise until app enters foreground? As I understand how services works, Android won't run multiple services at once, so if you run headless task first time (service initialized) and then keep that promise not resolved and run another headless task, system won't actually run another service it will reuse what it had. More over, as docs states:

Once your task completes (i.e. the promise is resolved), React Native will go into "paused" mode (unless there are other tasks running, or there is a foreground app).

So it won't make that extra work you writing about even with resolved promise (not sure however, but that how it worked for me). So I don't see any performance issues here. Am I miss something?

Can you find any warns from RN docs that you must not keep promises unresolved? Even first sentence of Headless docs states that playing a music is what they had in mind for that task. Thats might be an answer. If you can provide lib which just plays a music without service (and without React components being present in a tree) we can actually use headless task ourselves to run that lib in background.

We should always handle playback natively but let's have an option that enables events firing when user uses media controls (remote events). Not meant for playback modification but to be notified of intention. Let's proceed with this option.

We should definitely keep background playback functionality. We're one of the only libraries that handles this well on Android (we also support it on iOS).

@egorkhmelev If you resolve the promise when the player stops, it's fine. The problem is not ever resolving the promise, which will keep the headless task running forever.

I have a few ideas I want to try before I ditch the service, I'll probably create a new branch for that.

The reason I started using this library is because the way you are using the Service. If you ditch it and you can't play music in the background... That would not make sense as an Android media player app.

I agree you can remove the whole HeadlessJS. We can have listeners when the app is opened. When the app is killed and music is playing... I don't think we need any headless listening. Next time you open the app, we can reconnect to the service (bind) if it's still alive. Does it make sense?

Hello, just weighing in.

In most cases we only need to implement a couple of events if any at all. Having the events available still I think is important for many use cases but removing the event handler implementation from the critical path will be a great improvement.

Hello,
First of all, thanks for this great plugin. I am moving all my native radio apps to react after trying this plugin.

For users like us who uses for playing network audio streams, background playing mode is a must have feature. This plugin does it well.
Removing the service based architecture will make users like me to look for other plugins, because without background playback, player will pause the playback when the screen is locked.

I agree with the others. Please keep the background functionality. It is critical.

Is there any progress on this one?

I'll close this because of v.1.0.0 -- it has improvements to events while keeping background functionality.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fabiocosta88 picture fabiocosta88  路  3Comments

mnlbox picture mnlbox  路  4Comments

amed picture amed  路  4Comments

elioscordo picture elioscordo  路  3Comments

moduval picture moduval  路  4Comments