Vídeo de gravação do Android, várias formas de API de codificação para realizar a gravação

Diversas soluções e uso simples de gravação através da API do sistema

prefácio

Com relação a como usar a gravação de vídeo, mencionei que há muitas maneiras de alcançá-lo. A intenção salta para a página do sistema, edição suave como FFmpeg e implementação de codificação permanente do pacote CameraX. A codificação rígida de configuração do MediaRecorder também pode ser implementada através do MediaCodec + O MediaMuxer implementa a codificação por si só.

Porque falei sobre a prévia do uso das três câmeras e do pacote simples. Em seguida, este artigo revisará brevemente os seguintes esquemas de codificação permanente, que são todas as APIs do sistema Android e suas APIs de encapsulamento.

O texto não envolve pontos de conhecimento de áudio e vídeo muito profissionais. Precisamos apenas entender algumas informações de configuração necessárias para a gravação básica de vídeo para concluir a gravação (afinal, a API do sistema foi bem encapsulada).

  1. Taxa de quadros: A taxa de quadros refere-se ao número de imagens exibidas por segundo, geralmente em FPS. Quanto maior a taxa de quadros, mais suave será o movimento no vídeo. Taxas de quadros comuns são 24, 30, 60, etc.
  2. Resolução: A resolução refere-se ao tamanho do pixel do vídeo, geralmente representado por largura e altura, como 1920x1080 ou 1280x720. Resoluções mais altas fornecem uma imagem mais nítida.
  3. Taxa de bits: A taxa de bits indica o número de bits transmitidos por segundo, geralmente em Mbps. A taxa de bits determina a quantidade de dados no vídeo e também afeta a qualidade e o tamanho do arquivo do vídeo. Geralmente, configuramos para multiplicar a largura e a altura da resolução por 3 ou multiplicar a largura e a altura por 5. Você também pode definir um valor relativamente grande, como 3500000.
  4. Quadro-chave (quadro I): O vídeo geral é dividido em quadro-chave (quadro I), quadro de previsão (quadro P) e quadro de previsão bidirecional (quadro B). Só precisamos entender que o quadro I possui informações de imagem completas e de alta qualidade Eles são frequentemente usados ​​como quadros-chave. Geralmente escolhemos a capa ou miniatura do I frame, um porta-retrato independente. Na gravação, geralmente precisamos escolher o intervalo do quadro I do vídeo gravado, geralmente escolha 1 (cada quadro se torna um quadro principal, o arquivo é maior) ou 2 (haverá alguns quadros P ou quadros B entre dois quadros I, arquivo será menor)

Após um breve entendimento sobre isso, podemos começar a gravar, então que tipo de câmera devemos usar como exemplo para obter edição e gravação difíceis?

Na verdade, cada câmera tem suas próprias vantagens e desvantagens, e os dados de retorno de chamada são diferentes. O retorno de chamada Camera1 é NV21, e o retorno de chamada Camera2 e Camerax é YUV420. Podemos converter o formato correspondente por meio de algumas ferramentas para realizar a codificação Mediacodec. Se você quiser apenas Implementação simples de gravação Em seguida, use o caso de uso VideoCapture do CameraX para concluir rapidamente as funções de visualização e gravação.

Embora o Camera2, que é mais complicado de usar, tenha muitas implementações de código e a compatibilidade e o suporte de diferentes dispositivos também sejam diferentes, ele pode atender a alguns requisitos personalizados. ISO escalonado, exposição, foco automático, balanço de branco, suporte a várias câmeras e muito mais.

A implementação da API de gravação usada neste artigo também é baseada em Camera2 e seu encapsulamento.

Vamos falar sobre como os diferentes métodos são implementados em detalhes.

1. Gravação do MediaRecorder

É muito conveniente usar o próprio Intent, mas algumas funções têm problemas de compatibilidade, e o grau de versão do sistema e suporte ao dispositivo também é diferente, portanto, não é tão fácil de usar, a menos que não haja restrições.

Portanto, no início do desenvolvimento, gravamos de acordo com o MediaRecorder empacotado pelo sistema.A gravação de áudio e vídeo pode ser concluída por meio das opções de configuração, que podem ser consideradas muito convenientes.

 public void startCameraRecord() {

        mMediaRecorder = new MediaRecorder();
        mMediaRecorder.reset();
        if (mCamera != null) {
            mMediaRecorder.setCamera(mCamera);
        }
        mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
            @Override
            public void onError(MediaRecorder mr, int what, int extra) {
                if (mr != null) {
                    mr.reset();
                }
            }
        });

        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); // 视频源
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);  // 音频源
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);  // 视频封装格式
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);  // 音频格式
        if (mBestPreviewSize != null) {
//            mMediaRecorder.setVideoSize(mBestPreviewSize.width, mBestPreviewSize.height);  // 设置分辨率
            mMediaRecorder.setVideoSize(640, 480);  // 设置分辨率
        }
