研究Android音视频-3-在Android设备上采集音视频并使用MediaCodec编码为H.264

Posted Android Developer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了研究Android音视频-3-在Android设备上采集音视频并使用MediaCodec编码为H.264相关的知识,希望对你有一定的参考价值。

WechatIMG24.png

本文解决的问题

本文主要使用MediaCodec硬编码器对android设备采集的音视频编码

  1. 封装音视频基础编码器
  2. 封装音频编码器
  3. 封装视频编码器
  4. 使用新封装的视频编码器改造示例2
  5. 使用Camera进行视频录制(YUV420SP)并保存为视频流(H.264)
  6. 使用AudioRecord进行音频录制(PCM)并保存为音频流(AAC)
  7. 使用MediaMuxer混合器合并视频和音频为一路流(H.264)(待完成)

示例链接

一、封装音视频基础编码器

  1. 定义编码接口类ICodec:
interface ICodec {
    //入队
    fun putBuf(data: ByteArray, offset: Int, size: Int)
    //处理数据
    fun dealWith(data: ByteArray)
    //停止线程
    fun stopWorld()
}
复制代码
  1. 定义编码任务线程BaseCodec:

音视频逐帧编码,编码是一项耗时任务,所以需要定义一队列来存储需要编码的数据帧.

BaseCodec为抽象类并且实现了ICodec接口,在任务启动时不停的从队列中取出数据,dealWith方法进行处理

abstract class BaseCodec : Thread(), ICodec {

    val inBlockingQueue = ArrayBlockingQueue<ByteArray>(30)

    override fun putBuf(data: ByteArray, offset: Int, size: Int) {
        val byteArray = ByteArray(size)
        System.arraycopy(data, offset, byteArray, 0, size)
        inBlockingQueue.put(byteArray)
    }

    var threadRunning = true;

