Android OpenGL ES 十五.ffmpeg+opensl es音频录制

在上一篇的中,已经可以播放MP3音乐了,那么如何把声音加入的录制的视频中呢

总体思路如下:

  1. 打包musicplayer项目,拿到编译后的.so文件
  2. 把该.so放入到录屏的项目,集成进去
  3. 仿照视频录制,把上一篇回调给java端的callBackPcm(byte[] pcmData, int size),数据放入音频的mAudioCodec队列,然后开启线程用mMediaMuxer写入音频数据。

一. 打包so

使用gradlew assembleRelease打包命令,然后再build路径下拿到.so文件

image.png

二. 放入新的录屏项目中

要注意也需要放到新的项目的v8a路径



然后配置CmakeList.txt

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)

# 需要引入我们头文件,以这个配置的目录为基准
include_directories(src/main/jniLibs/include)

AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp SRC_LIST)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

# 添加共享库搜索路径
LINK_DIRECTORIES(${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a)

add_library(
        # Sets the name of the library.
        video-record
        # Sets the library as a shared library.
        SHARED
        ${SRC_LIST}
)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        # 链接额外的 ffmpeg 的编译
        video-record
        # 编解码(最重要的库)
        avcodec
        # 滤镜特效处理库
        avfilter
        # 封装格式处理库
        avformat
        # 工具库(大部分库都需要这个库的支持)
        avutil
        # 后期处理
        postproc
        # 音频采样数据格式转换库
        swresample
        # 视频像素数据格式转换
        swscale
        music-player
        # 链接 android ndk 自带的一些库
        android
        # 链接 OpenSLES
        OpenSLES
        # Links the target library to the log library
        # included in the NDK.
        log)

music-player在target_link_libraries中引入,其他的ffmpeg因为要调用到,也需要放入
SRC_LIST配置了,放一个空的.cpp文件就行,没有该变量还报错

注意事项:


这里是从music-player拿过来的,整个包的包名要和原来的项目保持一致,原来是什么包名不要动,要不然和native层代码对应不上。

三. 编写录制播放的音乐代码

  1. 初始化mediaPlayer
//BaseVideoRecorder.java
public BaseVideoRecorder(Context context,EGLContext eglContext) {
      //...
        mediaPlayer = new DarrenPlayer();
}

