Stories Behind SpotDJ

Yuki Li
10 min readFeb 16, 2019

--

SpotDJ is a serverless web app for anyone with a Spotify account to play DJ with their friends or coworkers by simply sharing a unique URL. Any listener who visits that URL will have their Spotify player automatically synchronized with the DJ via SpotDJ — it’s a lot of fun!

Introduction

As a bit of background, I’m a junior developer, and I started programming full-time 8 months ago. SpotDJ is written entirely client-side in ReasonML, and the implementation was pretty interesting for me since this is only my second ReasonML web app, after my first professional Reason project ~4 months ago. SpotDJ is a collaboration with Xavier Cazalot (@xavxyz ), who is excited about exploring ReasonML and creating products on the Internet, and with Sean Grove (@sgrove), who builds apps on top of OneGraph.

My goal with this project was to do a deep dive into a web app front-end implementation with ReasonML, especially animation and css-in-js.

To that end, SpotDJ is mainly built with

I’ll describe my high-level thoughts on designing SpotDJ and some low-level experiments that I tried below, but first, go try it at https://spotdj.onegraphapp.com/!

Phase 1 — Basic Spotify player activity and rendering

To kick things off, I wanted to start by building the basic structure with some real data from Spotify. With the bones, I can add to the meat of design much easier later.

Authenticate!

To start with though, I needed to handle authorization to access the Spotify APIs on behalf of the user (at this stage, just me). This is by far the least interesting part of the project and usually is very frustrating. However, OneGraph made this pretty easy with their auth library. I spent ~10 minutes on the logic to log in/out, and another ~30 minutes implementing the pages and wiring up the actions.

Fetch data!

In total, I spent less than an hour on the basic infrastructure, including Spotify authorization, fetching real-time data via Spotify APIs, and writing BuckleScript bindings to use the OneGraph auth library with Reason. OneGraph made interacting with the Spotify API so easy and simple that throughout the development, I never visited the Spotify’s API documentation page even once.

With the basic page structure and functionality built, I could dive into the more interesting side of the project — polishing the UI/UX of the page.

Animation: Transition from the “Login Prompt” to the “DJ/Audience View”

The first thing our user sees when landing on SpotDJ is the login page, asking the user to give us access to the Spotify API via their account. Upon login, however, the page shifts to the DJ/Audience view. Without a smooth transition, the jump between the two pages causes a short flash of a blank screen and feels momentarily confusing (“The screen went blank, is the app still ok?”).

In order to provide a more continuous experience, we need to signal to the user, “You’re still in the SpotDJ app”. Xavier suggested a very simple, effective way of conveying this by preventing the re-rendering of the page title. The persistent existence of the page title prevents the flash of a blank screen and so provide a smooth experience upon page transition.

Animation: Intermediate loading screen

Once the user logs in, we immediately start pulling in data from the Spotify API. But as fast as their API may be, there will still be a (hopefully) short loading period before we can display the player information on the page.

We could simply render some text saying loading... or show a spinner on the page. But because the loading time tends to be extremely short, the user may not even have enough time to finish reading the word “loading”. Therefore, we applied a fade-in animation to make the loading phase unnoticeable.

While the loading time can vary for many different reasons, we should still handle the case where the loading time exceeds the duration of the fade-in animation. For the initial version of SpotDJ, I chose to skip this case. But thinking about SpotDJ v2, I’d like to display a basic placeholder with grey shapes to convey the loading status. And, if the loading time exceeds three seconds, we display a more direct message or progress bar. Still, these would be nice features to add. But in the meantime, our fade-in animation is satisfying the basic UX needs.

Animation: Sound-Wave, Player Statu Playing vs Paused

One of the most important pieces of info we need to communicate to the user is the player status — is Spotify current playing, or paused?

Displaying traditional play/pause icons could be confusing because we don’t allow users to control their own Spotify player via SpotDJ. Seeing the play/pause icons may mislead the user into thinking that they can click on the icon and play or pause the player, rather than thinking it’s a read-only status indicator.

I wanted a better way to convey the player status and came up with the idea of sound wave animation.

Initially, I wanted to mimic the wave movement by changing the number of visible sound blocks. However, there are 24 blocks in total (3 columns of 8 blocks each). Customizing the animation timing for each of the blocks is a lot of work.

As I was implementing the sound wave with the animation-per-block approach, it felt like quite a tedious work. I started to wonder whether there might be another better (or, honestly, lazier) way of doing this.

“If necessity is the mother of invention, then laziness is the father.”

I realized that changing the height of each sound wave column, and using a simple overflow:hide attribute would also achieve a moving sound-wave effect. It’s a significantly simpler (and easier) implementation! One side effect of this implementation though is that the sound blocks are smoothly cut through the middle as the column height changes. Still, while I’m not sure what other UI designers would think about my shortcut, in this case, I’m the designer, so I’ll call the shots! We don’t always have to be held to historical accuracy in our designs, so I didn’t see any reason the sound block shouldn’t be cut through the middle.

It’s just a coincidence that my decision also coincided with the easier and more efficient sound-wave animation implementation. Probably. Maybe.

Phase 2 — Standing up to the “serverful” establishment with a “serverless”, p2p WebRTC Spotify sync strategy

Even though OneGraph handles all our authentication and API access for the data we need, we still need to replicate the DJ’s player state across to the listeners. My first thought was to use a server, but that introduces a whole lot of complexity, and a single-point-of-failure, which is no good. Again, l̶a̶z̶i̶n̶e̶s̶s̶lateral-thinking to the rescue!

