Not that long ago, our company was contracted to develop an application which placed a large emphasis on users being able to listen to audio podcasts while browsing the rest of the application completely uninterrupted or multitask on their devices.
The previous version of the application had a separate, hybrid mobile app that allowed users just that, but had performance and functionality issues.
Our approach was to use our React + Firebase tech stack to develop a PWA application, covering all use cases under one flexible, performant code base.
We decided to present the audio player to the user at the bottom of the screen when an audio track is being played, without blocking any further interaction with other content displayed on the screen. Also, a condensed version for mobile screens is presented so that it can be expanded with a button press. A simple solution for both small and large screens.
From a functional perspective, there were a couple of features we had to look out for, such as:
- Switching tracks from outside the player component itself
- Setting narration speed within the player
- A fully functional seek/progress bar
- The ability to download the audio source
- Most importantly, background playback on mobile devices
Using React and Redux made most of these requirements a non-issue, with the default HTML Audio element supporting most of these controls out of the box. It didn’t take long before we had a functional component ready for testing.
The requirements to pass testing were the following:
- Desktop versions had to be able to persist playback even when minimised in Safari, Chrome, and Firefox
- Mobile versions had to be able to persist playback when the application is in the foreground, in the background and when the screen is locked, with basic media controls showing up on the screen when the application isn’t in the foreground.
Android and desktop passed with flying colours in all cases, but iOS - not so much:
- Worked only when the application was in the foreground.
- When added to the home screen, audio playback stopped completely when the app was minimised or screen was locked.
- When opened directly in the browser, audio playback persisted, but sometimes lost context and stopped playing.
- Lock screen controls were spotty at best in both cases, sometimes showing up, sometimes not, and sometimes losing context during minimised playback (did nothing to audio playback).
- No option to download the audio file, only open it in another tab.
- Audio track doesn’t autoplay if there is no user action involved.
The harsh reality
As it turns out, we were fortunate to even have basic PWA functionalities. iOS 12 released a couple of days before testing with improved PWA support on iOS devices to the point that they were now usable.
Before iOS 12, PWAs didn’t persist their state — for example, with iOS 11, if a user was to browse the application, minimise it and then go back to it, the app would restart with a completely cleared cache.
The recent iOS 13 update didn’t fix any of the issues we had with iOS 12 at all, so all our hopes of Apple fixing the problem in the near future were squashed.
This presented a major problem since a large percentage of existing users had iOS devices and audio playback is a major feature to them, so Android and Desktop versions working as intended meant little at this point. Without a functioning audio player on iOS we could not ship the app. We entered limbo at this point, with all other features finished. We had to do something about it, but there was little in the way of helping us.
The next solution
The only thing left for us to try was to wrap the entire web app in a native shell and publish it on the App Store. The entire idea of having everything in one solution went flying out the window, but what can you do.
We continued developing the solution and interoping between our web app, Cordova plugins for background playback, a separate build procedure and generally a separate project to maintain alongside the already huge PWA app.
We managed to utilise Cordova plugins and a little bit of tweaking to our audio player component to finally allow background audio playback. After a while, we had a native app to push to the store.
We tried and got rejected.
We had to implement in-app purchases to get published (because of other sections in the app allowing purchasing tickets to events). There was no efficient way to interop our React frontend with a functioning Cordova in-app purchase plugin from a technical perspective.
At this point, we had a serious discussion on what to do next. We had to go native for iOS. It felt stupid after all this work and with Android and Desktop working perfectly, but it was the only thing to do.
The final solution
After seeing what maintaining a Cordova shell around the PWA app would look like, a native iOS application seems like a much wiser choice when considering future development.
Enter React Native, the obvious choice. We could reuse most of the functional components (entire Redux modules) from our PWA solution, which helped us immensely with a quick delivery. All we had to do was build screens and components.
It took a couple of weeks to finish development of the simplified media player app with all the audio player bells and whistles we expected to work in the PWA version in the first place. Audio download, background playback, fully functional lock screen media controls — we finally had it all.
The good thing is that audio player and PWA functionalities work perfectly fine on Android and Desktop. We built an extremely fast solution with React, Redux and Firestore that we’re very proud of. However, there are a couple of ugly aspects of it:
- There is no control over lock screen media controls, only play and pause work.
- An audio element has to be rendered at all times to not mess up playback permissions, no matter if there is an audio source or not. We’ve opted to use an audio object instead of an element, that is always rendered in the DOM.
Of course, the ugliest part of this is that iOS is the cause of nearly all the problems we had with audio playback and we had to build a native app to provide users the functionality Android allows from the get-go.
The worst part of the whole ordeal is that none of the issues were on our side.
Apple proved to be the blocking factor in all instances, even to the point of an application not working as intended. As to the reasons for it, we can’t be sure, but Spotify already has a pretty good idea why PWAs are discriminated against (which you can read about here: https://www.timetoplayfair.com/ ).
In the end, we lost the unified solution we wanted/the client expected and will have to maintain two solutions for the foreseeable future. Thank you, Apple.
A word of advice for any future projects involving PWA — if it has anything to do with media playback, don’t bet on iOS. Think of PWAs as ProblemsWithApple.