//        mMediaRecorder.setVideoFrameRate(16); // 比特率
        mMediaRecorder.setVideoEncodingBitRate(1024 * 512);// 设置帧频率,
        mMediaRecorder.setOrientationHint(90);// 输出旋转90度,保持竖屏录制
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);// 视频输出格式
        mMediaRecorder.setOutputFile(mVecordFile.getAbsolutePath());

        try {
            mMediaRecorder.prepare();
            mMediaRecorder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

Ele exibe diretamente a página da câmera, não pode gravar efeitos especiais, não pode especificar a fonte de codificação, pode ser apenas imagens de câmera puras e não suporta resolução de taxa de bits amigável e precisa se adaptar à resolução suportada pelo dispositivo, etc.

O mais inaceitável é que muitos modelos iniciam o MediaRecorder com um bipe de 'bipe', esse bipe do sistema realmente deixa as pessoas carecas.

Não é à toa que a gravação do CameraX lançada posteriormente não usa seu próprio MediaRecorder, e o próprio MediaRecorder também é implementado com base no MediaCodec e é encapsulado, para que possamos ver como o MediaCodec de nível inferior é implementado.

2. MediaCodec + Camera implementa codificação de vídeo de forma assíncrona

Se for apenas uma única gravação de vídeo, não precisamos considerar a sincronização de áudio e vídeo e não precisamos lidar com carimbos de data/hora. Na verdade, podemos usar callbacks assíncronos MediaCodec para implementá-lo de maneira mais simples.

Por exemplo, podemos codificar diretamente o formato I420 da câmera original no formato de arquivo H264.

Tomando o Camera2 como exemplo, definimos callbacks em nosso pacote anterior, portanto não repetiremos o código do pacote aqui. Se você estiver interessado, pode ir ao artigo anterior para verificar e postar o código usado diretamente.

    //子线程中使用同步队列保存数据
    private val originVideoDataList = LinkedBlockingQueue<ByteArray>()

    //标记当前是否正在录制中
    private var isRecording: Boolean = false

    private lateinit var file: File
    private lateinit var outputStream: FileOutputStream

    fun setupCamera(activity: Activity, container: ViewGroup) {

        file = File(CommUtils.getContext().externalCacheDir, "${System.currentTimeMillis()}-record.h264")
        if (!file.exists()) {
            file.createNewFile()
        }
        if (!file.isDirectory) {
            outputStream = FileOutputStream(file, true)
        }

        val textureView = AspectTextureView(activity)
        textureView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)

        mCamera2Provider = Camera2ImageReaderProvider(activity)
        mCamera2Provider?.initTexture(textureView)

        mCamera2Provider?.setCameraInfoListener(object :
            BaseCommonCameraProvider.OnCameraInfoListener {
            override fun getBestSize(outputSizes: Size?) {
                mPreviewSize = outputSizes
            }

            override fun onFrameCannback(image: Image) {
                if (isRecording) {

                    // 使用C库获取到I420格式,对应 COLOR_FormatYUV420Planar
                    val yuvFrame = yuvUtils.convertToI420(image)
                    // 与MediaFormat的编码格式宽高对应
                    val yuvFrameRotate = yuvUtils.rotate(yuvFrame, 90)

                    // 用于测试RGB图片的回调预览
                    bitmap = Bitmap.createBitmap(yuvFrameRotate.width, yuvFrameRotate.height, Bitmap.Config.ARGB_8888)
                    yuvUtils.yuv420ToArgb(yuvFrameRotate, bitmap!!)
                    mBitmapCallback?.invoke(bitmap)

                    // 旋转90度之后的I420格式添加到同步队列
                    val bytesFromImageAsType = yuvFrameRotate.asArray()

                    originVideoDataList.offer(bytesFromImageAsType)
                }
            }

            override fun initEncode() {
                mediaCodecEncodeToH264()
            }

            override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture?, width: Int, height: Int) {
                [email protected] = surfaceTexture
            }
        })

        container.addView(textureView)
    }

Inicialize o codificador quando a câmera estiver pronta e, em cada retorno de chamada de quadro, adicionamos à fila de sincronização, porque os dados de codificação e visualização não são o mesmo encadeamento. Podemos então definir a codificação usando um retorno de chamada assíncrono.

    /**
     * 准备数据编码成H264文件
     */
    fun mediaCodecEncodeToH264() {

        if (mPreviewSize == null) return

        //确定要竖屏的,真实场景需要根据屏幕当前方向来判断,这里简单写死为竖屏
        val videoWidth: Int
        val videoHeight: Int
        if (mPreviewSize!!.width > mPreviewSize!!.height) {
            videoWidth = mPreviewSize!!.height
            videoHeight = mPreviewSize!!.width
        } else {
            videoWidth = mPreviewSize!!.width
            videoHeight = mPreviewSize!!.height
        }
        YYLogUtils.w("MediaFormat的编码格式,宽:${videoWidth} 高:${videoHeight}")

        //配置MediaFormat信息(指定H264格式)
        val videoMediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight)

        //添加编码需要的颜色格式
        videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)
