4. 实现音频播放器缓冲与进度回调
任务说明
本案例实现一个Android音频播放器核心模块,支持音频文件加载、播放/暂停控制、缓冲状态上报与播放进度回调。目标是在不依赖系统MediaPlayer封装的前提下,基于MediaCodec + AudioTrack构建最小可用的音频播放链路,并通过回调接口将缓冲百分比与播放位置(毫秒)实时通知上层。
核心实现
// AudioPlayerEngine.java
public class AudioPlayerEngine {
public interface Callback {
void onBuffering(int percent); // 0-100
void onProgress(long positionMs); // 当前播放位置
void onStateChanged(int state); // 0=IDLE,1=BUFFERING,2=PLAYING,3=PAUSED,4=COMPLETED
}
private static final int STATE_IDLE = 0;
private static final int STATE_BUFFERING = 1;
private static final int STATE_PLAYING = 2;
private static final int STATE_PAUSED = 3;
private static final int STATE_COMPLETED = 4;
private final Callback callback;
private MediaExtractor extractor;
private MediaCodec codec;
private AudioTrack audioTrack;
private Thread decodeThread;
private volatile boolean isPlaying = false;
private volatile boolean isPaused = false;
private volatile boolean isEOS = false;
private final Object lock = new Object();
// 缓冲统计
private long totalBytes = 0;
private long bufferedBytes = 0;
private long lastProgressMs = 0;
// 音频参数(从MediaFormat读取)
private int sampleRate = 44100;
private int channelCount = 2;
private int pcmFormat = AudioFormat.ENCODING_PCM_16BIT;
public AudioPlayerEngine(Callback cb) {
this.callback = cb;
}
public void load(String filePath) throws IOException {
release();
extractor = new MediaExtractor();
extractor.setDataSource(filePath);
int trackIndex = selectAudioTrack(extractor);
if (trackIndex < 0) throw new IOException("No audio track found");
extractor.selectTrack(trackIndex);
MediaFormat format = extractor.getTrackFormat(trackIndex);
sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
String mime = format.getString(MediaFormat.KEY_MIME);
codec = MediaCodec.createDecoderByType(mime);
codec.configure(format, null, null, 0);
codec.start();
int minBufSize = AudioTrack.getMinBufferSize(sampleRate,
channelCount == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO,
pcmFormat);
audioTrack = new AudioTrack.Builder()
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(pcmFormat)
.setChannelMask(channelCount == 1 ?
AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO)
.build())
.setBufferSizeInBytes(Math.max(minBufSize, 64 * 1024))
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
// 获取文件总大小用于缓冲百分比计算
File file = new File(filePath);
totalBytes = file.length();
bufferedBytes = 0;
isEOS = false;
callback.onStateChanged(STATE_IDLE);
}
private int selectAudioTrack(MediaExtractor extractor) {
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime != null && mime.startsWith("audio/")) {
return i;
}
}
return -1;
}
public void play() {
if (codec == null || audioTrack == null) return;
if (isPlaying) return;
isPlaying = true;
isPaused = false;
audioTrack.play();
callback.onStateChanged(STATE_BUFFERING);
decodeThread = new Thread(() -> {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean inputDone = false;
boolean outputDone = false;
long presentationTimeUs = 0;
while (isPlaying && !outputDone) {
// 暂停处理
synchronized (lock) {
while (isPaused && isPlaying) {
try { lock.wait(); } catch (InterruptedException e) { return; }
}
}
if (!isPlaying) break;
// 输入缓冲
if (!inputDone) {
int inIndex = codec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(inIndex);
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
codec.queueInputBuffer(inIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
codec.queueInputBuffer(inIndex, 0, sampleSize,
extractor.getSampleTime(), 0);
extractor.advance();
bufferedBytes += sampleSize;
// 上报缓冲进度
int percent = (int) (bufferedBytes * 100 / totalBytes);
callback.onBuffering(Math.min(percent, 100));
}
}
}
// 输出缓冲
int outIndex = codec.dequeueOutputBuffer(info, 10000);
if (outIndex >= 0) {
if (info.size > 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outIndex);
byte[] pcmData = new byte[info.size];
outputBuffer.get(pcmData);
outputBuffer.clear();
audioTrack.write(pcmData, 0, pcmData.length);
// 上报播放进度
presentationTimeUs = info.presentationTimeUs;
long posMs = presentationTimeUs / 1000;
if (posMs != lastProgressMs) {
lastProgressMs = posMs;
callback.onProgress(posMs);
}
callback.onStateChanged(STATE_PLAYING);
}
codec.releaseOutputBuffer(outIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
outputDone = true;
callback.onStateChanged(STATE_COMPLETED);
}
} else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 格式变化,忽略(已固定参数)
}
}
// 清理
try {
codec.stop();
codec.release();
audioTrack.stop();
audioTrack.release();
} catch (Exception ignored) {}
codec = null;
audioTrack = null;
isPlaying = false;
});
decodeThread.start();
}
public void pause() {
synchronized (lock) {
if (isPlaying && !isPaused) {
isPaused = true;
audioTrack.pause();
callback.onStateChanged(STATE_PAUSED);
}
}
}
public void resume() {
synchronized (lock) {
if (isPlaying && isPaused) {
isPaused = false;
audioTrack.play();
callback.onStateChanged(STATE_PLAYING);
lock.notifyAll();
}
}
}
public void seekTo(long positionMs) {
if (extractor != null) {
extractor.seekTo(positionMs * 1000,
MediaExtractor.SEEK_TO_CLOSEST_SYNC);
lastProgressMs = positionMs;
callback.onProgress(positionMs);
}
}
public void release() {
isPlaying = false;
isPaused = false;
synchronized (lock) {
lock.notifyAll();
}
if (decodeThread != null) {
try { decodeThread.join(500); } catch (InterruptedException ignored) {}
decodeThread = null;
}
if (codec != null) {
try { codec.stop(); codec.release(); } catch (Exception ignored) {}
codec = null;
}
if (audioTrack != null) {
try { audioTrack.stop(); audioTrack.release(); } catch (Exception ignored) {}
audioTrack = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
}
调用示例
// MainActivity.java 片段
AudioPlayerEngine player = new AudioPlayerEngine(new AudioPlayerEngine.Callback() {
@Override
public void onBuffering(int percent) {
Log.d("Player", "缓冲进度: " + percent + "%");
}
@Override
public void onProgress(long positionMs) {
Log.d("Player", "播放位置: " + positionMs + "ms");
}
@Override
public void onStateChanged(int state) {
Log.d("Player", "状态: " + state);
}
});
try {
player.load("/sdcard/Music/test.mp3");
player.play();
// 3秒后暂停
new Handler().postDelayed(() -> player.pause(), 3000);
// 5秒后恢复
new Handler().postDelayed(() -> player.resume(), 5000);
} catch (IOException e) {
e.printStackTrace();
}
验证方法
在Android设备或模拟器上运行,将音频文件路径替换为实际存在的MP3/AAC文件。观察Logcat输出:
- 缓冲回调:播放开始后应看到
缓冲进度从0逐步增长到100; - 进度回调:播放过程中
播放位置应持续递增,暂停时停止,恢复后继续; - 状态切换:依次出现IDLE→BUFFERING→PLAYING→PAUSED→PLAYING→COMPLETED;
- 判定标准:音频正常输出,无卡顿或崩溃,回调数值与播放行为一致。
关键说明
- 缓冲百分比:基于已读取字节数/文件总字节数计算,反映数据源读取进度而非网络缓冲;
- 进度回调:使用MediaCodec输出的
presentationTimeUs,与音频实际播放位置一致; - 线程模型:解码在独立线程执行,回调直接在该线程触发,上层需避免在回调中执行耗时操作;
- 权限需求:读取音频文件需在AndroidManifest.xml中添加
READ_EXTERNAL_STORAGE权限(Android 6+需动态申请)。