音视频 系列文章
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,做音视频开发的人应该都知道,它一般存在于编码器生成的IDR帧之中。

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

更多相关文章

  1. SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
  2. 一句话锁定MySQL数据占用元凶
  3. Android(安卓)ionic工程中调用webrtc获取视频流
  4. MVVM实现数据双向绑定
  5. Android中保存和恢复Fragment状态的最好方法
  6. java读取文本文件内容2
  7. Android(安卓)Volley 完全解析(三),定制自己的Request
  8. 视频教程-MongoDB数据库从入门到精通-Mongo DB
  9. android中-----JSON数据解析

随机推荐

  1. android中textview的文字处理--同一段文
  2. 《Android(安卓)Framework 之路》Android
  3. android property_get 与 property_set
  4. android响应事件(按钮)的三种方式
  5. Android通过Termux安装scrapy遇到的问题
  6. Dashboards (Android)
  7. android Error:Execution failed for tas
  8. Android(安卓)由图片资源ID获取图片的文
  9. android 签名和adb命令(时刻更新)
  10. android 手机虚拟按键 震动过程的追溯(1)