//        videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)

        //设置帧率
        videoMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)

        //设置比特率
        videoMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mPreviewSize!!.width * mPreviewSize!!.height * 5)

        //设置每秒关键帧间隔
        videoMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)

        //创建编码器
        val videoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)

        //这里采取的是异步回调的方式,与dequeueInputBuffer,queueInputBuffer 这样的方式获取数据有区别的
        // 一种是异步方式,一种是同步的方式。
        videoMediaCodec.setCallback(callback)
        videoMediaCodec.configure(videoMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        videoMediaCodec.start()
    }

    /**
     * 具体的音频编码Codec回调
     */
    val callback = object : MediaCodec.Callback() {

        override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
        }

        override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
            Log.e("error", e.message ?: "Error")
        }

        /**
         * 系统获取到有可用的输出buffer,写入到文件
         */
        override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
            //获取outputBuffer
            val outputBuffer = codec.getOutputBuffer(index)
            val byteArray = ByteArray(info.size)
            outputBuffer?.get(byteArray)

            when (info.flags) {
                MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> {  //编码配置完成
                    // 创建一个指定大小为 info.size 的空的 ByteArray 数组,数组内全部元素被初始化为默认值0
                    configBytes = ByteArray(info.size)
                    // arraycopy复制数组的方法,
                    // 5个参数,1.源数组 2.源数组的起始位置 3. 目标数组 4.目标组的起始位置 5. 要复制的元素数量
                    // 这里就是把配置信息全部写入到configBytes数组,用于后期的编码
                    System.arraycopy(byteArray, 0, configBytes, 0, info.size)
                }
                MediaCodec.BUFFER_FLAG_END_OF_STREAM -> {  //完成处理
                    //当全部写完之后就回调出去
                    endBlock?.invoke(file.absolutePath)
                }
                MediaCodec.BUFFER_FLAG_KEY_FRAME -> {  //包含关键帧(I帧),解码器需要这些帧才能正确解码视频序列
                    // 创建一个临时变量数组,指定大小为 info.size + 配置信息 的空数组
                    val keyframe = ByteArray(info.size + configBytes!!.size)
                    // 先 copy 写入配置信息,全部写完
                    System.arraycopy(configBytes, 0, keyframe, 0, configBytes!!.size)
                    // 再 copy 写入具体的帧数据,从配置信息的 end 索引开始写,全部写完
                    System.arraycopy(byteArray, 0, keyframe, configBytes!!.size, byteArray.size)

                    //全部写完之后我们就能写入到文件中
                    outputStream.write(keyframe, 0, keyframe.size)
                    outputStream.flush()
                }
                else -> {  //其他帧的处理
                    // 其他的数据不需要写入关键帧的配置信息,直接写入到文件即可
                    outputStream.write(byteArray)
                    outputStream.flush()
                }
            }

            //释放
            codec.releaseOutputBuffer(index, false)
        }

        /**
         * 当系统有可用的输入buffer,读取同步队列中的数据
         */
        override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
            val inputBuffer = codec.getInputBuffer(index)
            val yuvData = originVideoDataList.poll()

            //如果获取到自定义结束符,优先结束掉
            if (yuvData != null && yuvData.size == 1 && yuvData[0] == (-333).toByte()) {
                isEndTip = true
            }

            //正常的写入
            if (yuvData != null && !isEndTip) {
                inputBuffer?.put(yuvData)
                codec.queueInputBuffer(
                    index, 0, yuvData.size,
                    surfaceTexture!!.timestamp / 1000,  //注意这里没有用系统时间,用的是surfaceTexture的时间
                    0
                )
            }

            //如果没数据,写入空数据,等待执行...
            if (yuvData == null && !isEndTip) {
                codec.queueInputBuffer(
                    index, 0, 0,
                    surfaceTexture!!.timestamp / 1000,  //注意这里没有用系统时间,用的是surfaceTexture的时间
                    0
                )
            }

            if (isEndTip) {
                codec.queueInputBuffer(
                    index, 0, 0,
                    surfaceTexture!!.timestamp / 1000,  //注意这里没有用系统时间,用的是surfaceTexture的时间
                    MediaCodec.BUFFER_FLAG_END_OF_STREAM
                )

            }

        }

    }


O objeto de retorno de chamada é um retorno de chamada assíncrono e cada linha de código é comentada em detalhes.

Controles para iniciar e finalizar a gravação:

    /**
     * 停止录制
     */
    fun stopRecord(block: ((path: String) -> Unit)? = null) {
        endBlock = block

        //写入自定义的结束符
        originVideoDataList.offer(byteArrayOf((-333).toByte()))

        isRecording = false
    }


    /**
     * 开始录制
     */
    fun startRecord() {
        isRecording = true
    }

Efeito gravado:

h264-1.gif

3. MediaCodec + AudioRecord implementa codificação de áudio de forma assíncrona

Após usarmos o MediaCodec para completar a gravação H264, podemos compilar o áudio separadamente da mesma forma, vamos pegar o formato comum AAC como exemplo.

Só que a fonte de vídeo anterior é o callback da Camera2, mas aqui nossa fonte de áudio vem da gravação do AudioRecord.

    //子线程中使用同步队列保存数据
    private var mAudioList: LinkedBlockingDeque<ByteArray>? = LinkedBlockingDeque()

    //标记当前是否正在录制中
    private var isRecording: Boolean = false

    // 输入的时候标记是否是结束标记
    private var isEndTip = false

    /**
     * 初始化音频采集
     */
    private fun initAudioRecorder() {
        //根据系统提供的方法计算最小缓冲区大小
        minBufferSize = AudioRecord.getMinBufferSize(
            AudioConfig.SAMPLE_RATE,
            AudioConfig.CHANNEL_CONFIG,
            AudioConfig.AUDIO_FORMAT
        )

        //创建音频录制器对象
        mAudioRecorder = AudioRecord(
            MediaRecorder.AudioSource.MIC,
            AudioConfig.SAMPLE_RATE,
            AudioConfig.CHANNEL_CONFIG,
            AudioConfig.AUDIO_FORMAT,
            minBufferSize
        )


        file = File(CommUtils.getContext().externalCacheDir, "${System.currentTimeMillis()}-record.aac")
        if (!file.exists()) {
            file.createNewFile()
        }
        if (!file.isDirectory) {
            outputStream = FileOutputStream(file, true)
            bufferedOutputStream = BufferedOutputStream(outputStream, 4096)
        }

        YYLogUtils.w("最终输入的File文件为:" + file.absolutePath)
    }

    /**
     * 启动音频录制
     */
    fun startAudioRecord() {

        //开启线程启动录音
        thread(priority = android.os.Process.THREAD_PRIORITY_URGENT_AUDIO) {
            isRecording = true  //标记是否在录制中

            try {
                //判断AudioRecord是否初始化成功
                if (AudioRecord.STATE_INITIALIZED == mAudioRecorder.state) {

                    mAudioRecorder.startRecording()  //音频录制器开始启动录制
                    val outputArray = ByteArray(minBufferSize)

                    while (isRecording) {

                        var readCode = mAudioRecorder.read(outputArray, 0, minBufferSize)

                        //这个readCode还有很多小于0的数字,表示某种错误,
                        if (readCode > 0) {
                            val realArray = ByteArray(readCode)
                            System.arraycopy(outputArray, 0, realArray, 0, readCode)
                            //将读取的数据保存到同步队列
                            mAudioList?.offer(realArray)

                        } else {
                            Log.d("AudioRecoderUtils", "获取音频原始数据的时候出现了某些错误")
                        }
                    }

                    //自定义一个结束标记符,便于让编码器识别是录制结束
                    val stopArray = byteArrayOf((-666).toByte(), (-999).toByte())
                    //把自定义的标记符保存到同步队列
                    mAudioList?.offer(stopArray)

                }
            } catch (e: IOException) {
                e.printStackTrace()

            } finally {
                //释放资源
                mAudioRecorder.release()
            }

        }

        //测试编码
        thread {
            mediaCodecEncodeToAAC()
        }

    }


