Prototypr

Prototyping, UX Design, Front-end Development and Beyond 👾 | ✍️ Write for us…

Follow publication

I made a cross-platform SoundCloud® player with Fuse

Fuse
Prototypr
Published in
7 min readDec 13, 2016

--

A cross-platform SoundCloud® player, implemented with Fuse.

By Kristian Hasselknippe

Software Engineer at Fuse

Please note: the FuseCloud app is no longer available in the App Store or Google Play due to a claim from SoundCloud that it is in breach of their API terms and conditions. We’ve been in contact with both SoundCloud and Apple to explain how FuseCloud conforms with SoundCloud API terms, however SoundCloud has refused to issue new API keys to the app. The source and explanation for how it was built will remain online.

Every once in a while, we get requests from users who wants to see what “real app code” looks like when made with Fuse. Our tutorial series is explicitly designed to be a good (as in: “soft”) starting point for getting the hang of Fuse, and doesn’t cover advanced use-cases.

Therefore, I decided to make an actual complete app (including publishing it to app stores). It would serve as an extensive code example, include complex use-cases and also be a great ”dogfooding” exercise.

The app had to connect to a real backend, be truly cross-platform (work on both Android and iOS) and show off some custom native integration on both platforms.

As it turns out, SoundCloud® ticked all the boxes:

  • They offer a REST API (and it’s free)
  • Tons of content to work with (both images and music)
  • The app would need native components (idle-screen playback controls and more)

Before we begin..

You don’t need to install Fuse or look at code on Github to check out the result. T̶h̶e̶ ̶F̶u̶s̶e̶C̶l̶o̶u̶d̶ ̶a̶p̶p̶ ̶i̶s̶ ̶a̶v̶a̶i̶l̶a̶b̶l̶e̶ ̶t̶o̶ ̶d̶o̶w̶n̶l̶o̶a̶d̶ ̶f̶r̶o̶m̶ ̶t̶h̶e̶ ̶A̶p̶p̶l̶e̶ ̶A̶p̶p̶ ̶S̶t̶o̶r̶e̶ ̶a̶n̶d̶ ̶G̶o̶o̶g̶l̶e̶ ̶P̶l̶a̶y̶.̶

Please note: the FuseCloud app is an unofficial SoundCloud music player, and is not in any way endorsed by SoundCloud. It simply uses the SoundCloud API.

Implementation

There are three main parts to the app: the app UI written in UX and JavaScript, the SoundCloud REST API wrapper and the native music player.

Navigation

I used the Router and Navigator components for most of the navigation, except for a PageControl for the main view that lets you go between three tabs (news feed, search, and favorites). By making each page of the app its own component, the navigation could be succinctly described:

Endless scrolling

Thanks to some of the newer features of Fuse, I easily implemented a smooth endless scroller to display all the comments for each track. By wrapping each comment item in a Deferred node, I got rid of all lag resulting from adding new elements. Below you can see the UX required to make such a scroller:

Scrolling within 100 points of the bottom triggers a function in JavaScript that loads more comments:

Blurring the background

The track player page displays the album art of the current track in the middle of the page, but it also fills the entire background with a slightly darkened, blurry version of the same artwork. You can blur anything in Fuse with just one line of code: <Blur />, but blurring large elements can come with a performance cost. I therefore use a classic GPU programming trick to speed things up a bit:

In the code above, I scale the album artwork down to 20% of its original size and perform the blur on the smaller image, and then scale it back up again to its original size. Since we’re blurring the image quite a lot in this case, I have the benefit of optimizing for fillrate (fewer pixels = fewer cycles required = faster rendering!) without any noticeable quality loss.

Wrapping the SoundCloud API

This was a straight forward task and didn’t really involve many Fuse specific techniques. I structured the wrapper so that each request returned a promise. The app model was then used as the only (almost) interface to the API through a matching set of getter functions that turned the promises into Observables. The following code illustrates that pattern:

A fetcher function is used to fetch the like status for a track, returned as a promise:

The model turns this promise into an observable using a conveniency function (DelayedObservable):

This is really convenient since the returned observable will be filled as soon as the data arrives. We can then data-bind to it directly and not worry about any callbacks or UI updates.

The DelayedObservable function acts as a bridge between the promise-based API and the Observable based API:

The supplied function is in charge of updating the Observable when the data arrives. Smooth sailing!

OAuth 2.0

The SoundCloud API allows you to log in using the OAuth 2.0 protocol. Using the InterApp module, I can easily navigate to the login endpoint using the native browser:

The above URL proves a callback URI, which SoundCloud uses to return a token. Fuse lets you register a custom URI scheme by editing the project file:

That way, the SoundCloud API will automatically route back to our app, once the access token is ready.

