音视频 系列文章
Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
音视频工程

前几章,我们已经为音视频学习打下了一定的基础。
这一章,我们来学习如何使用 MediaExtractor 对视频流进行分离,比如视频轨,音频轨,并通过 MediaMuxer 把音频轨和视频轨重新合成新的视频。

通过这章,你将学习到:

  1. MediaExtractor 的基础使用,并分离视频轨和音频轨
  2. MediaMuxer 的基础使用,并合成新视频

由于合成时间比较久,这里用一张静图来演示 :
在这里插入图片描述

一. MediaExtactor

MediaExtractor 便于从数据源中提取已解压的(通常是编码的) 媒体数据。比如一个MP4格式的视频,其实已经是编码过,多媒体能识别的数据。这样,我们就可以通过 MediaExtactor 对它进行解析和分离。
它的使用非常简单。
首先,初始化:

mMediaExtractor = new MediaExtractor();

1.1 设置数据源

接着,需要设置你需要解析的数据源,通过 setDataSource() 方法:

mMediaExtractor.setDataSource("/sdcard/test.mp4");

除了设置路径,还可以设置 AssetFileDescriptor,网络等

1.2 getTrackCount()

通过 getTrackCount() 就可以获取该视频包含多少个轨道,一般视频都有 视频轨和音频轨

 int count = mMediaExtractor.getTrackCount();

1.3 MediaFormat

当拿到轨道 index 之后,我们就可以通过 getTrackFormat(index) 拿到 MediaFormat 了,MediaFormat即媒体格式类,用于描述媒体的格式参数,如视频帧率、音频采样率等,可以通过 getxxx()方法获取轨道的相关信息,比如:

int count = mMediaExtractor.getTrackCount();
for (int i = 0; i < count; i++) {
    MediaFormat format = mMediaExtractor.getTrackFormat(i);
    //获取 mime 类型
    String mime = format.getString(MediaFormat.KEY_MIME);
    // 视频轨
    if (mime.startsWith("video")) {
        mVideoTrackId = i;
        mVideoFormat = format;
    } else if (mime.startsWith("audio")) {
    	//音频轨
        mAudioTrackId = i;
        mAudioFormat = format;
    }
}

上面说到 MediaFormat 可以获取不同轨道包含的信息,比如,我们想要知道视频的大小,就可以使用:

int width = mVideoFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = mVideoFormat.getInteger(MediaFormat.KEY_HEIGHT);

播放时长:

 long time = mVideoFormat.getLong(MediaFormat.KEY_DURATION);

等等

其他 API,如下:

  • getSampleTime():返回当前的时间戳
  • readSampleData(ByteBuffer byteBuf, int offset):把指定轨道中的数据,按照偏移量读取到 ByteBuffer 中,后面读取视频数据就需要用到它
  • advance():读取下一帧数据
  • release(): 读取结束后释放资源

二. MediaMuxer

MediaExtactor 是分离视频信息,二 MediaMuxer 则是合成(生成) 音频或视频文件,也可以把音频和视频合成一个新的音视频文件,目前 MediaMuxer 支持MP4、Webm和3GP文件作为输出。

创建该类对象,需要传入输出的文件位置以及格式,构造函数如下:

public MediaMuxer(String path, int format);

接着,则需要做一个比较重要的参数,就是添加通道, addTrack(),它函数需要传入一个 MediaFormat 对象,我们可以使用上面 MediaExtractor.getTrackFormat(index) 拿到视频轨或者音频轨。

当然,你也可以自己创建 MediaFormat,使用它的静态方法:

MediaFormat format = MediaFormat.createVideoFormat("video/avc",320,240);

但需要注意的是,一定要记得设置"csd-0"和"csd-1"这两个参数:

byte[] csd0 = {x,x,x,x,x,x,x...}
byte[] csd1 = {x,x,x,x,x,x,x...}

format.setByteBuffer("csd-0",ByteBuffer.wrap(csd0));
format.setByteBuffer("csd-1",ByteBuffer.wrap(csd1));

