- One build per Android level, as in Tenda and Plexamp Lite: build/Podlite.apk (Android 2.2+, versionCode 1308) and build/Podlite-api16.apk (Android 4.1+, 1316), from shared src/ plus platform/api8 or platform/api16; build.sh now runs on the local toolchain (aapt2, dx or R8, signapk.py; the JKS key converted for the run). - Android 4.1 build: ExoPlayer 2.18.7 behind an Engine interface, with playback speed (player header) and skipping silences (Settings), a 3-minute buffer and MediaPlayer as fallback for audio it can't decode; a notification and lock screen with the cover and buttons; Holo; a menu button on phones without a Menu key; metered networks for "download on Wi-Fi only". - Both builds: tlskit's Http.abort fix (a finished response's pooled connection is no longer closed under a later request); the lock screen's and newer headsets' play/pause keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
6.2 KiB
Java
134 lines
6.2 KiB
Java
package com.miniabs;
|
|
|
|
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.PlaybackParameters;
|
|
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
|
|
* playback speed and skipping silences (its own audio processing, so it works on Android 4.1), a buffer of minutes,
|
|
* and errors that say what failed. Audio focus, unplugging and the wake/Wi-Fi locks stay with PlayerService.
|
|
*/
|
|
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)};
|
|
}
|
|
};
|
|
DefaultHttpDataSource.Factory http = new DefaultHttpDataSource.Factory()
|
|
.setUserAgent("Podlite").setConnectTimeoutMs(15000).setReadTimeoutMs(30000)
|
|
.setAllowCrossProtocolRedirects(true);
|
|
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: rides out Wi-Fi gaps that MediaPlayer's 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_SPEECH).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, "Erro de reprodución: " + e.getErrorCodeName() + (why != null ? " (" + why + ")" : ""), decoder);
|
|
}
|
|
|
|
public void onTracksChanged(Tracks tracks) {
|
|
// ExoPlayer plays silence through audio it can't decode instead of failing: fail, so MediaPlayer can try
|
|
if (listener == null || failed || tracks.isEmpty() || tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, true)) return;
|
|
failed = true;
|
|
listener.onError(ExoEngine.this, "Este teléfono non pode descodificar este audio.", true);
|
|
}
|
|
});
|
|
}
|
|
|
|
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(int ms) { player.seekTo(ms); }
|
|
|
|
public int position() { return (int) player.getCurrentPosition(); }
|
|
|
|
public int duration() {
|
|
long d = player.getDuration();
|
|
return d == C.TIME_UNSET || d < 0 ? 0 : (int) d;
|
|
}
|
|
|
|
public int bufferedPercent() { return player.getBufferedPercentage(); }
|
|
|
|
public void setVolume(float v) { player.setVolume(v); }
|
|
|
|
public void setSpeed(float speed, boolean skipSilence) {
|
|
player.setPlaybackParameters(new PlaybackParameters(speed));
|
|
player.setSkipSilenceEnabled(skipSilence);
|
|
}
|
|
|
|
public void release() {
|
|
listener = null;
|
|
player.release();
|
|
}
|
|
}
|