Making a cross-platform media player

Implementing a wrapper for the native audio players was the most interesting part of the process. Partly because the APIs differ between Android and iOS, but also due to the media players monolithic nature. I started with a minimal set of requirements.

The StreamingPlayer should:

  • Stream audio from a URL
  • Keep playing when the app is sent to the background
  • Allow for navigation between tracks while the app was in the background (using the native lock screen controls)
  • Display the album artwork on the lock screen

On paper, this shouldn’t have been much of a challenge, but it turned out to involve a few more things than I thought when I started.

First off: getting a native audio player to play from a URL was super easy. Both the Android MediaPlayer and the iOS AVPlayer APIs provide this out of the box. My initial thought was to use a minimal wrapper over these two APIs and just do the rest of the work (like managing playlists and state) in JavaScript, however the background JS execution restrictions on the target platforms quickly put an end to that (remember that one of our requirements was to be able to use the lock screen controls).

This meant that I had to implement our playlist abstraction in native code, meaning it had to be customized for both Android and iOS. Luckily this wasn’t all that difficult since the Fuse foreign code feature lets you easily drop Java and Objective-C files into Fuse projects. So damn convenient!

Another interesting challenge was to find a common state machine that could wrap both MediaPlayer and AVPlayer. These two APIs have very different state models and different ways of managing them, but I came up with a way that works well.

And lastly, there’s the lock screen support. For iOS this was quite straight forward; simply register some callbacks. For Android, however, there is a whole different game you have to play. In API level 21, Android got media style notifications, which replaced the normal lock screen controls.

This means you have to dig into the Android intent system as well as how to set up communication channels between the notification intent and the running background service. The nice thing about the lock screen controls was that since the playlists were handled in native control, the interaction with the media players was straight forward.

Features

The FuseCloud app actually has a ton of features and mechanisms built into it, and since I like enumerating things, here’s a quick list of things covered in the app (and source code):

  • Authentication with SoundCloud® using OAuth 2.0
  • Using the InterApp package to launch url in external browser and repond to URI
    Automatically refresh invalid tokens
  • Fetching from REST API
  • News feed, track search, favorites
  • Like/unlike track
  • Track artwork
  • Display comments for track
  • Post comment
  • User profile stats
  • Swipe left/right to go to previous/next track
  • Pull to refresh
  • Infinite scrolling lists
  • Swipe to reveal list item actions (unlike in favorites view)
  • Persistent UI state using the Storage API (welcome information only shows first time the app is started)®
  • HTTP Audio StreamingPlayer for iOS and Android
  • Streaming music from SoundCloud®
  • Custom seek bar
  • Background audio
  • Lock screen controls on iOS and Android
  • iOS: next, previous, play/pause, seek from lock screen
  • Lock screen artwork
  • Android media style notification: next, previous, play/pause
  • Displays artwork in notification and background
  • Playlists
  • Auto play next on song completed

Conclusion and downloads

It was a really fun experience to work on this project. I am clearly biased, but Fuse has really impressed me — again.

Creating components and structuring navigation in Fuse is becoming well defined and a really streamlined experience. Fuse’s foreign code mechanisms has proven to be an extremely good way of creating native components. It allowed me to use the original API documentation for each platform in their intended language, where it belongs, without any convoluted JavaScript wrapping.

Y̶o̶u̶ ̶c̶a̶n̶ ̶g̶e̶t̶ ̶t̶h̶e̶ ̶F̶u̶s̶e̶C̶l̶o̶u̶d̶ ̶a̶p̶p̶ ̶f̶o̶r̶ ̶b̶o̶t̶h̶ ̶A̶n̶d̶r̶o̶i̶d̶ ̶a̶n̶d̶ ̶i̶O̶S̶ ̶o̶n̶ ̶t̶h̶e̶ ̶A̶p̶p̶l̶e̶ ̶A̶p̶p̶ ̶S̶t̶o̶r̶e̶ ̶a̶n̶d̶ ̶o̶n̶ ̶G̶o̶o̶g̶l̶e̶ ̶P̶l̶a̶y̶,̶ and download the full source code on Github.

Note: the thing about creating a “real” app (with custom native components and backend integration) is that you’ll almost certainly encounter some edge cases. We are continuously improving our documentation to cover as much of the app making process as possible, so if you get stuck, please let our community and us know, and we’ll help you out :)

To learn more about Fuse, check out our ever-expanding list of examples (with source code of course), join our community (we have a nice forum and a Slack group too) or simply follow us on Twitter or Facebook.

--

--

Published in Prototypr

Prototyping, UX Design, Front-end Development and Beyond 👾 | ✍️ Write for us https://bit.ly/apply-prototypr

Responses (3)

Write a response