package com.minamp; import java.io.File; import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.Looper; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.video.VideoRendererEventListener; /** * ExoPlayer 2.18 (Android 4.1+ build): audio only, decoded with Android's MediaCodec. Over MediaPlayer it brings * gapless hand-over to a queued file, a buffer of minutes instead of seconds, and errors that say what failed. * Audio focus, headphone unplugging and the wake/Wi-Fi locks stay with PlayerService, as for MediaPlayer. */ final class ExoEngine implements Engine { private final ExoPlayer player; private Listener listener; private boolean preparedSent; private boolean failed; ExoEngine(final Context c) { RenderersFactory audioOnly = new RenderersFactory() { public Renderer[] createRenderers(Handler h, VideoRendererEventListener v, AudioRendererEventListener a, TextOutput t, MetadataOutput m) { return new Renderer[] {new MediaCodecAudioRenderer(c, MediaCodecSelector.DEFAULT, h, a)}; } }; // the loopback StreamProxy: it never takes long to answer, but may wait on the server for data DefaultHttpDataSource.Factory http = new DefaultHttpDataSource.Factory() .setUserAgent("PlexampLite").setConnectTimeoutMs(8000).setReadTimeoutMs(30000); ProgressiveMediaSource.Factory sources = new ProgressiveMediaSource.Factory(new DefaultDataSource.Factory(c, http), new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)); player = new ExoPlayer.Builder(c, audioOnly, sources) // up to 3 minutes ahead in memory (a few MB of compressed audio): rides out Wi-Fi gaps that // MediaPlayer's few seconds of buffer couldn't .setLoadControl(new DefaultLoadControl.Builder() .setBufferDurationsMs(60000, 180000, 1500, 3000) .setPrioritizeTimeOverSizeThresholds(true) .build()) .setAudioAttributes(new AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA).setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).build(), false) .setHandleAudioBecomingNoisy(false) .setWakeMode(C.WAKE_MODE_LOCAL) .setLooper(Looper.getMainLooper()) .build(); player.addListener(new Player.Listener() { public void onPlaybackStateChanged(int state) { if (listener == null || failed) return; if (state == Player.STATE_READY && !preparedSent) { preparedSent = true; listener.onPrepared(ExoEngine.this); } else if (state == Player.STATE_ENDED) { listener.onCompletion(ExoEngine.this); } } public void onPlayerError(PlaybackException e) { if (listener == null || failed) return; failed = true; int code = e.errorCode; // 4xxx: decoding; 3003: a container this player can't read. Android's MediaPlayer may manage. boolean decoder = (code >= 4000 && code < 5000) || code == PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED; Throwable cause = e.getCause(); String why = cause != null && cause.getMessage() != null ? cause.getMessage() : e.getMessage(); listener.onError(ExoEngine.this, e.getErrorCodeName() + (why != null ? ": " + why : ""), decoder); } public void onTracksChanged(Tracks tracks) { // ExoPlayer plays silence through a track it can't decode instead of failing: fail, so another try can if (listener == null || failed || tracks.isEmpty() || tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, true)) return; failed = true; listener.onError(ExoEngine.this, "no decoder on this phone for this audio", true); } public void onMediaItemTransition(MediaItem item, int reason) { if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_AUTO || listener == null) return; // the queued file took over: forget the finished one int i = player.getCurrentMediaItemIndex(); if (i > 0) player.removeMediaItems(0, i); listener.onGaplessNext(ExoEngine.this); } }); } public void setListener(Listener l) { listener = l; } public void setSource(File file, String url) { player.setMediaItem(MediaItem.fromUri(file != null ? Uri.fromFile(file) : Uri.parse(url))); } public void prepare() { player.prepare(); } public void start() { player.setPlayWhenReady(true); } public void pause() { player.setPlayWhenReady(false); } public void seekTo(long ms) { player.seekTo(ms); } public long position() { return player.getCurrentPosition(); } public void setVolume(float v) { player.setVolume(v); } public boolean canSeek() { return player.isCurrentMediaItemSeekable(); } public boolean supportsNext() { return true; } public void setNext(File file) { int after = player.getCurrentMediaItemIndex() + 1; if (player.getMediaItemCount() > after) player.removeMediaItems(after, player.getMediaItemCount()); if (file != null) player.addMediaItem(MediaItem.fromUri(Uri.fromFile(file))); } public void release() { listener = null; player.release(); } public String name() { return "ExoPlayer"; } }