Android OpenGL ES 十五.ffmpeg+open

2021-10-04  本文已影响0人  有心人2021

在上一篇的中,已经可以播放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录制视频(转载整理)下方已有,集成在一起了。

上一篇下一篇

猜你喜欢

热点阅读