Android音视频学习:MediaCodec 硬编 aac

AudioRecord 录制音频
MediaCodec 把录制的 PCM 流硬编为 aac (Advanced Audio Coding)
裸的 PCM 流是不能直接播放的,要加上 ADTS (Audio Data Transport Stream) 头
大部分播放器可以播放 aac

codec 5.0 以上推荐异步获取 buffer。这里用 3 个线程一个录制线程、一个 codec 线程、一个写 aac 文件的线程。用两个阻塞队列 (ArrayBlockingQueue)来做线程间的数据传递。

private static final int QUEUE_SIZE = 10;
private volatile boolean isRecording = false;
private volatile boolean isCodecComplete = false;

ArrayBlockingQueue inputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE); // codec 输入队列
ArrayBlockingQueue outputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE); // codec 输出队列

录制线程

class RecordRunnable implements Runnable {
    int bufferSize;

    RecordRunnable(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    @Override
    public void run() {
        byte[] buffer = new byte[bufferSize];

        while (isRecording) {
            int len = audioRecord.read(buffer, 0, bufferSize);
            if (len <= 0) {
                continue;
            }

            byte[] data = new byte[len];
            System.arraycopy(buffer, 0, data, 0, len);

            try {
                Log.d(TAG, "inputQueue 等待入队");
                inputQueue.put(data); // 阻塞方法
                Log.d(TAG, "inputQueue 入队了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        audioRecord.stop();
        audioRecord.release();
        Log.d(TAG, "录制线程结束");
    }
}

codec 线程(这里是主线程)

// 默认在调用它的线程中运行,这里是主线程。也可以传一个 handle 进去让它运行在 handler 所在的线程
// 异步模式需要 API Level 21(5.0) 以上
// API Level 23 (6.0) 以上才支持传 handler 参数
mediaCodec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
        // input buffer 用完后要及时提交(queueInputBuffer)到 codec ,否则可能导致 input buffer 被占满
        Log.d(TAG, "onInputBufferAvailable");
        byte[] data = null;
        if (isRecording) {
            try {
                data = inputQueue.take(); // 阻塞方法,不要过多的 take, 队列为空时 take 会阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            // 录制结束后如果队列非空要处理完队列中的剩余数据
            Log.d(TAG, "11111111111111111 " + inputQueue.size());
            data = inputQueue.poll();
        }

        if (data != null) {
            // 只要有数据就放入 codec (无论录制是否结束)
            ByteBuffer inputBuffer = codec.getInputBuffer(index);
            inputBuffer.clear();
            inputBuffer.put(data);
            codec.queueInputBuffer(index, 0, data.length, System.currentTimeMillis() * 1000, 0);
            Log.d(TAG, "codec 入队 " + Thread.currentThread().getName());
        } else if (!isRecording) {
            // 录制结束且队列中没有数据说明已经没有输入了,用一个带 BUFFER_FLAG_END_OF_STREAM 的空 buffer 标志输入结束
            mediaCodec.queueInputBuffer(index, 0, 0, System.currentTimeMillis() * 1000, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            Log.d(TAG, "codec 最后一个空 buffer 了");
        }
    }

    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
        // output buffer 用完后要及时释放(releaseOutputBuffer)回 codec, 否则可能导致 output buffer 被占满
        Log.d(TAG, "codec 出队 " + Thread.currentThread().getName() + " size " + info.size + " flag " + info.flags);
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
        byte[] outData = new byte[info.size];
        outputBuffer.get(outData);
        codec.releaseOutputBuffer(index, false);

        try {
            Log.d(TAG, "outputQueue 等待入队");
            outputQueue.put(outData);
            Log.d(TAG, "outputQueue 入队");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            Log.d(TAG, "flags " + info.flags);
            Log.d(TAG, "codec 处理完毕了");
            isCodecComplete = true;

            mediaCodec.stop();
            mediaCodec.release();
        }
    }

    @Override
    public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
        Log.d(TAG, "codec onError " + e.getMessage());
    }

    @Override
    public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
        // 输出格式改变时被调用
    }
});

aac 写文件线程

class WriteRunnable implements Runnable {

