Android 音视频开发第4弹 - 音视频分离与合成

Android 的音视频分离与合成需要用到 MediaExtractor 和 MediaMuxer,MediaExtractor 可以从一个媒体文件中提取出音频或视频流,并以数据块(buffer)的形式提供给应用程序,MediaMuxer 则可以将音频或视频数据存储到容器中,并将其写入到指定的输出文件中。

分离

MediaExtractor 可以用来分离容器中的视频轨道和音频轨道,支持多种常见的媒体格式,例如 MP4,3GP,WebM,FLV,MPEG-TS 等等。

主要 API 如下:

  • setDataSource(String path):设置媒体文件的路径
  • getTrackCount():获取媒体文件中的音视频轨道数量
  • getTrackFormat(int index):获取指定音视频轨道的格式
  • selectTrack(int index):选择指定音视频轨道
  • readSampleData(ByteBuffer byteBuf, int offset):读取一帧数据
  • advance():读取下一帧数据
  • release():释放资源

举个例子,现在有个 mp4 格式的音视频,需要将它的视频和音频分离开来,怎么做呢?这就可以使用 MediaExtractor ,大概的使用步骤是:设置数据源,获取轨道数,选择特定的轨道,然后循环读取每帧的样本数据,完成后释放资源即可,代码如下:

