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<byte[]> inputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE); // codec 输入队列
ArrayBlockingQueue<byte[]> 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容