Droidkaigi 2019 (part 3)

10 February 2019

This is the third part in my series about my talk at DroidKaigi 2019. You can find the rest of them below:

In part 1 and part 2, I have discussed about how challenging it is to have ExoPlayer work in RecyclerView, and part of my proposal, to solve those challenges. In this part, I would continue with how resource management is implemented in my approach. And with it, how could I build comlicated UX with ease.

You may notice some lagging in this gif. This was because Android Studio is not so good in recording screen of Emulator device.

3. Resource management & More

First, let me repeat my principle when designing this approach again:

  • Balanced UX and performance

    • 2019 devices has more RAM than my Mac
    • And more CPU cores too
    • UX gain requries Performance loss
  • Avoid resource reloading as much as possible.

    • No reload on config change
  • Well lifecycle control does the magic

    • Design lifecycle flow if need

In part 2, my components could easily accomplish the last 2 principles. What remains is The balance between UX and performance.

First, let get back to how Playback and Playable and Target work together:

So all the actual playback will be dispatched to Playable once Playback receives the signal from Target behavior. You can think that: if I implementation actual playback logic in Playable, it will finish our approach here, and because Playable is created on demand, and kept alive across complicated lifecycle transitions, and will be cleaned up once the Playback for it is no longer available (which means that Playable is not bound to any Target). This way, we ensure that there is no Playable lives longer than it should be, and the number of Playable available at a time should be less than or equal to the number of Target available on the screen (eg: number of PlayerView visible on screen).

Here I say "less than or equal" because, depending on the implementation, the number of Playable will in fact not acceed that of Target. On the other hand, an unbound Playable will be ignore, which explains the "less than or equal".

In my implementation, I would like to make Playable to be more flexible: because we do not care about actual playback logic here, so if we don't limit ourselves to just Video playback, but to whatever playable (eg: gif), we can build much more useful things with these components.

To make Playable extensible, I define one more abstract component that provides actual playback logic, and let Playable to depend on it. I call it the Bridge:

Bridge: provides Playable with actual playback resource/logic

Below is how Bridge looks like, and why it will empower the whole system:

As you can see, Bridge shares almost the same interface with Playable. Once created, Playable will also create a Bridge and levarage the callback from Playback to it. As Playable is bound to Target, it will then pass that Target to Bridge accordingly. When the Playback is no longer available, Playable will then release the Bridge and cleans up itself, make sure no resources leaking.

With this design, I can put whatever playback logic I want to the system, via the detailed implementation of Bridge (yeah, that's why I name it Bridge). It can use the backend API from ExoPlayer, or MediaPlayer for fallback, or the ijkplayer which is another well-known media playback library built on top of ffmpeg, or something else (APIs for non-Video playback for example).

Below figure shows the flexible in practice:

The left video is my current implementation, and the one on the right uses the dummy Bridge implementation to showcase in DroidKaigi (which I could not >.<).

It is about how flexible this approach can be. Next, I will talk about the Brigde for ExoPlayer: ExoBridge.

Before going into this implementation, below is the core definition of a Bridge

interface Bridge {

  //  set/get
  var playerView: PlayerView?

  fun prepare()

  /** [com.google.android.exoplayer2.Player.setPlayWhenReady] to true */
  fun play()

  /** [com.google.android.exoplayer2.Player.setPlayWhenReady] to false */
  fun pause()

  fun release()

3.1 ExoBridge implementation

First, here is the principle that ExoBridge bases on:

- “More than one ExoPlayer instance” strategy
  - Globally manage a Player Pool.
  - Create ExoPlayer instance on demand, release to Pool for reuse.
  - Cleanup Pool when no Managers are available.
- Prepare resources as late as possible: in playable.play()
  - Resource warming up will affect the scroll performance.
  - Too early = frame drop at bad timing.

Implementation of ExoBridge can be summarized as below:

- Global Singleton manages Player instances in Pool
  - Create and Release by Bridge’s demand.
- Target: PlayerView.
- prepare(): Do nothing to ExoPlayer resource. Just to init listeners.
- play(): lazily create resource, then call play from ExoPlayer instance.
  - Ensure ExoPlayer instance, create if need. 
  - Ensure MediaSource instance, create if need.
- pause(): normally pause if in playing state
- release(): release MediaSource, “release” ExoPlayer instance to 
ExoPlayer instance Pool
- ExoPlayer is actually released when no Managers are available

You can find the full-version of this in my repository.

Bring it all together, we have the following figure, describing how my approach works:

And finally, below is how you can use it from your code:

  .copy(tag = videoUrl)

With one line above, your playerView will be prepared once visible, and start the playback automatically. If user scrolls the playerView offscreen (or partly visible, not good enough for UX), it will pause the playback automatically.

If you want to keep this playback continues in new lifecycle (says new Activity), in that Activity's onCreate or onStart, call:

Kohii[activity].findPlayable(tag = videoUrl)

For exact usage, please refer to the repository.

That is. I have shared with all of you about my new approach for using ExoPlayer in RecyclerView. At the moment, many design concepts are in my bluesprint. I'm working hard to bring it to life and hoping it can help a lot of developers those are having the same need can find one more solution.