public void initMediaParams(String audioPath, String outPath, int videoWidth, int videoHeight) {

        try {
            //...
            initAudioParams(audioPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

private void initAudioParams(String audioPath) throws Exception{
        // 设置mediaPlayer音频路径,并做初始化
        mediaPlayer.setDataSource(audioPath);
        mediaPlayer.setOnPreparedListener(new MediaPreparedListener() {
            @Override
            public void onPrepared() {
                //native准备好后,可以播放开启录制了
                start();

                //音频播放
                mediaPlayer.play();
            }
        });
        mediaPlayer.setMediaInfoListener(new MediaInfoListener() {

            int sampleRate;
            int channel;

            long audioPts;

            @Override
            public void musicInfo(int sampleRate, int channel) {
                //创建audio的codec
                initAudioCodec(sampleRate,channel);
                this.sampleRate = sampleRate;
                this.channel = channel;
            }

            @Override
            public void callBackPcm(byte[] pcmData, int size) {

                int inputBufferTrack = mAudioCodec.dequeueInputBuffer(0);
                if(inputBufferTrack >= 0){
                    ByteBuffer inputBuffer = mAudioCodec.getInputBuffers()[inputBufferTrack];
                    inputBuffer.clear();

                    inputBuffer.put(pcmData);

                    //0.41795918 *1000 000
                    audioPts += 1000000 * size * 1.0f / sampleRate * channel * 2;
                    Log.e(TAG, "callBackPcm: "+audioPts);
                    //数据放入mAudioCodec的队列中
                    mAudioCodec.queueInputBuffer(inputBufferTrack,0,size,audioPts,0);
                }
            }
        });
        mediaPlayer.setOnErrorListener( new MediaErrorListener() {
            @Override
            public void onError(int code, String msg) {
                Log.e(TAG, "mediaPlayer jni onError: code="+code+", msg="+msg);
            }
        });
    }

    private void start(){
        // 视频渲染开始
        videoRenderThread.start();
        // 视频编码开始
        videoEncoderThread.start();
        // 音频编码开始
        audioEncoderThread.start();
    }

callBackPcm会不断的循环被调用,拿到的数据放入到音频的mediacodec中,这样muxer那边能拿到

  1. 设置音频Mediacodec的参数,需要使用aac_lc
    private void initAudioCodec(int sampleRate, int channel) {
        try {
            // 采样率,44.1khz,双声道,每个声道16位,2字节
            MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,sampleRate,channel);
            // 设置比特率96k hz
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,96000);
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            // 设置输入数据缓冲区的最大大小
            mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,sampleRate * channel * 2);

            mAudioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            mAudioCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);

            audioEncoderThread = new AudioEncoderThread(recorderReference);

            mAudioCodec.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  1. 一般是init设定参数后,调用startRecord,这样等播放器准备好的方法回调了,就开始录屏和录音
public void startRecord(){
        // mediaPlayer的prepare有0.几秒的延迟,最好这里启动准备,准备好后启动音、视频录制线程
        // 如果这里用play(),并启动start()方法的线程,会导致录制的视频黑屏
        mediaPlayer.prepareAsync();
    }
  1. 录音代码
private class AudioEncoderThread extends Thread{

        WeakReference<BaseVideoRecorder> videoRecorderWf;
        private boolean shouldExit =false;

        private MediaCodec mAudioCodec;
        private MediaMuxer mMediaMuxer;
        MediaCodec.BufferInfo bufferInfo;

        long audioPts = 0;
        final CyclicBarrier startCb;
        final CyclicBarrier stopCb;
        /**
         * 音频轨道
         */
        private int mAudioTrackIndex = -1;

        public AudioEncoderThread(WeakReference<BaseVideoRecorder> videoRecorderWf){
            this.videoRecorderWf = videoRecorderWf;
            this.mAudioCodec = videoRecorderWf.get().mAudioCodec;
            this.mMediaMuxer = videoRecorderWf.get().mMediaMuxer;
            this.startCb = videoRecorderWf.get().startCb;
            this.stopCb = videoRecorderWf.get().stopCb;
            bufferInfo = new MediaCodec.BufferInfo();
        }

        @Override
        public void run() {
            while (true){
                try {
                    if(shouldExit){
                        onDestroy();
                        return;
                    }

                    // 返回有效数据填充的输出缓冲区的索引
                    int outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo,0);
                    if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                        // 将mMediaCodec的指定的格式的数据轨道,设置到mMediaMuxer上
                        mAudioTrackIndex = mMediaMuxer.addTrack(mAudioCodec.getOutputFormat());
                        Log.e(TAG, "run:  audio mMediaMuxer.await before");
                        startCb.await();
                        Log.e(TAG, "run: audio mMediaMuxer.await after");
                    }else{

                        int index = 0;
                        while (outputBufferIndex >= 0){

                            Log.e(TAG,"outputBufferIndex:"+outputBufferIndex+" count:"+index);
                            // 获取数据
                            ByteBuffer outBuffer = mAudioCodec.getOutputBuffers()[outputBufferIndex];

                            outBuffer.position(bufferInfo.offset);
                            outBuffer.limit(bufferInfo.offset+bufferInfo.size);

                            // 修改视频的 pts,基准时间戳
                            if(audioPts ==0)
                                audioPts = bufferInfo.presentationTimeUs;
                            bufferInfo.presentationTimeUs -= audioPts;

//                                System.out.println(bufferInfo.presentationTimeUs);
                            System.out.println("writeSampleData mAudioTrackIndex:"+ mAudioTrackIndex);
                            // 写入音频数据
                            mMediaMuxer.writeSampleData(mAudioTrackIndex,outBuffer,bufferInfo);

                            // 释放 outBuffer
                            mAudioCodec.releaseOutputBuffer(outputBufferIndex,false);
                            outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo,0);
                        }
                    }
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }

        private void onDestroy() {
            try {
                if (mAudioCodec != null){
                    mAudioCodec.stop();
                    mAudioCodec.release();
                    mAudioCodec = null;
                }
                //和videoEncoderThread的退出,避免一方退出了,一方还在运行,需保持一致
                Log.e(TAG, "run: audio stopCb.await() before");
                stopCb.await();
                Log.e(TAG, "run: audio stopCb.await() after");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void requestExit() {
            shouldExit = true;
        }
    }

    private class VideoEncoderThread extends Thread{

       //...
        CyclicBarrier startCb;
        CyclicBarrier stopCb;

        /**
         * 视频轨道
         */
        private int mVideoTrackIndex = -1;

        public VideoEncoderThread(WeakReference<BaseVideoRecorder> videoRecorderWf){
       //...
            this.startCb = videoRecorderWf.get().startCb;
            this.stopCb = videoRecorderWf.get().stopCb;
            //...
        }

        @Override
        public void run() {
                //...
                    if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                        // 将mMediaCodec的指定的格式的数据轨道,设置到mMediaMuxer上
                        mVideoTrackIndex = mMediaMuxer.addTrack(mVideoCodec.getOutputFormat());
                        mMediaMuxer.start();
                        //等待audioEncoder,一起往 startCb.wait()后面走
                        Log.e(TAG, "run: video mMediaMuxer.start()");
                        startCb.await();
                        Log.e(TAG, "run: video mMediaMuxer.await after");
                  //...
        }

        private void onDestroy() {
            try {
               //...
                Log.e(TAG, "run: video stopCb.await() before");
                stopCb.await();
                Log.e(TAG, "run: video stopCb.await() after");
                if (mMediaMuxer != null){
                    mMediaMuxer.stop();
                    mMediaMuxer.release();
                    mMediaMuxer = null;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

     
    }

这里使用CyclicBarrier 来保持线程之间步调一致,因为mMediaMuxer需要开启后,才能写入,而开启是在一个线程中,还有释放也是需要均结束后再释放,避免一方尚未结束,一方已经释放了。

  1. 流程退出
    停止音频播放,停止录制线程。
public void stopRecord(){

        mediaPlayer.stop();

        videoRenderThread.requestExit();
        videoEncoderThread.requestExit();
        audioEncoderThread.requestExit();
    }

代码在Android OpenGL ES 十三.MediaCodec录制视频(转载整理)下方已有,集成在一起了。

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

推荐阅读更多精彩内容