那什么是 "csd-0"和"csd-1"是什么,对于H264视频的话,它对应的是sps和pps,对于AAC音频的话,对应的是ADTS。

addTrack() 之后,需要使用 MediaMuxer.start() 方法,开始合成,等待数据的到来,注意 addTrack() 只能在 start() 之前添加。

2.1 writeSampleData()

添加完通道之后,就可以使用 MediaMuxer.writeSampleData() 向视频(音频)文件写入数据了,这里需要用到 MediaCodec.BufferInfo 写入信息。
它有4个参数:

  • offset : 数据开始的位置
  • size :需要写入数据的大小
  • presentationTimeUs:缓冲区的时间戳(微妙)
  • flags:缓冲区的标志位

如果你使用 MediaExtractor 解析出的轨道,那么上面的写法,可以这样去写:

int videoSize = videoExtractor.readSampleData(buffer, 0);
info.offset = 0;
info.size = videoSize;
info.presentationTimeUs = videoExtractor.getSampleTime();
info.flags = videoExtractor.getSampleFlags();

2.2 停止合成

在数据写入之后,需要使用 MediaMuxer.stop() 停止合成,并生成视频。
使用 MediaMuxer.release() 释放资源。

三. 解析视频并生成新的视频

了解了上面的知识之后,我们可以这样实践,分离一个视频的视频轨和音视频,并合成新的视频

3.1 解析视频

首先,我们创建一个 MyExtractor ,创建 MediaExtractor 实例,用来拿到不同的视频轨和音频轨,并拿到对应的 MediaFormat:

    class MyExtractor {
        MediaExtractor mediaExtractor;
        int videoTrackId;
        int audioTrackId;
        MediaFormat videoFormat;
        MediaFormat audioFormat;
        long curSampleTime;
        int curSampleFlags;

        public MyExtractor() {
            try {
                mediaExtractor = new MediaExtractor();
                // 设置数据源
                mediaExtractor.setDataSource(Constants.VIDEO_PATH);
            } catch (IOException e) {
                e.printStackTrace();
            }
            //拿到所有的轨道
            int count = mediaExtractor.getTrackCount();
            for (int i = 0; i < count; i++) {
                //根据下标拿到 MediaFormat
                MediaFormat format = mMediaExtractor.getTrackFormat(i);
                //拿到 mime 类型
                String mime = format.getString(MediaFormat.KEY_MIME);
                //拿到视频轨
                if (mime.startsWith("video")) {
                    videoTrackId = i;
                    videoFormat = format;
                } else if (mime.startsWith("audio")) {
                    //拿到音频轨
                    audioTrackId = i;
                    audioFormat = format;
                }

            }
        }
}

接着,我们需要用到 selectTrack() 先选择要解析的轨道,然后通过 mediaExtractor.readSampleData() 去读取该轨道的帧数据,并记录当前帧的时间戳和标志位,如下:


 /**
   * 读取一帧的数据
   * @param buffer
   * @return
   *  #MyExtractor#readBuffer
   */
  int readBuffer(ByteBuffer buffer, boolean video) {
      //先清空数据
      buffer.clear();
      //选择要解析的轨道
      mediaExtractor.selectTrack(video ? videoTrackId : audioTrackId);
      //读取当前帧的数据
      int buffercount = mediaExtractor.readSampleData(buffer, 0);
      if (buffercount < 0) {
          return -1;
      }
      //记录当前时间戳
      curSampleTime = mediaExtractor.getSampleTime();
      //记录当前帧的标志位
      curSampleFlags = mediaExtractor.getSampleFlags();
      //进入下一帧
      mediaExtractor.advance();
      return buffercount;
  }