    override fun run() {

        try {
            BaseCodecLoop1@ while (threadRunning) {
                val item = inBlockingQueue.take()
                dealWith(item)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun dealWith(data: ByteArray) {}

    override fun stopWorld() {
        inBlockingQueue.clear()
        threadRunning = false;
        interrupt()
        join(1000)
    }
}

const val SAMPLE_RATE_IN_HZ = 44100 //采样率44.1KHz
const val CHANNEL = AudioFormat.CHANNEL_IN_MONO //单声道,立体声:CHANNEL_IN_STEREO
const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT //每个采样点16bit
const val DEST_BIT_RATE = 128000 //编码码率
复制代码
  1. 封装MediaCodec音视频编码基础类BaseMediaCodec:

该类包含

  • 初始化对应mime类型的MediaCodec
/**
 * mime类型对应的格式
 * video/avc: h.264
 * video/hevc: h.265
 * audio/mp4a-latm: aac
 */
复制代码
  • 数据入队
  • 发送结束标记
  • 数据解码出队线程
  • CodecListener编码结束回传结果的接口
  • stopWorld释放接口

二、封装音频编码器

  • 初始化:默认初始化audio/mp4a-latm编码器
createCodec("audio/mp4a-latm")
val format = MediaFormat.createAudioFormat(mime, SAMPLE_RATE_IN_HZ, 1)
format.setInteger(MediaFormat.KEY_BIT_RATE, DEST_BIT_RATE)
//buffer 最大值
val bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_IN_HZ, CHANNEL, AUDIO_FORMAT)
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize)
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
configEncoderBitrateMode(format)
codec.start()
复制代码
  • 接收编码产生的数据
override fun bufferUpdate(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
    buffer.position(bufferInfo.offset)
    buffer.limit(bufferInfo.offset + bufferInfo.size)
    val data = ByteArray(bufferInfo.size + 7)
    addADTStoPacket(data, data.size)
    buffer.get(data, 7, bufferInfo.size)
    buffer.position(bufferInfo.offset)
    listener?.bufferUpdate(data)
}

/**
 * 添加ADTS头部的7个字节
 */
private fun addADTStoPacket(packet: ByteArray, packetLen: Int) {
    val profile = 2 // AAC LC
    val freqIdx: Int = 4// 44.1kHz
    val chanCfg = 2 // CPE
    packet[0] = 0xFF.toByte()
    packet[1] = 0xF9.toByte()
    packet[2] = ((profile - 1 shl 6) + (freqIdx 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()
}
复制代码

三、封装视频编码器

  • 初始化

init {
    createCodec("video/avc")
}

fun setUpVideoCodec(width: Int, height: Int) {
    val format = MediaFormat.createVideoFormat(mime, width, height)
    format.setInteger(
        MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
    )
    //width*height*frameRate*[0.1-0.2]码率控制清晰度
    format.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 3)
    format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
    //每秒出一个关键帧,设置0为每帧都是关键帧
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
//        format.setInteger(
//            MediaFormat.KEY_BITRATE_MODE,
//            MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR//遵守用户设置的码率
//        )
    configEncoderBitrateMode(format)

//        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
//        codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    codec.start()
}
复制代码
  • 接收编码产物
override fun bufferUpdate(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
    listener?.bufferUpdate(buffer, bufferInfo)
}
复制代码

四、使用新封装的视频编码器改造示例2

接下来改造第二篇文章的yuv编码mp4代码:

步骤如下:示例代码链接

  • 定义混合器
  • 定义编码器
  • 读取yuv文件,开始编码
  • CodecListener回调中接收编码完成的数据,使用MediaMuxer混合器保存视频文件
  • 读取文件结束,编码结束
fun convertYuv2Mp4_2(context: Context) {
    val yuvPath = "${context.filesDir}/test.yuv"
    val saveMp4Path = "${context.filesDir}/test.mp4"
    File(saveMp4Path).deleteOnExit()

    //定义混合器:输出并保存h.264码流为mp4
    val mediaMuxer =
        MediaMuxer(
            saveMp4Path,
            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
        );
    var muxerTrackIndex = -1

    val videoEncoder = VideoEncoder()
    videoEncoder.setUpVideoCodec(1920, 1080)
    videoEncoder.start()
    videoEncoder.setCodecListener(object : CodecListener {
        override fun formatUpdate(format: MediaFormat) {
            //step3.1 标记新的解码数据到来,在此添加视频轨道到混合器
            muxerTrackIndex = mediaMuxer.addTrack(format)
            mediaMuxer.start()
        }

        override fun bufferUpdate(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
            mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo)
        }

        override fun bufferOutputEnd() {
            mediaMuxer.release()
            videoEncoder.stopWorld()
        }
    })

    val byteArray = ByteArray(1920 * 1080 * 3 / 2)
    var read = 0
    FileInputStream(yuvPath).use { fis ->
        while (true) {
            read = fis.read(byteArray)
            if (read == byteArray.size) {
                Thread.sleep(30)
                videoEncoder.putBuf(byteArray, 0, byteArray.size)
            } else {
                videoEncoder.putBufEnd()
                break
            }
        }
    }

}
复制代码

使用Camera进行视频录制并保存为视频流

流程与前一示例基本一致,只是获取yuv数据从文件修改到camera实时流

  • 定义编码器VideoEncoder,混合器MediaMuxer
  • 启动Camera,并在回调中获取yuv数据流输入编码器
  • 在编码器回调中获取编码完成的数据,使用混合器保存至本地
  • 退出页面时,调用videoEncoder.putBufEnd()方法通知编码器结束

示例代码如下:链接

var capture = false;

//视频编码为mp4
val videoEncoder = VideoEncoder()
override fun initView() {

    videoEncoder.setUpVideoCodec(640, 480)
    videoEncoder.start()
    binding.cameraview0.apply {
        cameraParams.facing = 1
        cameraParams.isScaleWidth = false
        cameraParams.oritationDisplay = 90
        cameraParams.previewSize.previewWidth = 640
        cameraParams.previewSize.previewHeight = 480
        cameraParams.isFilp = false

        addPreviewFrameCallback(object : CameraView.PreviewFrameCallback {
            override fun analyseData(data: ByteArray?): Any {
                if (capture) {
                    videoEncoder.putBuf(data!!, 0, data.size)
                }
                return 0
            }
            override fun analyseDataEnd(p0: Any?) {}
        })
    }
    addLifecycleObserver(binding.cameraview0)

    val saveMp4Path = "${filesDir}/test.mp4"
    File(saveMp4Path).deleteOnExit()
    //定义混合器:输出并保存h.264码流为mp4
    val mediaMuxer =
        MediaMuxer(
            saveMp4Path,
            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
        )
    var muxerTrackIndex = -1

    videoEncoder.setCodecListener(object : CodecListener {
        override fun formatUpdate(format: MediaFormat) {
            muxerTrackIndex = mediaMuxer.addTrack(format)
            mediaMuxer.start()
        }

        override fun bufferUpdate(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
            mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo)
        }

        override fun bufferOutputEnd() {
            mediaMuxer.release()
            videoEncoder.stopWorld()
        }
    })

