ExoPlayer Hacks: Part 1 – Stats For Nerds

Google’s ExoPlayer is best in class media player for Android and it supports many features which are not currently supported by stock MediaPlayer.

Best part about Exo is it’s  OpenSource-ness 🙂

In this series of blog posts I’m gonna walk through a couple of useful hacks/customisations on some of Exo components:

  • Stats For Nerds – Part 1
  • Improved Buffering – Part 2

Find the demo on github which covers both features.

Stats for Nerds:

charts

With Adaptive streaming techniques(HLS, Smooth, DASH), playback is heavily relied on connection speed, meaning resolution changes to adapt to fluctuations in bandwidth which means data consumption varies with time.

As a geek I’ve wondered what’s my n/w consumption, conn speed & sometimes buffer length when I stream a video online. Youtube on Chrome answered these concerns with ‘Stats For Nerd’ mode which depicts all those stats using simple dynamic charts and I found it intriguing. So I decided to do something similar on ExoPlayer and turned out it’s easier than I thought.

Which Stats?

  • Connection Speed:
  • Network Activity
  • Buffer Health
  • Dropped Frames
  • Video & Screen Resolutions

Code Please

  • Connection speed & n/w activity can be obtained by passing BandwidthMeter.EventListener while creating DefaultBandwidthMeter object.
bandwidthMeter = new DefaultBandwidthMeter(mUiUpdateHandler, new BandwidthMeter.EventListener() {
    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
        bitrateEstimate = bitrate;
        bytesDownloaded = bytes;
    }
});
  • ExoPlayer exposes all available Tracks and their corresponding Formats and setting a Video debug listener will give update you whenever input video format changes. Format is a container of all meta data related to a Video Rendition (width, height, bitrate etc.)
  • Dropped frames can be obtained by Video debug listener too.
player.setVideoDebugListener(new VideoRendererEventListener() {
  
    ....
    @Override
    public void onVideoInputFormatChanged(Format format) {
        currentVideoFormat = format;
    }

    @Override
    public void onDroppedFrames(int count, long elapsedMs) {
        droppedFrames += count;
    }
    ......
}

 Buffer health & LoadControl

LoadControl is an Exo Component interface to control buffering of Media and there is a DefaultLoadControl in SDK which takes of when to start buffering and for how long.

We can create our version of DefaultLoadControl with an event listener & a handler to notify player on a buffer data. Check CustomLoadControl class for more info.

player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, new CustomLoadControl(new CustomLoadControl.EventListener() {
    @Override
    public void onBufferedDurationSample(long bufferedDurationUs) {
        bufferedDurationMs = bufferedDurationUs;
    }
}, mUiUpdateHandler));
player.addListener(this);

OK. How to draw those awesome charts?

MPAndroidChart is a second to none charting library in Android and it covers all our charting requirements with no exceptions.

Create a Handler to repetitively call to itself every 500ms to update speed & buffer data and N/W activity has to be updated atleast with 1100ms so that updates wont overlap.

private void startPlayerStats() {
    mUiUpdateHandler.removeMessages(MSG_UPDATE_STATS);
    mUiUpdateHandler.removeMessages(MSG_UPDATE_STATS_NW_ONLY);
    depictPlayerStats();
    depictPlayerNWStats();
}

protected void depictPlayerStats() {
    if (!canShowStats())
        return;
    String buffer = DemoUtil.getFormattedDouble((bufferedDurationMs / Math.pow(10, 6)), 1);
    String brEstimate = DemoUtil.getFormattedDouble((bitrateEstimate / Math.pow(10, 3)), 1);
    updateStatChart(player_stats_health_chart, Float.parseFloat(buffer), ColorTemplate.getHoloBlue(), "Buffer Health: " + buffer + " s");
    updateStatChart(player_stats_speed_chart, Float.parseFloat(brEstimate), Color.LTGRAY, "Conn Speed: " + DemoUtil.humanReadableByteCount(
            bitrateEstimate, true, true) + "ps");
    player_stats_size.setText("Screen Dimensions: " + simpleExoPlayerView.getWidth() + " x " + simpleExoPlayerView.getHeight());
    player_stats_res.setText("Video Resolution: " + (null != currentVideoFormat ? (currentVideoFormat.width + " x " + currentVideoFormat.height) : "NA"));
    player_stats_dropframes.setText("Dropped Frames: " + droppedFrames);
    mUiUpdateHandler.sendEmptyMessageDelayed(MSG_UPDATE_STATS, 500);
}

protected void depictPlayerNWStats() {
    if (!canShowStats())
        return;
    updateStatChart(player_stats_nw_chart, (float) (bytesDownloaded / Math.pow(10, 3)), Color.CYAN, "Network Activity: " + DemoUtil.humanReadableByteCount(
            bytesDownloaded, true));
    bytesDownloaded = 0;
    mUiUpdateHandler.sendEmptyMessageDelayed(MSG_UPDATE_STATS_NW_ONLY, 1100);
}

 

Follow my next blog in series for Part 2 – Improved Buffering.