    @Override
    public void run() {
        File file = new File(getExternalFilesDir(null), "demo.aac");
        FileOutputStream out = null;

        try {
            out = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        if (out == null) {
            return;
        }

        try {
            byte[] header = new byte[7];
            while (true) {
                Log.d(TAG, "write thread running");
                byte[] data;
                if (!isCodecComplete) {
                    Log.d(TAG, "outputQueue 等待出队");
                    data = outputQueue.take(); // 阻塞方法,不要过多的 take, 否则线程会阻塞无法退出
                    Log.d(TAG, "outputQueue 出队");
                } else {
                    // codec 结束后如果队列非空要处理完队列中的剩余数据
                    Log.d(TAG, "222222222222222222 " + inputQueue.size());
                    data = outputQueue.poll();
                }

                if (data != null) {
                    // 队列非空就处理(不论 codec 是否处理完成)
                    addADTStoPacket(header, data.length + 7);
                    out.write(header);
                    out.write(data);
                    Log.d(TAG, "aac 写入文件");
                } else if (isCodecComplete) {
                    // 队列空且 codec 结束说明所有数据都写完了
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        Log.d(TAG, "aac写线程结束");
    }
}

我用非阻塞方法,同步获取 codec buffer 也写了个 demo

class RecordRunnable implements Runnable {
    int bufferSize;

    RecordRunnable(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    @Override
    public void run() {
        byte[] buffer = new byte[bufferSize];

        while (isRecording) {
            int len = audioRecord.read(buffer, 0, bufferSize);
            if (len <= 0) {
                continue;
            }

            byte[] data = new byte[len];
            System.arraycopy(buffer, 0, data, 0, len);

            if (inputQueue.size() == QUEUE_SIZE) {
                try {
                    Thread.sleep(100);
                    Log.d(TAG, "inputQueue 满了等待。。。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                inputQueue.offer(data);
                Log.d(TAG, "inputQueue 入队 " + data.length);
            }
        }

        audioRecord.stop();
        audioRecord.release();
        Log.d(TAG, "录制线程结束");
    }
}

class CodecRunnable implements Runnable {

    @Override
    public void run() {
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

        for (; ; ) {
            Log.d(TAG, "codec thread running...");
            byte[] data = inputQueue.poll();
            if (data != null) {
                // 只要有数据就放入 codec (无论录制是否结束)
                // dequeueInputBuffer 后要及时提交(queueInputBuffer)到 codec,否则 input buffer 用尽后 dequeueInputBuffer 会一直返回 -1
                int inputBufferId = mediaCodec.dequeueInputBuffer(1000);
                Log.d(TAG, "inputBufferId " + inputBufferId);
                if (inputBufferId >= 0) {
                    ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
                    inputBuffer.clear();
                    inputBuffer.put(data);
                    mediaCodec.queueInputBuffer(inputBufferId, 0, data.length, System.currentTimeMillis() * 1000, 0);
                    Log.d(TAG, "codec 入队 ");
                } else {
                    Log.d(TAG, "codec 入队失败数据丢失,没有可用 buffer");
                }
            } else if (!isRecording) {
                // 录制结束且队列中没有数据说明已经没有输入了,用一个带 BUFFER_FLAG_END_OF_STREAM 的空 buffer 标志输入结束
                int inputBufferId = mediaCodec.dequeueInputBuffer(1000);
                Log.d(TAG, "inputBufferId2 " + inputBufferId);
                if (inputBufferId >= 0) {
                    mediaCodec.queueInputBuffer(inputBufferId, 0, 0, System.currentTimeMillis() * 1000, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    Log.d(TAG, "codec 最后一个空 buffer 了");
                } else {
                    Log.d(TAG, "最后一个空 buffer 入队失败,没有可用 buffer");
                }
            } else {
                // 录制未结束,队列为空,这里什么也不做
                // 这里不要 sleep ,因为没有输入时还要处理输出呢
                Log.d(TAG, "inputQueue 为空。。。");
            }

            // dequeueOutputBuffer 用完后要及时 releaseOutputBuffer ,否则 out buffer 会用尽后 dequeueOutputBuffer 会一直返回 -1
            int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 1000);
            Log.d(TAG, "outputBufferId " + outputBufferId);
            if (outputBufferId >= 0) {
                ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
                byte[] outData = new byte[bufferInfo.size];
                outputBuffer.get(outData);
                mediaCodec.releaseOutputBuffer(outputBufferId, false);
                Log.d(TAG, "codec 出队 size " + bufferInfo.size + " flag " + bufferInfo.flags);

                if (outputQueue.size() == QUEUE_SIZE) {
                    try {
                        // outputQueue 满后要等一下,否则解码后数据会丢失
                        // 睡一小会对 codec 线程影响不大
                        Thread.sleep(100);
                        Log.d(TAG, "outputQueue 满了等待。。。");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    outputQueue.offer(outData);
                }

                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    Log.d(TAG, "flags " + bufferInfo.flags);
                    Log.d(TAG, "codec 处理完毕了");
                    isCodecComplete = true;
                    break;
                }
            } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // Subsequent data will conform to new format.
                // Can ignore if using getOutputFormat(outputBufferId)
            }
        }

        mediaCodec.stop();
        mediaCodec.release();
        Log.d(TAG, "codec 线程结束");
    }
}

class WriteRunnable implements Runnable {

    @Override
    public void run() {
        File file = new File(getExternalFilesDir(null), "demo.aac");
        FileOutputStream out = null;

        try {
            out = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        if (out == null) {
            return;
        }

        try {
            byte[] header = new byte[7];
            while (true) {
                byte[] data = outputQueue.poll();
                Log.d(TAG, "write thread running");
                if (data != null) {
                    // 队列非空就处理(不论 codec 是否处理完成)
                    addADTStoPacket(header, data.length + 7);
                    out.write(header);
                    out.write(data);
                    Log.d(TAG, "aac 写入文件");
                } else if (isCodecComplete) {
                    // 队列空且 codec 结束说明所有数据都写完了
                    break;
                } else {
                    // 队列空且 codec 未结束,这时要等待一下
                    try {
                        Thread.sleep(200);
                        Log.d(TAG, "暂无数据 aac 写线程等待。。。");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        Log.d(TAG, "aac写线程结束");
    }
}

多线程交互比较复杂,如果测试不充分很容易出现问题。经常出现线程无法退出的情况。自己水平有限,如果有 bug 请指出我再修改。

源码 https://github.com/lesliebeijing/audio_video_learn/tree/master/MediaCodecDemo

你可能感兴趣的:(Android音视频学习:MediaCodec 硬编 aac)