Private
Public Access
Plexamp Lite 1.14: Android 4.1 build with ExoPlayer, lyrics, home-screen widget
- One build per Android level, as in Tenda: build/PlexampLite.apk (Android 2.2+, versionCode 1508) and build/PlexampLite-api16.apk (Android 4.1+, 1516), from shared src/ plus platform/api8 or platform/api16. - Android 4.1 build: ExoPlayer 2.18.7 behind a small Engine interface (gapless hand-over to read-ahead and downloaded files, a longer buffer, MediaPlayer fallback for tracks it can't decode), a notification and lock screen with art and buttons, a resizable widget, Holo, a menu button without a Menu key, metered networks. R8 shrinks ExoPlayer and Guava (tools/fetch_exoplayer.sh, tools/r8-rules.pro). - Both builds: lyrics in Now Playing as in the Mac app (timed lyrics follow and seek), a home-screen widget, media play/pause keys, the moon icon drawn without clipPath. - PlexampLite-noreport on the SD card stops play reports, for tests against a real server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e7476669e
commit
13f29e26c4
39 files changed
+1685
-119
No files matched your search
@@ -0,0 +1,141 @@
|
||||
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"; }
|
||||
}
|
||||
Reference in new issue
Block a user