    binding.cameraview0.setOnClickListener {
        DLog.d("录制:$capture")
        capture = !capture
    }

}

override fun onDestroy() {
    videoEncoder.putBufEnd()
    super.onDestroy()
}
复制代码

使用AudioRecord进行音频录制并保存为音频流

使用AudioRecord采集pcm裸数据,然后送往MediaCodec编码器编码为aac

示例代码

===========

==深刻注意点:初始化AudioRecord录制音频时使用的声道数必须跟初始化编码器MediaFormat时输入的声道数保持一致,否则会出现裸数据pcm播放正常,编码好的aac数据播放异常,叽里呱啦的,,,貌似加速,android设备就都写1吧,,,立体声基本上都不支持

AudioFormat.CHANNEL_IN_MONO: 单声道 对应声道数 1
AudioFormat.CHANNEL_IN_STEREO:立体声 对应声道数 2
复制代码

===========

流程基本与录制视频一致

    1. 创建AudioRecord,用于采集音频信息
    1. 创建AudioEncoder,用于编码采集到的PCM裸流
    1. 创建MediaMuxer,用于存储编码好的AAC数据
//存储pcm裸数据
val file_pcm = File("$filesDir/test.pcm")
//直接FileOutputStream写出,存储编码完成的aac数据
val file_mp3 = File("$filesDir/test.mp3")
//使用混合器mediaMuxer存储编码好的aac数据
val file_mp32 = File("$filesDir/test2.mp3")

binding.startRecord.setOnClickListener {
    file_pcm.deleteOnExit()
    file_mp3.deleteOnExit()
    file_mp32.deleteOnExit()
    thread {
        DLog.d("开始录制")
        isCapture = true
        val bufferSize = AudioRecord.getMinBufferSize(
            SAMPLE_RATE_IN_HZ, CHANNEL, AUDIO_FORMAT
        )
        val audioRecord = AudioRecord(
            MediaRecorder.Audiosource.MIC,
            SAMPLE_RATE_IN_HZ,
            CHANNEL,
            AUDIO_FORMAT,
            bufferSize
        )

        audioRecord.startRecording()
        val data = ByteArray(bufferSize)
        val fos = FileOutputStream(file_pcm)
        val fos_mp3 = FileOutputStream(file_mp3)

        //定义混合器:输出并保存编码后的aac为mp3
        val mediaMuxer =
            MediaMuxer(
                file_mp32.absolutePath,
                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
            )
        var muxerTrackIndex = -1

        val audioEncoder = AudioEncoder()
        audioEncoder.setCodecListener(object : CodecListener {
            override fun formatUpdate(format: MediaFormat) {
                super.formatUpdate(format)

            }

            override fun bufferUpdate(data: ByteArray) {
                super.bufferUpdate(data)
                DLog.d("bufferUpdate ${data.size}")
                fos_mp3.write(data, 0, data.size)
            }

            override fun bufferOutputEnd() {
                super.bufferOutputEnd()
                audioEncoder.stopWorld()
                fos_mp3.flush()
                fos_mp3.close()
            }
        })
        audioEncoder.start()

        while (isCapture) {
            val len = audioRecord.read(data, 0, data.size)
            audioEncoder.putBuf(data, 0, len)
            fos.write(data, 0, len);
        }

        fos.flush()
        fos.close()

        audioEncoder.putBufEnd()
        audioRecord.stop()
        DLog.d("录制结束")
    }
}
复制代码

使用MediaMuxer混合器合并视频和音频为一路流

本文在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

以上是关于研究Android音视频-3-在Android设备上采集音视频并使用MediaCodec编码为H.264的主要内容,如果未能解决你的问题,请参考以下文章

研究Android音视频-3-在Android设备上采集音视频并使用MediaCodec编码为H.264

Android 捕捉慢动作视频

几个不错的Android开源音视频播放器

5.3Android Studio录像

5.3Android Studio录像

5.3Android Studio录像