kotlin 复制代码
private fun separateVideo() {
    val mediaExtractor = MediaExtractor()
    // 源视频存放的路径
    val filePath = getExternalFilesDir(null)!!.absolutePath + "/test_video.mp4"
    try {
        // 设置数据源,可以是本地文件或者网络地址。
        mediaExtractor.setDataSource(filePath)
        // 获取轨道数
        val trackCount = mediaExtractor.trackCount
        // 遍历轨道,查看音频轨道或视频轨道信息
        for (i in 0 until trackCount) {
            // 获取某一个轨道的媒体格式
            val trackFormat = mediaExtractor.getTrackFormat(i)
            val keyMime = trackFormat.getString(MediaFormat.KEY_MIME)
            if (keyMime.isNullOrEmpty()) {
                continue
            }
            // 通过 MIME 信息识别音频轨道和视频轨道
            if (keyMime.startsWith("video/")) {
                val outputFile = getOutputFile(mediaExtractor, i, "/video.mp4")
                Log.i(TAG, "video file path:${outputFile.absolutePath}")
            } else if (keyMime.startsWith("audio/")) {
                val outputFile = getOutputFile(mediaExtractor, i, "/audio.aac")
                Log.i(TAG, "audio file path:${outputFile.absolutePath}")
            }

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

其中,getOutputFile 方法就是确定音频轨道或视频轨道后的文件输出,代码如下:

kotlin 复制代码
@Throws(IOException::class)
private fun getOutputFile(mediaExtractor: MediaExtractor, i: Int, outputName: String): File {
    val trackFormat = mediaExtractor.getTrackFormat(i)
    mediaExtractor.selectTrack(i)
    // 文件保存路径
    val outputFile =
        File(getExternalFilesDir(Environment.DIRECTORY_MUSIC)!!.absolutePath + outputName)
    if (outputFile.exists()) {
        outputFile.delete()
    }
    val mediaMuxer =
        MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
    // 添加轨道信息
    mediaMuxer.addTrack(trackFormat)
    // 开始合成
    mediaMuxer.start()
    // 设置每一帧的大小
    val buffer = ByteBuffer.allocate(500 * 1024)
    val bufferInfo = MediaCodec.BufferInfo()
    var sampleSize: Int
    // 循环读取每帧样本数据
    while (mediaExtractor.readSampleData(buffer, 0).also { sampleSize = it } > 0) {
        bufferInfo.apply {
            flags = mediaExtractor.sampleFlags
            offset = 0
            size = sampleSize
            presentationTimeUs = mediaExtractor.sampleTime
        }
        // 通过 mediaExtractor 解封装的数据通过 writeSampleData 写入到对应的轨道
        mediaMuxer.writeSampleData(0, buffer, bufferInfo)
        // 读取下一帧数据
        mediaExtractor.advance()
    }
    // 提取完毕
    mediaExtractor.unselectTrack(i)
    mediaMuxer.stop()
    mediaMuxer.release()
    return outputFile
}

执行程序之后,我们就会发现 test_video.mp4 这个文件被分离成了视频和音频两个文件。

合成

现在,我们把源音视频文件 test_video.mp4 删了,通过 audio.aac 音频文件和 video.mp4 视频文件合成一个音视频文件 test_video.mp4,这就使用到了 MediaMuxer,其实上面的代码也有用到,只是用途不同而已。MediaMuxer 除了可以生成音频或视频文件,还可以把音频与视频合成一个音视频文件。

主要 API 如下:

  • MediaMuxer(String path, int format):path 为输出文件的名称,format 指输出文件的格式
  • addTrack(MediaFormat format):添加轨道
  • start():开始合成文件
  • writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把ByteBuffer 中的数据写入到在构造器设置的文件中
  • stop():停止合成文件
  • release():释放资源

MediaMuxer 大概的使用步骤是:设置目标文件路径和音视频格式,添加要合成的轨道,包括音频轨道和视频轨道,然后开始合成,循环写入每帧样本数据,完成后释放即可,代码如下:

kotlin 复制代码
private fun compositeVideo(): String? {
    val videoFile = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "video.mp4")
    val audioFile = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "audio.aac")
    // 输出文件
    val outputFile = File(getExternalFilesDir(null), "test_video.mp4")
    if (outputFile.exists()) {
        outputFile.delete()
    }
    if (!videoFile.exists() || !audioFile.exists()) {
        return null
    }
    val videoExtractor = MediaExtractor()
    val audioExtractor = MediaExtractor()
    try {
        val mediaMuxer =
            MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        var videoTrackIndex = 0
        var audioTrackIndex = 0
        //添加视频轨道
        videoExtractor.setDataSource(videoFile.absolutePath)
        val videoTrackCount = videoExtractor.trackCount
        for (i in 0 until videoTrackCount) {
            val trackFormat = videoExtractor.getTrackFormat(i)
            val mimeType = trackFormat.getString(MediaFormat.KEY_MIME)
            if (mimeType.isNullOrEmpty()) {
                continue
            }
            if (mimeType.startsWith("video/")) {
                videoExtractor.selectTrack(i)
                videoTrackIndex = mediaMuxer.addTrack(trackFormat)
                break
            }
        }
        // 添加音频轨道
        audioExtractor.setDataSource(audioFile.absolutePath)
        val audioTrackCount = audioExtractor.trackCount
        for (i in 0 until audioTrackCount) {
            val trackFormat = audioExtractor.getTrackFormat(i)
            val mimeType = trackFormat.getString(MediaFormat.KEY_MIME)
            if (mimeType.isNullOrEmpty()) {
                continue
            }
            if (mimeType.startsWith("audio/")) {
                audioExtractor.selectTrack(i)
                audioTrackIndex = mediaMuxer.addTrack(trackFormat)
                break
            }
        }
        // 开始合成
        mediaMuxer.start()
        val byteBuffer = ByteBuffer.allocate(500 * 1024)
        val bufferInfo = MediaCodec.BufferInfo()
        var videoSampleSize: Int
        while (videoExtractor.readSampleData(byteBuffer, 0).also { videoSampleSize = it } > 0) {
            bufferInfo.apply {
                flags = videoExtractor.sampleFlags
                offset = 0
                size = videoSampleSize
                presentationTimeUs = videoExtractor.sampleTime
            }
            mediaMuxer.writeSampleData(videoTrackIndex, byteBuffer, bufferInfo)
            videoExtractor.advance()
        }
        var audioSampleSize: Int
        val audioBufferInfo = MediaCodec.BufferInfo()
        while (audioExtractor.readSampleData(byteBuffer, 0).also { audioSampleSize = it } > 0) {
            audioBufferInfo.apply {
                flags = audioExtractor.sampleFlags
                offset = 0
                size = audioSampleSize
                presentationTimeUs = audioExtractor.sampleTime
            }
            mediaMuxer.writeSampleData(audioTrackIndex, byteBuffer, audioBufferInfo)
            audioExtractor.advance()
        }
        // 释放资源
        videoExtractor.release()
        audioExtractor.release()
        mediaMuxer.stop()
        mediaMuxer.release()
        return outputFile.absolutePath
    } catch (e: IOException) {
        e.printStackTrace()
        return null
    }
}
相关推荐
WilliamLuo1 天前
MP4结构初识-第一篇
前端·javascript·音视频开发
音视频牛哥7 天前
Android平台如何拉取RTSP|RTMP流并转发至轻量级RTSP服务?
音视频开发·视频编码·直播
声知视界7 天前
音视频基础能力之 iOS 视频篇(一):视频采集
音视频开发
关键帧Keyframe10 天前
音视频面试题集锦第 15 期 | 编辑 SDK 架构 | 直播回声 | 播放器架构
音视频开发·视频编码·客户端
关键帧Keyframe15 天前
iOS 不用 libyuv 也能高效实现 RGB/YUV 数据转换丨音视频工业实战
音视频开发·视频编码·客户端
关键帧Keyframe17 天前
音视频面试题集锦第 7 期
音视频开发·视频编码·客户端
关键帧Keyframe17 天前
音视频面试题集锦第 8 期
ios·音视频开发·客户端
蚝油菜花22 天前
MimicTalk:字节跳动和浙江大学联合推出 15 分钟生成 3D 说话人脸视频的生成模型
人工智能·开源·音视频开发
音视频牛哥24 天前
Android平台RTSP|RTMP播放器高效率如何回调YUV或RGB数据?
音视频开发·视频编码·直播
<Sunny>1 个月前
MPP音视频总结
音视频开发·1024程序员节·海思mpp