美文网首页Android---视频
《Android音视频系列-5》音视频采集,生成mp4

《Android音视频系列-5》音视频采集,生成mp4

作者: 蓝师傅_Android | 来源:发表于2019-08-15 23:29 被阅读0次

    最近晚上和周末基本都在排队练车,累成狗,好久没写文章了~

    抽空整理了一下音视频采集的方式,最终生成mp4。

    一、音频采集,得到PCM数据

    音频采集比较简单,通过 AudioRecord 录音,然后在子线程不断去读PCM数据

    记得声明录音权限 <uses-permission android:name="android.permission.RECORD_AUDIO" />

    开始录音

        //默认参数
        private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
        private static final int SAMPLE_RATE = 44100;
        private static final int CHANNEL_CONFIGS = AudioFormat.CHANNEL_IN_STEREO;
        private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
        private int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
    
        private AudioRecord audioRecord;
    
        public void start() {
            start(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
        }
    
        public void start(int audioSource, int sampleRate, int channels, int audioFormat) {
            if (isStartRecord) {
                Log.d(TAG, "音频录制已经开启");
                return;
            }
    
            bufferSize = AudioRecord.getMinBufferSize(sampleRate, channels, audioFormat);
    
            if (bufferSize == AudioRecord.ERROR_BAD_VALUE) {
                Log.d(TAG, "无效参数");
                return;
            }
    
            audioRecord = new AudioRecord(audioSource, sampleRate, channels, audioFormat, bufferSize);
            audioRecord.startRecording();
            isStopRecord = false;
    
            threadCapture = new Thread(new CaptureRunnable());
            threadCapture.start();
        }
    

    主要是创建AudioRecord的一些参数,然后调用audioRecord.startRecording();开始录制,然后启动一个子线程,去读取录制的PCM格式的数据

    读取PCM

     /**
     * 子线程读取采集到的PCM数据
     */
    private class CaptureRunnable implements Runnable {
    
        @Override
        public void run() {
            while (!isStopRecord) {
                byte[] buffer = new byte[bufferSize];
                int readRecord = audioRecord.read(buffer, 0, bufferSize);
                if (readRecord &gt; 0) {
                    if (captureListener != null)
                        captureListener.onCaptureListener(buffer,readRecord);
                    Log.d(TAG, "音频采集数据源 -- ".concat(String.valueOf(readRecord)).concat(" -- bytes"));
                } else {
                    Log.d(TAG, "录音采集异常");
                }
                //延迟写入 SystemClock  --  Android专用
                SystemClock.sleep(10);
            }
        }
    }
    

    读取PCM 比较简单,就是通过audioRecord.read(buffer, 0, bufferSize),最终PCM格式的数据会读到这个buffer里

    拿到录制的每一帧PCM数据之后,可以用AudioTrack播放,这里就不播放了,回调出去,后面合成mp4要用到。

    上面两个步骤,可以封装一个录音的管理类。
    【传送门】(待补充)

    现在获取的音频是PCM格式,我们要将它编码成aac,然后跟视频数据合成mp4,这里要用到 MediaCodecMediaMuxer

    MediaCodec 使用

    MediaCodec 是一个音视频编解码器,本篇主要用于:

    1. 将PCM格式的音频数据编码成aac格式,
    2. 将NV21格式的相机预览数据编码成avc格式。

    API 简介

    getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
    queueInputBuffer:输入流入队列
    dequeueInputBuffer:从输入流队列中取数据进行编码操作
    getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
    dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
    releaseOutputBuffer:处理完成,释放ByteBuffer数据

    初始化音频编解码器

        private MediaCodec mAudioCodec;
    
        String audioType = MediaFormat.MIMETYPE_AUDIO_AAC;  //编码成aac格式
        int sampleRate = 44100;
        int channels = 2;//单声道 channelCount=1 , 双声道  channelCount=2
    
        private void initAudioCodec(String audioType, int sampleRate, int channels) {
        try {
            mAudioCodec = MediaCodec.createEncoderByType(audioType);
            MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channels);
            int BIT_RATE = 96000;
            audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
            audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
                    MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            int MAX_INOUT_SIZE = 8192;
            audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_INOUT_SIZE);
    
            mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    
        } catch (IOException e) {
            Log.e(TAG, "initAudioCodec: 音频类型无效");
        }
    }
    

    视频编解码器的初始化同理

        String videoType = MediaFormat.MIMETYPE_VIDEO_AVC;
    
        private void initVideoCodec(String videoType, int width, int height) {
        try {
            mVideoCodec = MediaCodec.createEncoderByType(videoType);
            MediaFormat videoFormat = MediaFormat.createVideoFormat(videoType, width, height);
    
            videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            //MediaFormat.KEY_FRAME_RATE -- 可通过Camera#Parameters#getSupportedPreviewFpsRange获取
            videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            //mSurfaceWidth*mSurfaceHeight*N  N标识码率低、中、高,类似可设置成1,3,5,码率越高视频越大,也越清晰
            videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 4);
            //每秒关键帧数
            videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
    
            if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.N) {
                videoFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
                videoFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);
            }
    
            mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            //注意这里,获取视频解码器的surface,之后要将opengl输出到这个surface中
            mSurface = mVideoCodec.createInputSurface();
        } catch (IOException e) {
            Log.e(TAG, "initVideoCodec: 视频类型无效");
        }
    }
    

    这里要注意的是,音频数据从AudioRecord 直接读出来,但是视频数据处理有些不同,视频数据不直接读相机预览,而是通过相机纹理id,利用OpenGL直接渲染到视频编解码器的surface上,可以直接被编码,效率比较高,所以这里获取了MediaCodec的surface,mSurface = mVideoCodec.createInputSurface();,后面通过EGL创建NatieWindow的时候要用到,这里大概先了解一下就行,关于GLSurfaceView原理,EGL的使用,后面再整理一篇文章吧。

    两个编解码器创建好了,接下来要用到混合器, MediaMuxer

    MediaMuxer 使用详解

    MediaMuxer 是一个音视频混合器,我们录制音频和视频数据,经过MediaCodec编码,然后再将编码后的音视频数据混合在一起,最终生成mp4。

    MediaMuxer主要方法:

    1.int addTrack(@NonNull MediaFormat format)

    一个视频文件是包含一个或多个音视频轨道的,而这个方法就是用于添加一个音频频或视频轨道,并返回对应的ID。之后我们可以通过这个ID向相应的轨道写入数据。用于新建音视频轨道的MediaFormat是需要从MediaCodec.getOutputFormat()获取的,而不是自己简单构造的MediaFormat。

    2.start()

    当我们添加完所有音视频轨道之后,需要调用这个方法告诉Muxer,我要开始写入数据了。需要注意的是,调用了这个方法之后,我们是无法再次addTrack了的。

    3.void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
    @NonNull BufferInfo bufferInfo)

    用于向Muxer写入编码后的音视频数据。trackIndex是我们addTrack的时候返回的ID,byteBuf便是要写入的数据,而bufferInfo是跟这一帧byteBuf相关的信息,包括时间戳、数据长度和数据在ByteBuffer中的位移

    4.void stop()

    与start()相对应,用于停止写入数据,并生成文件。

    5.void release()

    释放Muxer资源。

    MediaMuxer 实战

    我们先来构造一个 MediaMuxer ,需要两个参数,第一个是音视频文件的保存路径,第二个是音视频封装文件的格式,可以选择mp4或3gp,我们使用mp4就好

    int mediaFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
    
    private void initMediaMuxer(String filePath, int mediaFormat) {
        try {
            mMediaMuxer = new MediaMuxer(filePath, mediaFormat);
        } catch (IOException e) {
            Log.e(TAG, "initMediaMuxer: 文件打开失败,path=" + filePath);
        }
    }
    

    添加音频轨道

    添加音频轨道,在音频编码线程 AudioCodecThread 处理

    public void run() {
        super.run();
        mIsStop = false;
        audioCodec.start();
        while (true) {
            if (mMediaEncodeManager == null) {
                Log.e(TAG, "run: mediaEncodeManagerWeakReference == null");
                return;
            }
            if (mIsStop) {
                mMediaEncodeManager.audioStop();
                return;
            }
    
            //获取一帧解码完成的数据到bufferInfo,没有数据就阻塞
            int outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
            //第一次会返回-2,在这时候添加音轨
            if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                mAudioTrackIndex = mediaMuxer.addTrack(audioCodec.getOutputFormat());
                mMediaEncodeManager.mAudioTrackReady  = true;
                Log.d(TAG, "run: 添加音轨 mAudioTrackIndex= " + mAudioTrackIndex);
                mMediaEncodeManager.startMediaMuxer();
            } else {
                while (outputBufferIndex != 0) {
                    if (!mMediaEncodeManager.mEncodeStart) {
                        Log.d(TAG, "run: 混合器还没开始,线程延迟");
                        SystemClock.sleep(10);
                        continue;
                    }
    
                    ByteBuffer outputBuffer = audioCodec.getOutputBuffers()[outputBufferIndex];
                    outputBuffer.position(bufferInfo.offset);
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
    
                    if (mPresentationTimeUs == 0) {
                        mPresentationTimeUs = bufferInfo.presentationTimeUs;
                    }
                    bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
                    mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);
    
                    audioCodec.releaseOutputBuffer(outputBufferIndex, false);
                    outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
                }
            }
        }
    }
    

    添加视频轨道

    添加视频轨道,在视频编码线程 VideoCodecThread 处理

    public void run() {
        mIsStop = false;
        videoCodec.start();
        while (true) {
            if (mMediaEncodeManager == null) {
                Log.e(TAG, "run: mMediaEncodeManager == null");
                return;
            }
            if (mIsStop) {
                mMediaEncodeManager.videoStop();
                return;
            }
    
            int outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
            //第一次返回 -2,在这个时候添加音轨
            if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                mVideoTrackIndex = mediaMuxer.addTrack(videoCodec.getOutputFormat());
                Log.d(TAG, "添加视频轨道,mVideoTrackIndex = " + mVideoTrackIndex);
                mMediaEncodeManager.mVideoTrackReady = true;
                mMediaEncodeManager.startMediaMuxer();
            } else {
                while (outputBufferIndex &gt;= 0) {
                    if (!mMediaEncodeManager.mEncodeStart) {
                        Log.d(TAG, "run: 混合器还没开始,线程延迟");
                        SystemClock.sleep(10);
                        continue;
                    }
    
                    ByteBuffer outputBuffer = videoCodec.getOutputBuffers()[outputBufferIndex];
                    outputBuffer.position(bufferInfo.offset);
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
    
                    if (mPresentationTimeUs == 0) {
                        mPresentationTimeUs = bufferInfo.presentationTimeUs;
                    }
                    bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
                    mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
                    if (bufferInfo != null) {
                        mMediaEncodeManager.onRecordTimeCallBack((int) (bufferInfo.presentationTimeUs / 1000000));
                    }
                    videoCodec.releaseOutputBuffer(outputBufferIndex, false);
                    outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
                }
            }
        }
    }
    

    添加音频轨道和视频轨道后,就可以启动混合器,然后不断从编解码器MediaCodec中读取已经编码成功的数据,然后调用mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);将编码后的音/视频数据写到混合器里,停止的时候要调用

    mMediaMuxer.stop();
    mMediaMuxer.release();
    

    如果不出意外的话,会在指定目录下生成mp4文件。

    然后在PCM回调那里,将PCM数据扔到 MediaCodec 里面去,这样AudioCodecThread 里面就能读到已经编码的aac格式数据。

        public void setPcmSource(byte[] pcmBuffer, int buffSize) {
    
        try {
    
            int buffIndex = mAudioCodec.dequeueInputBuffer(0);
            if (buffIndex &lt; 0) {
                return;
            }
            ByteBuffer byteBuffer;
            if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.LOLLIPOP) {
                byteBuffer = mAudioCodec.getInputBuffer(buffIndex);
            } else {
                byteBuffer = mAudioCodec.getInputBuffers()[buffIndex];
            }
    
            byteBuffer.clear();
            byteBuffer.put(pcmBuffer);
            //mPresentationTimeUs = 1000000L * (buffSize / 2) / mSampleRate
            //一帧音频帧大小 int size = 采样率 x 位宽 x 采样时间 x 通道数
            // 1s时间戳计算公式  mPresentationTimeUs = 1000000L * (totalBytes / mSampleRate/ mAudioFormat / mChannelCount / 8 )
            //totalBytes : 传入编码器的总大小
            //1000 000L : 单位为 微秒,换算后 = 1s,
            //除以8     : pcm原始单位是bit, 1 byte = 8 bit, 1 short = 16 bit, 用 Byte[]、Short[] 承载则需要进行换算
            mPresentationTimeUs += (long) (1.0 * buffSize / (mSampleRate * mChannelCount * (mAudioFormat / 8)) * 1000000.0);
            Log.d(TAG, "pcm一帧时间戳 = " + mPresentationTimeUs / 1000000.0f);
            mAudioCodec.queueInputBuffer(buffIndex, 0, buffSize, mPresentationTimeUs, 0);
        } catch (IllegalStateException e) {
            //mAudioCodec 线程对象已释放MediaCodec对象
            Log.d(TAG, "setPcmSource: " + "MediaCodec对象已释放");
        }
    }
    

    视频数据通过OpenGL渲染到视频编解码器的surface中,只要打开相机,视频编码器就能获取到编码后的视频数据,然后写到混合器里,跟音频处理基本差不多。当然,这里涉及到自定义GLSurfaceView,参照GLSurfaceView中对EGL的处理,自己写EglHelper,这里不是本文重点,后面有时间再说下GLSurfaceView源码。

    接下来再简单看一下如何通过Camera1采集视频数据

    视频数据采集

    相机功能封装在 CameraManager中,使用的是Camera1,需要注意的是设置预览数据格式,还有一个是SurfaceTexture,在外部创建(OpenGL创建纹理的时候),然后再启动相机,把纹理传过去,简单贴下启动相机代码

        private void startCamera(int cameraId) {
        try {
            camera = Camera.open(cameraId);
            camera.setPreviewTexture(surfaceTexture);
    
            Camera.Parameters parameters = camera.getParameters();
            parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
            parameters.setPreviewFormat(ImageFormat.NV21);
    
            //设置对焦模式,后置摄像头开启时打开,切换到前置时关闭(三星、华为不能设置前置对焦,魅族、小米部分机型可行)
            if (cameraId == 0) {
                //小米、魅族手机存在对焦无效情况,需要针对设备适配,想要无感知对焦完全适配最好是监听加速度传感器
                camera.cancelAutoFocus();
                //这种设置方式存在屏幕闪烁一下问题,包括Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
                parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            }
            Camera.Size size = getCameraSize(parameters.getSupportedPreviewSizes(), screenWidth,
                    screenHeight, 0.1f);
            parameters.setPreviewSize(size.width, size.height);
            //水平方向未旋转,所以宽就是竖直方向的高,对应旋转操作
            Log.d(TAG, "startCamera: 预览宽:" + size.width + " -- " + "预览高:" + size.height);
            previewWidth = size.width;
            previewHeight = size.height;
    
            size = getCameraSize(parameters.getSupportedPictureSizes(), screenWidth, screenHeight, 0.1f);
            parameters.setPictureSize(size.width, size.height);
            //水平方向未旋转,所以宽就是竖直方向的高
            Log.d(TAG, "startCamera: 图片宽:" + size.width + " -- " + "图片高:" + size.height);
    
            camera.setParameters(parameters);
            camera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    相机启动之后预览数据会输出到 surfaceTexture,这个surfaceTexture 关联一个纹理id,就是通过OpenGL创建并绑定的纹理id

      /**
     * 创建摄像头预览扩展纹理
     */
    private void createCameraTexture() {
        int[] textureIds = new int[1];
        GLES20.glGenTextures(1, textureIds, 0);
        cameraTextureId = textureIds[0];
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId);
        //环绕(超出纹理坐标范围)  (s==x t==y GL_REPEAT 重复)
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
        //过滤(纹理像素映射到坐标点)  (缩小、放大:GL_LINEAR线性)
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    
        surfaceTexture = new SurfaceTexture(cameraTextureId);
        surfaceTexture.setOnFrameAvailableListener(this);
    
        if (onSurfaceListener != null) {
            //回调给CameraManager获取surfaceTexture:通过camera.setPreviewTexture(surfaceTexture);
            onSurfaceListener.onSurfaceCreate(surfaceTexture, fboTextureId);
        }
    
        // 解绑扩展纹理
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
    }
    

    总结一下相机数据采集过程

    1. OpenGL创建纹理,绑定一个纹理id
    2. 启动相机,传入前面创建的纹理,这样相机预览数据就会输出到OpenGL绑定的纹理。
    3. 纹理并不能直接拿来编码,需要参考GLSurfaceView的显示原理,创建EGL,通过OpenGL不断将纹理渲染到MediaCodec的surface上,然后在一个子线程不断获取MediaCodec 中编码成功的数据,后面就跟音频处理一样,添加到混合器里,最终合成mp4文件。

    对OpenGL不熟悉的话没关系,有时间的话可以去学一下,不需要太深,也可以在我的简书主页查看OpenGL的入门系列文章
    https://www.jianshu.com/u/282785a6b12f

    这篇文章内容属于音视频开发的基础部分了,后面要整理相机推流,会涉及到音视频采集,也就是本章内容。

    在后面章节完成之后会把源码提交到github,

    想让自己变优秀,就要少看头条,少刷抖音,坚持学习,写文章,不然的话可能如下图:


    相关文章

      网友评论

        本文标题:《Android音视频系列-5》音视频采集,生成mp4

        本文链接:https://www.haomeiwen.com/subject/yfpojctx.html