Outcast 2: Pushing the Apple Watch's limits

Today we are releasing a major update to Outcast, our podcasting app made specifically for Apple Watch.

Outcast is a fully-standalone Apple Watch app, meaning you can browse and search for podcasts, download episodes, and play them back, all without your iPhone.

There are two cornerstone features of this update:

  1. Streaming: the ability to stream podcast episodes, so you can begin playback immediately.
  2. Playlists: The ability to manage a playback queue, so the next episode automatically advances, even with Outcast in the background.

You can download Outcast for just $0.99 from outcast.app.

Note: We're aware there are some major podcast-related features being added to watchOS 5. We'll have more to say on them and how they relate to Outcast in a future update.

Both the streaming and playlist solutions we've come up with are somewhat novel, so I want to cover them in a little more detail.

Streaming Remote Audio Using WKAudioFilePlayer

One of the new features we've added is the ability to stream podcast episodes. You can see in the screenshots below:

  1. An option to stream a file instead of downloading it first
  2. Playback of a streaming file. Note the cloud icon, and the partial download progress (separate to the playback progress)
  3. A fully-buffered file, which now behaves identically to regularly downloaded episodes.

Streaming an episode with Outcast

Currently, the only way to play background audio on the Apple Watch is to use the WKAudioFilePlayer class (and its child WKAudioFileQueuePlayer class, which we use for playlists).

In order to create a playable item, the WKAudioFileAsset class must be used. Unfortunately, it doesn't support remote URLs (link):


WKAudioFileAsset limitation


The Streaming Algorithm

Since streaming is essentially just starting playback once more bytes are received, we figured we could achieve this with the tools available to us:

  1. Start downloading the episode as usual.
  2. As soon as some threshold of bytes was received, save those bytes to disk and pass the file to WKAudioFileAsset.
  3. Keep the download active, and when more bytes are downloaded, pass a new file containing all of the bytes again to WKAudioFileAsset.

Switching Files

We were hoping that if you recorded, say, 100 KB to fileA.mp3, then if you recorded another 100 KB to that same file before playback concluded, it would continue to play until the end of 200 KB.

Unfortunately, WKAudioFilePlayer only plays the data that was available when the player was initialized.

Because of this, a new player item had to be created whenever bytes were available.

In reality, we continue to write newly-received data to the same file, and then pass the same file back to the player when the previous playback concludes. This works remarkably well!

Tracking Progress

One of the difficult parts of getting this to work was accurately tracking playback progress. When the player switches from the first buffer ("A"), to the second buffer ("A + B"), the player needs to start playing back from the end of "A".

When using WKAudioFileAsset, you can access the duration: TimeInterval property. Unfortunately, this may be the duration of the entire file, or just of the particular chunk. Because of this, we can't rely on currentTime for tracking progress in this particular use-case.

Instead, we make use of the "did play to end of file" notification (WKAudioFilePlayerItemDidPlayToEndTime), and use the current progress at that time (end of "A") as the starting offset for the new player item ("A + B").

Trade Offs

Unfortunately, there are some tradeoffs we've had to make with the streaming solution in Outcast.

  1. Just like with normal downloads in Outcast, you must keep the app active and screen on until the file is fully buffered.
  2. There's a slight audio pause when the streamer switches from the old file to the new file.

There are some things we plan on improving in subsequent updates though:

  1. Because we had to switch from URLSessionDownloadTask to URLSessionDataTask, resume isn't currently support if the download cuts out.
  2. Seeking forwards/backwards while buffering is not currently possible.
  3. If you're streaming, the option to move to the next item in your playlist isn't available.

Sorting Playlists With Apple Watch

The othe major new feature in Outcast is the ability to create a playlist.

Playing back multiple files is relatively straightforward using the built-in WKAudioFileQueuePlayer, but managing the actual order of playback isn't so straightforward on Apple Watch!

Initially, we planned to automate the playlist based on the files you've downloaded and your sorting preferences, but ultimately decided to give the user full control over their playlist.

You can see in the following screenshot how playlists are managed:

  1. A "next" button now appears to the right of the current item title (but only if there's an item available)
  2. The list of downloaded episodes shows a "bars" icon on the now playing item to access your playlist.
  3. All downloaded episodes appear, with the current playlist up top.
  4. You can move an item using the Digital Crown.

Managing your playlist in Outcast

The interesting part of this feature is the ability to sort items. Unlike UITableView and UICollectionView on iOS, there's no built-in way to sort items in WatchKit's WKInterfaceTable.

We wanted to implement a solution that fits the Apple Watch, so rather than dealing with drag-and-drop, we put the Digital Crown to use.

Here's what it looks like in action:

What we discovered is that WKInterfaceTable will automatically animate the following situations:

  1. When you use table.removeRows(), the rows below the one that is removed animate upward to close the newly-created gap.
  2. When you use table.insertRows(), the rows shift down to make space.

If you refer back to the video, when the "Planet Money" episode is shifted up, what's actually happening is that "Startup" episode is being remove (causing "Planet Money" to animate), then the "Startup" episode is being immediately re-inserted.

Specifically:

  1. Row 3 (Planet Money) is selected
  2. User scrolls Digital Crown upwards, meaning row 3 (Planet Money) should animate upwards
  3. Call table.removeRows() on row 2 (StartUp). This turns Planet Money into row 2, with the effect of it animating upwards.
  4. Use table.insertRows() to insert a row at position 3, then configure it with the details of StartUp.

Thanks for reading! We'd love to hear your feedback about the new version, you can email support@outcast.app, or tweet @OutcastForApple.

Outcast 2 is available now from outcast.app.


Outcast