Android Media Streaming with Exoplayer

Bay Area Android Developpers

- Dec 4th 2016 -

Easy with the Mediaplayer

String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(url);
mediaPlayer.setDisplay(surfaceView);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();


// later
mediaPlayer.pause();

// and later
mediaPlayer.seek();

// when you're finished
mediaPlayer.release();

Even easier with VideoView

VideoView mVideoView  = (VideoView)findViewById(R.id.videoview)
mVideoView.setVideoPath("http://....");
mVideoView.start();

But...

  • If anything goes wrong you're on your own
  • It's all or nothing
  • Bugs
  • Obscure error handling
  • Complex state machine
  • OEM customization
  • Missing features
07-18 10:25:10.996: D/MediaPlayer(17860): Couldn't open file on client side, trying server side
07-18 10:25:39.859: D/MediaPlayer(17860): getMetadata
07-18 10:25:45.070: E/MediaPlayer(17860): error (1, -2147483648)


07-18 13:47:14.245: E/OMXCodec(68): [OMX.qcom.video.decoder.avc] ERROR(0x8000100a, 0)

Exoplayer

  • Application level video player
  • Open Source
  • Started by google in 2014
  • Based on the MediaCodec Apis

Pros

  • More formats (Adaptive Streaming)
  • Better discontinuity detection.
  • Ability to seamlessly merge, concatenate and loop media.
  • Seeking in live.
  • Update with your app.
  • Fewer device specific issues.
  • Support for Widevine DRM
  • Peer to peer
  • Open source: everything is configurable.

Cons

  • Built on top of the MediaCodec APIs (API level 16+)
  • A bit more code and method count

Exoplayer Architecture

In practice

        player = ExoPlayerFactory.newInstance(renderers, trackSelector);
        player.setPlayWhenReady(true);
        player.prepare(mediaSource);
  • renderers
  • trackSelector
  • mediaSource

Renderers

        renderers = new Renderer[2];

        long joiningTime = 5000;
        int maxDroppedFramesToNotify = 50;
        renderers[0] = new MediaCodecVideoRenderer(this, MediaCodecSelector.DEFAULT,
                joiningTime, null, false, null, null,
                maxDroppedFramesToNotify);
        renderers[1] = new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT);

Decode and display content

TrackSelector

DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
                new AdaptiveVideoTrackSelection.Factory(bandwidthMeter);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);

Well... selects tracks

MediaSource

String uri = "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0";

DefaultHttpDataSourceFactory manifestDataSourceFactory 
                = new DefaultHttpDataSourceFactory("Meetup Demo");
DefaultHttpDataSourceFactory chunkDataSourceFactory 
                = new DefaultHttpDataSourceFactory("Meetup Demo", bandwidthMeter);
DashMediaSource mediaSource = new DashMediaSource(Uri.parse(uri), 
                manifestDataSourceFactory, new DefaultDashChunkSource.Factory(chunkDataSourceFactory),
                null, null);

Reads compressed audio/video/subtitle 

Slightly easier way :-)

// 1. Create a default TrackSelector
Handler mainHandler = new Handler();
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
    new AdaptiveVideoTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector =
    new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory);

// 2. Create a default LoadControl
LoadControl loadControl = new DefaultLoadControl();

SimpleExoPlayer player =
    ExoPlayerFactory.newSimpleInstance(context, trackSelector, loadControl);

simpleExoPlayerView.setPlayer(player);

The adaptive algorithm

  @Override
  public void updateSelectedTrack(long bufferedDurationUs) {
    long nowMs = SystemClock.elapsedRealtime();
    // Get the current and ideal selections.
    int currentSelectedIndex = selectedIndex;
    Format currentFormat = getSelectedFormat();
    int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
    Format idealFormat = getFormat(idealSelectedIndex);
    // Assume we can switch to the ideal selection.
    selectedIndex = idealSelectedIndex;
    // Revert back to the current selection if conditions are not suitable for switching.
    if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {
      if (idealFormat.bitrate > currentFormat.bitrate
          && bufferedDurationUs < minDurationForQualityIncreaseUs) {
        // The ideal track is a higher quality, but we have insufficient buffer to safely switch
        // up. Defer switching up for now.
        selectedIndex = currentSelectedIndex;
      } else if (idealFormat.bitrate < currentFormat.bitrate
          && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
        // The ideal track is a lower quality, but we have sufficient buffer to defer switching
        // down for now.
        selectedIndex = currentSelectedIndex;
      }
    }
    // If we adapted, update the trigger.
    if (selectedIndex != currentSelectedIndex) {
      reason = C.SELECTION_REASON_ADAPTIVE;
    }
  }

Gapless playback

MediaSource firstSource = new ExtractorMediaSource(firstVideoUri, ...);
MediaSource secondSource = new ExtractorMediaSource(secondVideoUri, ...);
// Plays the first video, then the second video.
ConcatenatingMediaSource concatenatedSource =
    new ConcatenatingMediaSource(firstSource, secondSource);

Exoplayer 2.0

  • Released in September
  • Mostly architectural changes
  • Not 100% stable yet (github)
  • You might or might not want to use it

Questions ?

martin.bonnin@dailymotion.com

Exoplayer and the state of Android Media Streaming

By mbonnin

Exoplayer and the state of Android Media Streaming

  • 1,317