A mesma coisa que a codificação de vídeo é que a coleta e a codificação de dados são realizadas em threads diferentes, portanto, as filas síncronas ainda são usadas para salvar os dados. Ativamos a gravação de áudio em subthreads e, ao mesmo tempo, permitimos que os subthreads iniciem o retorno de chamada assíncrono codificação.

    private fun mediaCodecEncodeToAAC() {

        try {

            //创建音频MediaFormat
            val encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AudioConfig.SAMPLE_RATE, 1)

            //配置比特率
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
            encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)

            //配置最大输入大小
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minBufferSize * 2)

            //初始化编码器
            mAudioMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
            //这里采取的是异步回调的方式,与dequeueInputBuffer,queueInputBuffer 这样的方式获取数据有区别的
            // 一种是异步方式,一种是同步的方式。
            mAudioMediaCodec?.setCallback(callback)

            mAudioMediaCodec?.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
            mAudioMediaCodec?.start()

        } catch (e: IOException) {
            Log.e("mistake", e.message ?: "Error")

        } finally {

        }

    }

    /**
     * 具体的音频编码Codec回调
     */
    val callback = object : MediaCodec.Callback() {

        val currentTime = Date().time * 1000  //以微秒为计算单位

        override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
        }

        override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
            Log.e("error", e.message ?: "Error")
        }

        /**
         * 系统获取到有可用的输出buffer,写入到文件
         */
        override fun onOutputBufferAvailable(
            codec: MediaCodec,
            index: Int,
            info: MediaCodec.BufferInfo
        ) {

            //通过bufferinfo获取Buffer的数据,这些数据就是编码后的数据
            val outBitsSize = info.size

            //为AAC文件添加头部,头部占7字节
            //AAC有 ADIF和ADTS两种  ADIF只有一个头部剩下都是音频文件
            //ADTS是每一段编码都有一个头部
            //outpacketSize是最后头部加上返回数据后的总大小
            val outPacketSize = outBitsSize + 7  // 7 is ADTS size

            //根据index获取buffer
            val outputBuffer = codec.getOutputBuffer(index)

            // 防止buffer有offset导致自己从0开始获取,
            // 取出数据(但是我实验的offset都为0,可能有些不为0的情况)
            outputBuffer?.position(info.offset)

            //设置buffer的操作上限位置,不清楚的可以查下ByteBuffer(NIO知识),
            //了解limit ,position,clear(),filp()都是啥作用
            outputBuffer?.limit(info.offset + outBitsSize)

            //创建byte数组保存组合数据
            val outData = ByteArray(outPacketSize)

            //为数据添加头部,后面会贴出,就是在头部写入7个数据
            addADTStoPacket(AudioConfig.SAMPLE_RATE, outData, outPacketSize)

            //将buffer的数据存入数组中
            outputBuffer?.get(outData, 7, outBitsSize)

            outputBuffer?.position(info.offset)

            //将数据写到文件
            bufferedOutputStream.write(outData)
            bufferedOutputStream.flush()
            outputBuffer?.clear()

            //释放输出buffer,并释放Buffer
            codec.releaseOutputBuffer(index, false)
        }

        /**
         * 当系统有可用的输入buffer,读取同步队列中的数据
         */
        override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {

            //根据index获取buffer
            val inputBuffer = codec.getInputBuffer(index)

            //从同步队列中获取还未编码的原始音频数据
            val pop = mAudioList?.poll()

            //判断是否到达音频数据的结尾,根据自定义的结束标记符判断
            if (pop != null && pop.size >= 2 && (pop[0] == (-666).toByte() && pop[1] == (-999).toByte())) {
                //如果是结束标记
                isEndTip = true
            }

            //如果数据不为空,而且不是结束标记,写入buffer,让MediaCodec去编码
            if (pop != null && !isEndTip) {

                //填入数据
                inputBuffer?.clear()
                inputBuffer?.limit(pop.size)
                inputBuffer?.put(pop, 0, pop.size)

                //将buffer还给MediaCodec,这个一定要还
                //第四个参数为时间戳,也就是,必须是递增的,系统根据这个计算
                //音频总时长和时间间隔
                codec.queueInputBuffer(
                    index,
                    0,
                    pop.size,
                    Date().time * 1000 - currentTime,
                    0
                )
            }

            // 由于2个线程谁先执行不确定,所以可能编码线程先启动,获取到队列的数据为null
            // 而且也不是结尾数据,这个时候也要调用queueInputBuffer,将buffer换回去,写入
            // 数据大小就写0

            // 如果为null就不调用queueInputBuffer  回调几次后就会导致无可用InputBuffer,
            // 从而导致MediaCodec任务结束 只能写个配置文件
            if (pop == null && !isEndTip) {

                codec.queueInputBuffer(
                    index,
                    0,
                    0,
                    Date().time * 1000 - currentTime,
                    0
                )
            }

            //发现结束标志,写入结束标志,
            //flag为MediaCodec.BUFFER_FLAG_END_OF_STREAM
            //通知编码结束
            if (isEndTip) {
                codec.queueInputBuffer(
                    index,
                    0,
                    0,
                    Date().time * 1000 - currentTime,
                    MediaCodec.BUFFER_FLAG_END_OF_STREAM
                )

                endBlock?.invoke(file.absolutePath)
            }
        }

    }

