一个音视频文件是由音频和视频组成的,Android 提供了 MediaExtractor 和 MediaMuxer 类,用来把音频或视频单独抽取出来,然后合成新的视频。

我们分别看一下 API 的使用,实现分离 MP4 视频文件,然后再合成分离的音视频功能。

MediaExtractor 的使用主要有这么几步:

  1. 设置数据源
  2. 获取通道数,切换到想要的轨道
  3. 循环读取每帧的样本数据
  4. 完成后释放资源

MediaMuxer 的使用和 MediaExtractor 类似:

  1. 设置目标文件路径和音视频格式
  2. 添加要合成的轨道,包括音轨和视轨
  3. 开始合成,循环写入每帧样本数据
  4. 完成后释放资源

MediaFormat 封装了媒体数据(音频,视频,字幕)格式的信息,所有信息都以键值对形式表示。MediaFormat 中定义的 key 对于不同媒体数据并不是全部通用的,某些 key 只适用于特定媒体数据。

通用 Keys

KEY_MIME 格式类型
KEY_MAX_INPUT_SIZE 输出缓冲区的最大字节数
KEY_BIT_RATE 比特率

Video Keys

KEY_WIDTH    视频宽度
KEY_HEIGHT   视频高度
KEY_DURATION  内容持续时间(以微妙为单位)
KEY_CORLOR_FORMAT   视频内容颜色空间
KEY_FRAME_RATE   视频帧率
KEY_I_FRAME_INTERVAL   关键之间的时间间隔
KEY_ROTATION  视频旋转顺时针角度 
KEY_BITRATE     码率/比特率(画质和文件体积)
KEY_BITRATE_MODE    比特率模式

Audio Keys

KEY_CHANNEL_COUNT   通道数
KEY_SAMPLE_RATE   采样率  
KEY_DURATION   内容持续时间(以微妙为单位

下面是个提取并合成视频的示例,MediaExtractor 和 MediaMuxer 一起使用,相当于把原视频的声音通道去掉,生成一个无声的视频文件。

    // 分离视频的视频轨,输入视频 input.mp4,输出视频 output_video.mp4
    private void extractVideo() {
        logger.info("extractVideo() start");
        MediaExtractor mediaExtractor = new MediaExtractor();
        MediaMuxer mediaMuxer = null;
        try {
            File fileDir = FileUtils.getFileDir(this);
            // 设置视频源
            mediaExtractor.setDataSource(new File(fileDir, VIDEO_SOURCE).getAbsolutePath());
            // 轨道索引 ID
            int videoIndex = -1;
            // 视频轨道格式信息
            MediaFormat mediaFormat = null;
            // 数据源的轨道数(一般有视频,音频,字幕等)
            int trackCount = mediaExtractor.getTrackCount();
            // 循环轨道数,找到我们想要的视频轨
            for (int i = 0; i < trackCount; i++) {
                mediaFormat = mediaExtractor.getTrackFormat(i);
                String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
                // //找到要分离的视频轨
                if (mimeType.startsWith("video/")) {
                    videoIndex = i;
                    break;
                }
            }
            if (mediaFormat == null || videoIndex < 0) {
                return;
            }

            // 最大缓冲区字节数
            int maxInputSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
            // 格式类型
            String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
            // 视频的比特率
            int bitRate = mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE);
            // 视频宽度
            int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
            // 视频高度
            int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
            // 内容持续时间(以微妙为单位)
            long duration = mediaFormat.getLong(MediaFormat.KEY_DURATION);
            // 视频的帧率
            int frameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
            // 视频内容颜色空间
            int colorFormat = -1;
            if (mediaFormat.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
                mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
            }
            // 关键之间的时间间隔
            int iFrameInterval = -1;
            if (mediaFormat.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) {
                iFrameInterval = mediaFormat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);
            }
            //  视频旋转顺时针角度
            int rotation = -1;
            if (mediaFormat.containsKey(MediaFormat.KEY_ROTATION)) {
                rotation = mediaFormat.getInteger(MediaFormat.KEY_ROTATION);
            }
            // 比特率模式
            int bitRateMode = -1;
            if (mediaFormat.containsKey(MediaFormat.KEY_BITRATE_MODE)) {
                bitRateMode = mediaFormat.getInteger(MediaFormat.KEY_BITRATE_MODE);
            }

            logger.info("mimeType:{}, maxInputSize:{}, bitRate:{}, width:{}, height:{}" +
                            ", duration:{}ms, frameRate:{}, colorFormat:{}, iFrameInterval:{}" +
                            ", rotation:{}, bitRateMode:{}", mimeType, maxInputSize, bitRate, width, height
                    , duration / 1000, frameRate, colorFormat, iFrameInterval, rotation, bitRateMode);
            //切换视频的轨道
            mediaExtractor.selectTrack(videoIndex);

            String outPath = new File(fileDir, OUTPUT_VIDEO).getAbsolutePath();
            mediaMuxer = new MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            //将视频轨添加到 MediaMuxer,并返回新的轨道
            int trackIndex = mediaMuxer.addTrack(mediaFormat);
            ByteBuffer byteBuffer = ByteBuffer.allocate(maxInputSize);
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            // 开始合成
            mediaMuxer.start();
            while (true) {
                // 检索当前编码的样本并将其存储在字节缓冲区中
                int readSampleSize = mediaExtractor.readSampleData(byteBuffer, 0);
                //  如果没有可获取的样本则退出循环
                if (readSampleSize < 0) {
                    mediaExtractor.unselectTrack(videoIndex);
                    break;
                }
                // 设置样本编码信息
                bufferInfo.size = readSampleSize;
                bufferInfo.offset = 0;
                bufferInfo.flags = mediaExtractor.getSampleFlags();
                bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();
                //写入样本数据
                mediaMuxer.writeSampleData(trackIndex, byteBuffer, bufferInfo);
                //推进到下一个样本,类似快进
                mediaExtractor.advance();
            }

            logger.info("finish extract video, path:{}", outPath);
            Toast.makeText(this, "分离视频完成", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            logger.error(e);
        } finally {
            if (mediaMuxer != null) {
                mediaMuxer.stop();
                mediaMuxer.release();
            }
            mediaExtractor.release();
        }
    }

同样地,分离音频和合成音视频的过程和上面差不多,代码在 GitHub 上,欢迎参阅。

参考文章: