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;
  • 判定标准:音频正常输出,无卡顿或崩溃,回调数值与播放行为一致。
音频播放器缓冲与进度回调时序 App AudioPlayerEngine MediaCodec AudioTrack Callback load(path) configure+start new AudioTrack+play() play() onStateChanged(BUFFERING) 解码循环 dequeueInputBuffer queueInputBuffer dequeueOutputBuffer onBuffering(percent) write(pcmData) onProgress(posMs) pause() audioTrack.pause() onStateChanged(PAUSED)

关键说明

  • 缓冲百分比:基于已读取字节数/文件总字节数计算,反映数据源读取进度而非网络缓冲;
  • 进度回调:使用MediaCodec输出的presentationTimeUs,与音频实际播放位置一致;
  • 线程模型:解码在独立线程执行,回调直接在该线程触发,上层需避免在回调中执行耗时操作;
  • 权限需求:读取音频文件需在AndroidManifest.xml中添加READ_EXTERNAL_STORAGE权限(Android 6+需动态申请)。