O mesmo processo, mas o identificador do final personalizado é diferente, e por ser um áudio gravado separadamente, precisamos adicionar um cabeçalho ADTS para tocar normalmente. Basta copiar um da Internet:

    private fun addADTStoPacket(sampleRateType: Int, packet: ByteArray, packetLen: Int) {
        val profile = 2 // AAC LC
        val chanCfg = 1 // CPE

        packet[0] = 0xFF.toByte()
        packet[1] = 0xF9.toByte()
        packet[2] = ((profile - 1 shl 6) + (sampleRateType shl 2) + (chanCfg shr 2)).toByte()
        packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
        packet[4] = (packetLen and 0x7FF shr 3).toByte()
        packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
        packet[6] = 0xFC.toByte()
    }

Neste ponto, podemos gravar um arquivo de áudio. Como a maioria deles são códigos fixos, mas a configuração é diferente, o efeito é quase o mesmo. Cada linha de código deve ter comentários detalhados o máximo possível.

O efeito de gravação é o seguinte:

imagem.png

Não consegue ouvir? Não tem jeito, vamos rodar o código sozinhos.

4. Codificação síncrona de áudio e vídeo MediaCodec + MediaMuxer e formato de empacotamento

É apenas uma gravação separada de áudio e vídeo, podemos usar o método de retorno de chamada para lidar com isso facilmente. E a gravação de áudio e vídeo juntos no formato MP4?

Eu entendo, chame de volta um vídeo, chame de volta um áudio e depois combine-os!

A razão é esta, mas não é implementada desta forma. A codificação de áudio e vídeo é lenta, então a imagem e o áudio ficarão fora de sincronia. Se você quiser garantir a sincronização de áudio e vídeo, você só pode usar o mesmo timestamp (timestamp) e o timestamp de apresentação (timestamp de apresentação), que associa quadros codificados de áudio e vídeo.

Geralmente é mais conveniente para nós codificar de maneira síncrona. Codificamos o fluxo de áudio como um encadeamento, o fluxo de vídeo como um vídeo e colocamos a operação do sintetizador MediaMuxer em um encadeamento e, em seguida, concluímos suas respectivas tarefas. , e, finalmente, gere um arquivo MP4.

As etapas gerais são as seguintes:

  1. Crie e configure objetos MediaCodec para áudio e vídeo.
  2. Forneça os dados de áudio a serem codificados para o MediaCodec de áudio para codificação e obtenha o quadro de áudio codificado.
  3. Forneça os dados de vídeo a serem codificados para o MediaCodec do vídeo para codificação e obtenha o quadro de vídeo codificado.
  4. Use os timestamps e timestamps de apresentação de quadros de áudio e vídeo para manter sua correspondência.
  5. Grave quadros de áudio e vídeo codificados em um buffer de saída compartilhado.
  6. Use o MediaMuxer para encapsular os dados de áudio e vídeo no buffer de saída compartilhado no formato final (como MP4).
  7. Após a conclusão da codificação e encapsulamento de áudio e vídeo, os recursos são liberados e a operação é concluída.

A codificação do vídeo ainda é a lógica anterior, a fonte de dados é obtida da Camera2 e depois adicionada ao codificador para processamento.

    private fun initVideoFormat() {
        //确定要竖屏的,真实场景需要根据屏幕当前方向来判断,这里简单写死为竖屏
        val videoWidth: Int
        val videoHeight: Int
        if (mPreviewSize!!.width > mPreviewSize!!.height) {
            videoWidth = mPreviewSize!!.height
            videoHeight = mPreviewSize!!.width
        } else {
            videoWidth = mPreviewSize!!.width
            videoHeight = mPreviewSize!!.height
        }
        YYLogUtils.w("MediaFormat的编码格式,宽:${videoWidth} 高:${videoHeight}")

        //配置MediaFormat信息(指定H264格式)
        val videoMediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight)
        //添加编码需要的颜色类型
        videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)
        //设置帧率
        videoMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
        //设置比特率
        videoMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mPreviewSize!!.width * mPreviewSize!!.height * 5)
        //设置关键帧I帧间隔
        videoMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)

        videoCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
        videoCodec!!.configure(videoMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        videoCodec!!.start()
    }

    /**
     * 视频流的编码处理线程
     */
    inner class VideoEncodeThread : Thread() {

        //由于摄像头数据的获取与编译不是在同一个线程,还是需要同步队列保存数据
        private val videoData = LinkedBlockingQueue<ByteArray>()

        // 用于Camera的回调中添加需要编译的原始数据,这里应该为YNV420
        fun addVideoData(byteArray: ByteArray?) {
            videoData.offer(byteArray)
        }

        override fun run() {
            super.run()

            initVideoFormat()

            while (!videoExit) {
                // 从同步队列中取出 YNV420格式,直接编码为H264格式
                val poll = videoData.poll()
                if (poll != null) {
                    encodeVideo(poll, false)
                }
            }

            //发送编码结束标志
            encodeVideo(ByteArray(0), true)

            // 当编码完成之后,释放视频编码器
            videoCodec?.release()
        }
    }

    //调用Codec硬编音频为AAC格式
    // dequeueInputBuffer 获取到索引,queueInputBuffer编码写入
    private fun encodeVideo(data: ByteArray, isFinish: Boolean) {

        val videoInputBuffers = videoCodec!!.inputBuffers
        var videoOutputBuffers = videoCodec!!.outputBuffers

        val index = videoCodec!!.dequeueInputBuffer(TIME_OUT_US)

        if (index >= 0) {

            val byteBuffer = videoInputBuffers[index]
            byteBuffer.clear()
            byteBuffer.put(data)

            if (!isFinish) {
                videoCodec!!.queueInputBuffer(index, 0, data.size, System.nanoTime() / 1000, 0)
            } else {
                videoCodec!!.queueInputBuffer(
                    index,
                    0,
                    0,
                    System.nanoTime() / 1000,
                    MediaCodec.BUFFER_FLAG_END_OF_STREAM
                )

            }
            val bufferInfo = MediaCodec.BufferInfo()
            Log.i("camera2", "编码video  $index 写入buffer ${data.size}")

            var dequeueIndex = videoCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)

            if (dequeueIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                if (MuxThread.videoMediaFormat == null)
                    MuxThread.videoMediaFormat = videoCodec!!.outputFormat
            }

            if (dequeueIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                videoOutputBuffers = videoCodec!!.outputBuffers
            }

            while (dequeueIndex >= 0) {
                val outputBuffer = videoOutputBuffers[dequeueIndex]
                if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
                    bufferInfo.size = 0
                }
                if (bufferInfo.size != 0) {
                    muxerThread?.addVideoData(outputBuffer, bufferInfo)
                }

                Log.i(
                    "camera2",
                    "编码后video $dequeueIndex buffer.size ${bufferInfo.size} buff.position ${outputBuffer.position()}"
                )

                videoCodec!!.releaseOutputBuffer(dequeueIndex, false)

                if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                    dequeueIndex = videoCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
                } else {
                    break
                }
            }
        }
    }

