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,434