What if we use WebRTC to do away with a server entirely? Then users could even potentially git clone the app themselves and sync while running two instances on their own localhost! And I don’t have to write or run a server! Win-win. Win.

I decided to used PeerJS, which seemed like a comprehensive WebRTC library, to send the synchronization data. On the DJ side, we used OneGraph to find the currently playing song on Spotify and its position. We then broadcast that data to all of the listeners of the DJ, who check their Spotify player state every ~1 second or so. We finally compare the two player states — if the trackId on both players doesn’t match, or if the progressMs (the song position) is more than 10s out of sync we use OneGraph on the listener’s side to synch the trackId and the progressMs in a single call. Pretty cool.

DJ: Hey y’all, I’m listening to 3:20 of Avicii — Muja

Listener 1: Well, I’m listening to 1:20 of The Knife — Heartbeats! I’ll switch to your song :D

Listener 1: Hey OneGraph! Would you mind playing 3:20 of Avicii — Muja?

Listener 2: I’m listening to 3:15 of Avicii — Muja, that’s close enough

Listener 3: What?!? I’m way back at 3:01 of Avicii — Muja! OneGraph, hook me up!

OneGraph: Listener 1, I got your back — Done!

OneGraph: Listener 3, you’re back at 3:20 now!

Now, we have a fully functional SpotDJ, that can:

1. Display the player information

2. Send/receive Spotify player data

3. Sync the player states

This is Cool~!

Still, I’d like to make SpotDJ feel more interactive. What if a listener likes a song and they want to find out more about it, but just before they get back to the app the song finishes — SpotDJ jumps to the next song, forever dooming our listener to wondering what the song that got away was.

What a dark world that would be, with no way for our intrepid user to track backward! We mustn’t let such a world come forth!

Let’s display a simple history of the played songs instead.

The most space-efficient way of displaying song history is to use a simple list. But it’s a bit plain, certainly not very entertaining. I want to make SpotDJ a web app that’s fun and engaging, something people would find some happiness in simply playing with.

Instead of a history list, let’s display the track history using something like the famous cover-flow animation.

Cover-flow Attempt 1: Player history via CSS (CSS-in-JS)

For my first try at implementing cover flow, I used CSS keyframes to animate the flip-to-left effect, and decided to display a maximum of three history tracks (accepting that we don’t want an entirely dark world, but also that pobody’s nerfect).

As a new song starts, every track shifts one position to the left, so the current playing track always stays centered. I used flex-flow to position the track components. Since I’m using React (or ReasonReact to be specific), I triggered the animation by assigning the track’s array index as the element’s key, so as to force the track component to re-render.

But take a look at the result below: the animation is not very smooth, coordinating the timing for all the pieces is very challenging, and it really doesn’t feel very interactive.

Cover-flow Attempt 2: ReactMotion

So css-animations-in-js is probably not the way to go if we want a fluid, interactive history list. Also, simply displaying the previous three tracks feels like too much of a compromise. We must strive for a brighter, more capable world.

Instead, I decided to display the entire history (users would have to listen to a lot of different tracks before the performance was too bad) and allow users to fluidly scroll horizontally across the older tracks. For this attempt, after watching Chengcheng’s ReactEurope 2015 talk on animation in React, I decided to get ReactMotion a try. I thought it might give me two benefits:

1. Make the animation motion look more realistic via springs

2. Simplify the timing and positioning by moving everything into styles with dynamic values

Instead of relying on the browser to flow the track list out correctly, I absolutely position the elements myself and translate them according to internal state in the component. Taking this approach, I have to do some additional effort to center the currently playing track on first load (calculating the center from the page width, etc.) and on resize.

I then manually calculate the horizontal position of each additional item, with older items to the left (because when I think about time, older things exit to the left and newer things enter from the right).

One bug I initially hit with this approach what that I can only scroll if items overflow to the right, but not when items overflow to the left. I’m not sure why — here’s a code example of the problem, if you do know, please let me know so I can send you some very sincere internet points, and also my thanks.

The workaround I tried was to add another list of invisible child elements that overflows to the right, and trigger the parent container to scroll to the end of the right-most element when the component is first loaded. This way, the currently playing track is placed on the center of the screen by default, and I can scroll to see the items that are overflowed to the left. However, this method only works well for a sufficiently long list of elements. overflow:scroll needs the element list to be long enough to trigger the overflow property (unsurprisingly).

After struggling for more than half of a day with this annoying overflow scroll issue, I realized that I can just use the mouse-wheel event instead — no overflow at all, thank you very much!

We calculate the diff based on the deltaX of the event, and apply the changes to the track component via the translate css attribute instead. This is more work to calculate, but it unblocks the “scroll-to-the-left” issue and ultimately provides more flexibility and control in the elements.

The future: Milestones for SpotDJ 2.0

For the SpotDJ 2.0, I’d like to:

  • Add an animation to handle cases where loading takes longer
  • Use the media capabilities of WebRTC to add a voice broadcast for the DJ to their audiences — give it that real live-radio feeling
  • Instead of a simple DJ centered system, implement an interconnected system, where listeners can double as DJs, forming a chain of listeners connected to a nested DJ. A sort of blockchain of people.

Try it! Really! Try it now!

Again, here’s the link for SpotDJ: https://spotdj.onegraphapp.com/

Try it. Share it with your friends. Tweet about it.

And please share any of your thoughts and advice on both design and implementation! Check out the source and open issues for it on the GitHub repo while you’re at it!

--

--