还需要注意的是,我们需要使用 mediaExtractor.advance() 为下一帧做准备,其他方法如下;

        /**
         * 获取音频 MediaFormat
         * @return
         */
        public MediaFormat getAudioFormat() {
            return audioFormat;
        }

        /**
         * 获取视频 MediaFormat
         * @return
         */
        public MediaFormat getVideoFormat() {
            return videoFormat;
        }

        /**
         * 获取当前帧的标志位
         * @return
         */
        public int getCurSampleFlags() {
            return curSampleFlags;
        }
        /**
         * 获取当前帧的时间戳
         * @return
         */
        public long getCurSampleTime() {
            return curSampleTime;
        }

        /**
         * 释放资源
         */
        public void release() {
            mediaExtractor.release();
        }

3.2 合成新视频

这里,我们也新建一个MyMuxer,在它的构造方法中,去生成 MediaMuxer 实例,并通过 addTrack() 添加视频轨和音频轨。
如下:

    class MyMuxer {
        //创建音频的 MediaExtractor
        MyExtractor audioExtractor = new MyExtractor();
        //创建视频的 MediaExtractor
        MyExtractor videoExtractor = new MyExtractor();
        MediaMuxer mediaMuxer;
        private int audioId;
        private int videoId;
        private MediaFormat audioFormat;
        private MediaFormat videoFormat;
        private MuxerListener listener;
        //新的视频名
        String name = "mixvideo.mp4";
        public MyMuxer(MuxerListener listener) {
            this.listener = listener;
            File dir = new File(Constants.PATH);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            File file = new File(Constants.PATH,name);
            //已存在就先删掉
            if (file.exists()) {
                file.delete();
            }

            try {
                //拿到音频的 mediaformat
                audioFormat = audioExtractor.getAudioFormat();
                //拿到音频的 mediaformat
                videoFormat = videoExtractor.getVideoFormat();
                mediaMuxer = new MediaMuxer(file.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

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

这里指定 MediaMuxer 的 format 为 MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,生成一个MP4格式的视频。

接着,合成的时间肯定是好使的,所以我们用线程来实现该逻辑;

        public void start() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        listener.onstart();
                        //添加音频
                        audioId = mediaMuxer.addTrack(audioFormat);
                        //添加视频
                        videoId = mediaMuxer.addTrack(videoFormat);
                        //开始混合,等待写入
                        mediaMuxer.start();

                        ByteBuffer buffer = ByteBuffer.allocate(500 * 1024);
                        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                        
                        //混合视频
                        int videoSize;
                        //读取视频帧的数据,直到结束
                        while ((videoSize = videoExtractor.readBuffer(buffer, true)) > 0) {
                            info.offset = 0;
                            info.size = videoSize;
                            info.presentationTimeUs = videoExtractor.getCurSampleTime();
                            info.flags = videoExtractor.getCurSampleFlags();
                            mediaMuxer.writeSampleData(videoId, buffer, info);
                        }
                        //写完视频,再把音频混合进去
                        int audioSize;
                        //读取音频帧的数据,直到结束
                        while ((audioSize = audioExtractor.readBuffer(buffer, false)) > 0) {
                            info.offset = 0;
                            info.size = audioSize;
                            info.presentationTimeUs = audioExtractor.getCurSampleTime();
                            info.flags = audioExtractor.getCurSampleFlags();
                            mediaMuxer.writeSampleData(audioId, buffer, info);
                        }
                        //释放资源
                        audioExtractor.release();
                        videoExtractor.release();
                        mediaMuxer.stop();
                        mediaMuxer.release();
                        listener.onSuccess(Constants.PATH+File.separator+name);
                    } catch (Exception e) {
                        e.printStackTrace();
                        listener.onFail(e.getMessage());
                    }


                }
            }).start();

代码都比较好懂,接着直接调用即可:

new MyMuxer(new MuxerListener()).start();

参考:
https://developer.android.google.cn/reference/android/media/MediaExtractor?hl=en
https://developer.android.google.cn/reference/android/media/MediaMuxer?hl=en
https://blog.51cto.com/ticktick/1710743
https://www.jianshu.com/p/105147d75dfa

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