Para processamento de áudio, não precisamos de processamento de fila síncrona e usamos codificação síncrona para processar diretamente em um thread.

    inner class AudioEncodeThread : Thread() {

        //由于音频使用同步的方式编译,且在同一个线程内,所以不需要额外使用同步队列来处理数据
//        private val audioData = LinkedBlockingQueue<ByteArray>()

        override fun run() {
            super.run()
            prepareAudioRecord()
        }
    }

    /**
     * 准备初始化AudioRecord
     */
    private fun prepareAudioRecord() {
        initAudioFormat()

        // 初始化音频录制器
        audioRecorder = AudioRecord(
            MediaRecorder.AudioSource.MIC, AudioConfig.SAMPLE_RATE,
            AudioConfig.CHANNEL_CONFIG, AudioConfig.AUDIO_FORMAT, minSize
        )

        if (audioRecorder!!.state == AudioRecord.STATE_INITIALIZED) {

            audioRecorder?.run {
                //启动音频录制器开启录音
                startRecording()

                //读取音频录制器内的数据
                val byteArray = ByteArray(SAMPLES_PER_FRAME)
                var read = read(byteArray, 0, SAMPLES_PER_FRAME)

                //已经在录制了,并且读取到有效数据
                while (read > 0 && isRecording) {
                    //拿到音频原始数据去编译为音频AAC文件
                    encodeAudio(byteArray, read, getPTSUs())
                    //继续读取音频原始数据,循环执行
                    read = read(byteArray, 0, SAMPLES_PER_FRAME)
                }

                // 当录制完成之后,释放录音器
                audioRecorder?.release()

                //发送EOS编码结束信息
                encodeAudio(ByteArray(0), 0, getPTSUs())

                // 当编码完成之后,释放音频编码器
                audioCodec?.release()
            }
        }
    }

    //调用Codec硬编音频为AAC格式
    // dequeueInputBuffer 获取到索引,queueInputBuffer编码写入
    private fun encodeAudio(audioArray: ByteArray?, read: Int, timeStamp: Long) {
        val index = audioCodec!!.dequeueInputBuffer(TIME_OUT_US)
        val audioInputBuffers = audioCodec!!.inputBuffers

        if (index >= 0) {
            val byteBuffer = audioInputBuffers[index]
            byteBuffer.clear()
            byteBuffer.put(audioArray, 0, read)
            if (read != 0) {
                audioCodec!!.queueInputBuffer(index, 0, read, timeStamp, 0)
            } else {
                audioCodec!!.queueInputBuffer(
                    index,
                    0,
                    read,
                    timeStamp,
                    MediaCodec.BUFFER_FLAG_END_OF_STREAM
                )

            }

            val bufferInfo = MediaCodec.BufferInfo()
            Log.i("camera2", "编码audio  $index 写入buffer ${audioArray?.size}")
            var dequeueIndex = audioCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
            if (dequeueIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                if (MuxThread.audioMediaFormat == null) {
                    MuxThread.audioMediaFormat = audioCodec!!.outputFormat
                }
            }
            var audioOutputBuffers = audioCodec!!.outputBuffers
            if (dequeueIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                audioOutputBuffers = audioCodec!!.outputBuffers
            }
            while (dequeueIndex >= 0) {
                val outputBuffer = audioOutputBuffers[dequeueIndex]
                Log.i(
                    "camera2",
                    "编码后audio $dequeueIndex buffer.size ${bufferInfo.size} buff.position ${outputBuffer.position()}"
                )
                if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
                    bufferInfo.size = 0
                }
                if (bufferInfo.size != 0) {
//                    Log.i("camera2", "音频时间戳  ${bufferInfo.presentationTimeUs / 1000}")

//                    bufferInfo.presentationTimeUs = getPTSUs()

                    val byteArray = ByteArray(bufferInfo.size + 7)
                    outputBuffer.get(byteArray, 7, bufferInfo.size)
                    addADTStoPacket(0x04, byteArray, bufferInfo.size + 7)
                    outputBuffer.clear()
                    val headBuffer = ByteBuffer.allocate(byteArray.size)
                    headBuffer.put(byteArray)
                    muxerThread?.addAudioData(outputBuffer, bufferInfo)  //直接加入到封装线程了

//                    prevOutputPTSUs = bufferInfo.presentationTimeUs

                }

                audioCodec!!.releaseOutputBuffer(dequeueIndex, false)
                if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                    dequeueIndex = audioCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
                } else {
                    break
                }
            }
        }

    }

    private fun initAudioFormat() {
        audioMediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AudioConfig.SAMPLE_RATE, 1)
        audioMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
        audioMediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
        audioMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minSize * 2)

        audioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
        audioCodec!!.configure(audioMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        audioCodec!!.start()
    }

    private fun getPTSUs(): Long {

        var result = System.nanoTime() / 1000L

        if (result < prevOutputPTSUs)
            result += prevOutputPTSUs - result
        return result
    }

    /**
     * 添加ADTS头
     */
    private fun addADTStoPacket(sampleRateType: Int, packet: ByteArray, packetLen: Int) {
        val profile = 2 // AAC LC
        val chanCfg = 1 // CPE

        packet[0] = 0xFF.toByte()
        packet[1] = 0xF9.toByte()
        packet[2] = ((profile - 1 shl 6) + (sampleRateType shl 2) + (chanCfg shr 2)).toByte()
        packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
        packet[4] = (packetLen and 0x7FF shr 3).toByte()
        packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
        packet[6] = 0xFC.toByte()
    }

Quando nossa codificação de vídeo ou codificação de áudio estiver concluída, podemos adicionar os dados codificados ao buffer de dados de áudio e vídeo do MediaMuxer.

  class MuxThread(val file: File) : Thread() {

        //音频缓冲区
        private val audioData = LinkedBlockingQueue<EncodeData>()
        //视频缓冲区
        private val videoData = LinkedBlockingQueue<EncodeData>()

        companion object {
            var muxIsReady = false
            var videoMediaFormat: MediaFormat? = null
            var audioMediaFormat: MediaFormat? = null
            var muxExit = false
        }

        private lateinit var mediaMuxer: MediaMuxer

        /**
         * 需要先初始化Audio线程与资源,然后添加数据源到封装类中
         */
        fun addAudioData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
            audioData.offer(EncodeData(byteBuffer, bufferInfo))
        }

        /**
         * 需要先初始化Video线程与资源,然后添加数据源到封装类中
         */
        fun addVideoData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
            videoData.offer(EncodeData(byteBuffer, bufferInfo))
        }


        private fun initMuxer() {

            mediaMuxer = MediaMuxer(file.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

            videoAddTrack = mediaMuxer.addTrack(videoMediaFormat!!)
            audioAddTrack = mediaMuxer.addTrack(audioMediaFormat!!)

            mediaMuxer.start()
            muxIsReady = true

        }

        private fun muxerParamtersIsReady() = audioMediaFormat != null && videoMediaFormat != null

        override fun run() {
            super.run()

            //校验音频编码与视频编码不为空
            while (!muxerParamtersIsReady()) {
            }

            initMuxer()

            while (!muxExit) {
                if (audioAddTrack != -1) {
                    if (audioData.isNotEmpty()) {
                        val poll = audioData.poll()
                        Log.i("camera2", "混合写入audio音频 ${poll.bufferInfo.size} ")
                        mediaMuxer.writeSampleData(audioAddTrack, poll.buffer, poll.bufferInfo)

                    }
                }
                if (videoAddTrack != -1) {
                    if (videoData.isNotEmpty()) {
                        val poll = videoData.poll()
                        Log.i("camera2", "混合写入video视频 ${poll.bufferInfo.size} ")
                        mediaMuxer.writeSampleData(videoAddTrack, poll.buffer, poll.bufferInfo)

                    }
                }
            }

            mediaMuxer.stop()
            mediaMuxer.release()

            Log.i("camera2", "合成器释放")
            Log.i("camera2", "未写入audio音频 ${audioData.size}")
            Log.i("camera2", "未写入video视频 ${videoData.size}")

        }
    }

Quando começamos a gravar, iniciamos esses threads e, em seguida, começamos a codificar dados de áudio e dados de vídeo, respectivamente. Quando os dados de áudio e vídeo são codificados e vinculados a carimbos de data/hora presentes, eles são finalmente adicionados ao MuxThread para iniciar a síntese e o MuxThread sintetizado internamente Segure os dados finais de áudio e vídeo, inicie internamente a travessia e julgue se há dados, comece a sintetizar em arquivos MP4, defina uma variável Flag quando a reprodução parar, pare a codificação e envie os arquivos.

Efeito:

imagem.png

[Nota] Este é apenas o nível de demonstração, que é usado apenas para demonstrar o uso da API. Não o use para projetos reais. Existem muitos bugs internos e problemas de compatibilidade. Quando a gravação for interrompida, ela será interrompida imediatamente. Por exemplo, a gravação é de 10 segundos, mas o vídeo real é de apenas 8 segundos, porque não há buffer de parada, liberação de recursos e assim por diante não são processados. Se você deseja escrever MediaCodec + MediaMuxer sozinho, pode recomendar a implementação do código-fonte do VideoCapture abaixo.

5. CameraX vem com gravação de vídeo

Se houver um ou outro problema com o código codificado síncrono que implementamos acima, o que devo fazer se quiser uma gravação de vídeo pronta para uso? Na verdade, a gravação do CameraX é suficiente para nós. Se houver não há necessidade de alguns efeitos especiais, usamos o VideoCapture da CameraX para atender plenamente às necessidades!

Se, de acordo com o uso anterior, o código estiver codificado no retorno de chamada, precisamos definir o retorno de chamada, obter o objeto Image e, em seguida, escrever MediaCodec + MediaMuxer por nós mesmos, o que não é diferente do uso acima e da gravação de vídeo função pode ser realizada da mesma maneira.

      ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
            .setTargetAspectRatio(screenAspectRatio)
            .setTargetRotation(rotation)
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build();

        // 在每一帧上应用颜色矩阵
        imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), new MyAnalyzer(mContext));

    private class MyAnalyzer implements ImageAnalysis.Analyzer {

        private YuvUtils yuvUtils = new YuvUtils();

        public MyAnalyzer(Context context) {

        }

        @Override
        public void analyze(@NonNull ImageProxy image) {

            // 使用C库获取到I420格式,对应 COLOR_FormatYUV420Planar
            YuvFrame yuvFrame = yuvUtils.convertToI420(image.getImage());
            // 与MediaFormat的编码格式宽高对应
            yuvFrame = yuvUtils.rotate(yuvFrame, 90);

            // 旋转90度之后的I420格式添加到同步队列
            videoThread.addVideoData(yuvFrame.asArray());

        }
    }

    // 启动 AudioRecord 音频录制以及编码等逻辑


Mas para esta série de operações de codificação, o CameraX já escreveu o caso de uso VideoCapture para gravarmos o vídeo. Ele já encapsula a lógica do MediaCodec + MediaMuxer para nós. Precisamos usá-lo com muita facilidade:

    //录制视频对象
    mVideoCapture = VideoCapture.Builder()
        .setTargetAspectRatio(screenAspectRatio)
        .setAudioRecordSource(MediaRecorder.AudioSource.MIC) //设置音频源麦克风
        //视频帧率
        .setVideoFrameRate(30)
        //bit率
        .setBitRate(3 * 1024 * 1024)
        .build()

    // 开始录制
    fun startCameraRecord(outFile: File) {
        mVideoCapture ?: return

        val outputFileOptions: VideoCapture.OutputFileOptions = VideoCapture.OutputFileOptions.Builder(outFile).build()

        mVideoCapture!!.startRecording(outputFileOptions, mExecutorService, object : VideoCapture.OnVideoSavedCallback {
            override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
                YYLogUtils.w("视频保存成功,outputFileResults:" + outputFileResults.savedUri)
                mCameraCallback?.takeSuccess()
            }

            override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                YYLogUtils.e(message)
            }
        })
    }


Quando terminarmos a configuração, podemos usar este caso de uso para iniciar a gravação de vídeo. Sua implementação interna é diferente do nosso método anterior. Não codifica através dos formatos I420 ou NV21, mas codifica diretamente através do Surface. O código da chave é o seguinte:

 ...

 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);

 // 绑定到 Surface 

  Surface cameraSurface = mVideoEncoder.createInputSurface();
        mCameraSurface = cameraSurface;

        SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);

        if (mDeferrableSurface != null) {
            mDeferrableSurface.close();
        }
        mDeferrableSurface = new ImmediateSurface(mCameraSurface);
        mDeferrableSurface.getTerminationFuture().addListener(
                cameraSurface::release, CameraXExecutors.mainThreadExecutor()
        );
   sessionConfigBuilder.addSurface(mDeferrableSurface);


Criou um objeto Surface de entrada antes e o associou ao codificador de vídeo. Use o método sessionConfigBuilder.addSurface() para adicionar mDeferrableSurface à configuração da sessão para garantir que o codificador de vídeo use essa superfície para entrada de dados. Dessa forma, os dados da câmera podem ser inseridos no codificador de vídeo por meio do Surface para processamento de codificação.

O código-fonte está em androidx.camera.core.VideoCapture. O Google o escreveu muito bem. Pessoalmente, prefiro esse método. A compatibilidade e a robustez do uso de COLOR_FormatSurface são melhores.

Resumir

Este artigo geralmente fala sobre alguns métodos de codificação fornecidos pelo próprio Android, que são essencialmente MediaCodec e algumas ferramentas baseadas em sua embalagem. Incluindo MediaRecorder e VideoCapture são implementados com base nele.

Há um pouco mais de código neste artigo e podemos simplesmente entender os diferentes métodos de codificação das fontes de dados I420, NV21 e Surface por meio do código. Diferentes configurações de MediaCodec representam que tipo de impacto. Também podemos entender os diferentes usos de vários métodos de codificação, qual é a diferença entre retorno de chamada assíncrono e processamento síncrono e como usar o sintetizador de encapsulamento?

Comparando os métodos de codificação de várias fontes de dados, eu pessoalmente prefiro o método Surface (preferência pessoal), que tem melhor compatibilidade e escalabilidade posterior, incluindo a visualização e gravação de efeitos especiais no estágio posterior e o vídeo de efeito especial gravado. ser mais conveniente.

Deve-se notar que, em relação à implementação de diferentes MediaCodecs, usamos métodos diferentes neste artigo, mas todos são métodos de aplicação de algumas APIs. Não há tempo para melhorá-los e sua robustez não é boa. Você pode usar para aprendizado de referência. Não é recomendado que você o use diretamente. É realmente recomendável usar o VideoCapture do sistema, que é totalmente suficiente para alguns efeitos de gravação simples. (Se você quiser usá-lo diretamente, você pode ler o seguinte artigo)

Como o VideoCapture no Camerax é tão bom, ele pode ser realizado na Camera1 ou Camera2 por meio de seu método de gravação de vídeo?

afinal

Se você deseja se tornar um arquiteto ou deseja ultrapassar a faixa salarial de 20 a 30 mil, não se limite a codificação e negócios, mas deve ser capaz de selecionar modelos, expandir e melhorar o pensamento de programação. Além disso, um bom plano de carreira também é muito importante, e o hábito de aprender é muito importante, mas o mais importante é saber perseverar.Qualquer plano que não pode ser implementado de forma consistente é conversa fiada.

Se você não tem direção, gostaria de compartilhar com você um conjunto de "Notas avançadas sobre os oito principais módulos do Android", escrito pelo arquiteto sênior de Ali, para ajudá-lo a organizar sistematicamente o conhecimento confuso, disperso e fragmentado, de modo que você possa dominar de forma sistemática e eficiente os vários pontos de conhecimento do desenvolvimento do Android.
insira a descrição da imagem aqui
Em comparação com o conteúdo fragmentado que costumamos ler, os pontos de conhecimento desta nota são mais sistemáticos, mais fáceis de entender e lembrar e são organizados estritamente de acordo com o sistema de conhecimento.

Conjunto completo de materiais de vídeo:

1. Coleta de entrevistas

insira a descrição da imagem aqui
2. Coleta de análise de código-fonte
insira a descrição da imagem aqui

3. A coleção de estruturas de código aberto
insira a descrição da imagem aqui
dá as boas-vindas a todos para oferecer suporte com um clique e três links. Se você precisar das informações no artigo, basta clicar no cartão WeChat de certificação oficial da CSDN no final do artigo para obtê-lo gratuitamente↓↓↓

Acho que você gosta

Origin blog.csdn.net/Eqiqi/article/details/131987358
Recomendado
Clasificación