Files
XoséandClaude Opus 5.5 c771aa01bd Podlite 1.12: set speed and skipping silences before the audio opens; reopen on a skip toggle
ExoPlayer 2.18 sets up skipping silences when it opens the audio, so the engine now gets both settings before
prepare, and turning the option on or off while an episode plays reopens it at the same spot. Tested on the
Xperia J against test/mock_abs.py: 2x speed, silences skipped, the toggle mid-play, notification buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:33:51 +01:00

135 lines
6.3 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); }
/** Before prepare for skipping silences to work: ExoPlayer 2.18 switches it on only when it sets up the audio. */
public void setSpeed(float speed, boolean skipSilence) {
player.setPlaybackParameters(new PlaybackParameters(speed));
player.setSkipSilenceEnabled(skipSilence);
}
public void release() {
listener = null;
player.release();
}
}