Android中使用MediaExtractor和MediaMuxer对MP4文件进行分离与融合

前言:

博客好久没更新了,小伙伴们非常抱歉。最近本人呢,在研究音视频方面,所以打算写个专栏来记录一下。分享给大家的同时也是和大家共同进步。相信小伙伴们,都能看得出来,现在短视频行业是异常火爆,而且人才还非常稀缺,人的天性就是喜欢看动的东西,所以短视频火爆绝对不是偶然的。随着5G时代的到来,这一点将会更加明显,最重要的一点是我认为做为一个Android开发人员,如果一直只是从事应用开发的话,是走不了多远的。我们必须结合一个应用方向,努力前进才是更好的选择。就比如音视频领域。好了,接下来进入今天的主题。我们将要对一个MP4文件进行分离与融合操作。

一,将MP4文件分离出视频和音频文件

MediaExtractor:MediaExtractor有助于提取解复用的,通常编码的媒体数据(来自数据源).

上面是MediaExtractor官方翻译解释,接下来直接上代码,值得注意的是我们先要在sdcard根目录下放一个MP4文件。

    //分隔视频(将MP4文件分隔出audio信道和video信道)
    public void exactorMedia(View view) {

        FileOutputStream videoOutputStream = null;
        FileOutputStream audioOutputStream = null;
        try{
           //分离的视频文件
            File videoFile = new File(SDCARD_PATH, "output_video.mp4");
            //分离的音频文件
            File audioFile = new File(SDCARD_PATH, "output_audio");
            videoOutputStream = new FileOutputStream(videoFile);
            audioOutputStream = new FileOutputStream(audioFile);
            //源文件
            mediaExtractor.setDataSource(SDCARD_PATH+"/input.mp4");
            //信道总数
            int trackCount = mediaExtractor.getTrackCount();
            int audioTrackIndex = -1;
            int videoTrackIndex = -1;

            for (int i = 0; i < trackCount; i++) {
                MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
                String mineType = trackFormat.getString(MediaFormat.KEY_MIME);
                //视频信道
                if(mineType.startsWith("video/")){
                    videoTrackIndex = i;
                }
                //音频信道
                if(mineType.startsWith("audio/")){
                    audioTrackIndex = i;
                }
            }

            ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);
            //切换到视频信道
            mediaExtractor.selectTrack(videoTrackIndex);
            while (true){
                int readSampleCount = mediaExtractor.readSampleData(byteBuffer, 0);
                if(readSampleCount < 0){
                    break;
                }
                //保存视频信道信息
                byte[] buffer = new byte[readSampleCount];
                byteBuffer.get(buffer);
                videoOutputStream.write(buffer);
                byteBuffer.clear();
                mediaExtractor.advance();
            }
            //切换到音频信道
            mediaExtractor.selectTrack(audioTrackIndex);
            while (true){
                int readSampleCount = mediaExtractor.readSampleData(byteBuffer, 0);
                if(readSampleCount < 0){
                    break;
                }
                //保存音频信道信息
                byte[] buffer = new byte[readSampleCount];
                byteBuffer.get(buffer);
                audioOutputStream.write(buffer);
                byteBuffer.clear();
                mediaExtractor.advance();
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            mediaExtractor.release();
            try {
                videoOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

上面分离出的音频与视频文件还没法进行播放,你暂且可以理解为没有进行编码格式化。因为我们还没使用MediaMuxer

二,将MP4文件抽离出纯视频文件

MediaMuxer:MediaMuxer有助于混合基本流。 目前,MediaMuxer支持MP4,Webm和3GP文件作为输出。 自从Android Nougat开始,它还支持在MP4中混合B帧。

这是MediaMuxer的官方源码解释,那里面还有它的使用示例,不多说,还是直接上代码吧。

    //分离MP4的纯视频
    public void muxerMedia(View view) {
        mediaExtractor = new MediaExtractor();
        int videoIndex = -1;
        try {
            //源文件
            mediaExtractor.setDataSource(SDCARD_PATH+"/input.mp4");
            //信道总数
            int trackCount = mediaExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) {
                //获取指定索引处的轨道格式。
                MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
                String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
                //取出视频的信号
                if(mimeType.startsWith("video/")){
                    videoIndex = i;
                }
            }
            //切换到视频信号的信道
            mediaExtractor.selectTrack(videoIndex);
            MediaFormat videoFormat = mediaExtractor.getTrackFormat(videoIndex);
            mediaMuxer = new MediaMuxer(SDCARD_PATH + "/output_video.mp4",MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            //追踪此信道
            int trackIndex = mediaMuxer.addTrack(videoFormat);
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 500);
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            mediaMuxer.start();
            //获取每帧之间的时间
            {
                mediaExtractor.readSampleData(byteBuffer,0);
                //跳过第一帧
                if(mediaExtractor.getSampleFlags()==MediaExtractor.SAMPLE_FLAG_SYNC){
                    mediaExtractor.advance();
                }
                mediaExtractor.readSampleData(byteBuffer,0);
                long firstVideoPTS = mediaExtractor.getSampleTime();
                mediaExtractor.advance();
                mediaExtractor.readSampleData(byteBuffer,0);
                long secondVideoPTS = mediaExtractor.getSampleTime();
                videoSampleTime = Math.abs(secondVideoPTS - firstVideoPTS);
                Log.d("yx", "videoSampleTime: "+videoSampleTime);
            }
            //重新切换此信道,不然上面跳过了3帧,造成前面的帧数模糊
            mediaExtractor.unselectTrack(videoIndex);
            mediaExtractor.selectTrack(videoIndex);
            while (true){
                //读取帧之间的数据
                int readSampleSize = mediaExtractor.readSampleData(byteBuffer, 0);
                if(readSampleSize < 0){
                    break;
                }
                mediaExtractor.advance();
                bufferInfo.size = readSampleSize;
                bufferInfo.offset = 0;
                bufferInfo.flags = mediaExtractor.getSampleFlags();
                bufferInfo.presentationTimeUs += videoSampleTime;
                //写入帧的数据
                mediaMuxer.writeSampleData(trackIndex,byteBuffer,bufferInfo);
            }
            //release
            mediaMuxer.stop();
            mediaExtractor.release();
            mediaMuxer.release();

            Log.e("yx", "finish");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

这里分离出的纯视频文件就可以进行播放了。

三,将MP4文件抽离出纯音频文件

其实做法和抽取视频文件一样只不过就换成音频信道就行,接下来的代码我将会注释的更详细,尽量让大家都能明白。

   //分离MP4的纯音频
   public void muxerAudio(View view) {
        mediaExtractor = new MediaExtractor();
        int audioIndex = -1;
        try{
            //设置源文件
            mediaExtractor.setDataSource(SDCARD_PATH+"/input.mp4");
            //获取信道总数
            int trackCount = mediaExtractor.getTrackCount();
            //遍历信道总数,获取音频信号
            for (int i = 0; i < trackCount; i++) {
                MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
                String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
                if(mimeType.startsWith("audio/")){
                    //拿到音频位置
                    audioIndex = i;
                }
            }
            //根据音频位置切换到音频信道
            mediaExtractor.selectTrack(audioIndex);
            //根据音频位置拿到音频格式
            MediaFormat audioFormat = mediaExtractor.getTrackFormat(audioIndex);
            //创建mediamuxer(用来混合基本流)
            mediaMuxer = new MediaMuxer(SDCARD_PATH+"/output_audios", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            //追踪这个音频格式
            int track = mediaMuxer.addTrack(audioFormat);
            //创建一个字节缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 500);
            //缓冲区的数据
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            //启动多路复用器
            mediaMuxer.start();
            //获取每帧之间的时间
            {
                //检索当前编码的样本并将其存储在字节缓冲区中,*从给定的偏移量开始。    
                mediaExtractor.readSampleData(byteBuffer,0);
                //如果样本是同步关键帧,那么前进到下一个样本,也就是跳过
                if(mediaExtractor.getSampleFlags()==MediaExtractor.SAMPLE_FLAG_SYNC){
                    mediaExtractor.advance();
                }
                mediaExtractor.readSampleData(byteBuffer,0);
                //拿到第二个样本的显示时间
                long secondAudioPTS = mediaExtractor.getSampleTime();
                //前进到下一个样本
                mediaExtractor.advance();
                mediaExtractor.readSampleData(byteBuffer,0);
                //拿到第三个样本的显示时间
                long thirdAudioPTS = mediaExtractor.getSampleTime();
                //拿到这段音频样本时间绝对值
                audioSampleTime = Math.abs(thirdAudioPTS - secondAudioPTS);
                Log.i("yx", "audioSampleTime: "+audioSampleTime);
            }
            //重新切换此信道,因为上面跳过了三帧,为了避免前面的帧数模糊
            mediaExtractor.unselectTrack(audioIndex);
            mediaExtractor.selectTrack(audioIndex);

            while (true){
                //读取帧之间的数据
                int readSampleData = mediaExtractor.readSampleData(byteBuffer, 0);
                if(readSampleData < 0){
                    break;
                }
                mediaExtractor.advance();
                //数据量
                bufferInfo.size = readSampleData;
                //缓冲区数据起始偏移量
                bufferInfo.offset = 0;
                //与缓冲区关联的缓冲区标志
                bufferInfo.flags = mediaExtractor.getSampleFlags();
                //缓冲区的显示时间戳
                bufferInfo.presentationTimeUs += audioSampleTime;
                //写入帧的数据
                mediaMuxer.writeSampleData(track,byteBuffer,bufferInfo);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            Toast.makeText(this, "分离音频完成", Toast.LENGTH_SHORT).show();
            mediaMuxer.stop();
            mediaExtractor.release();
            mediaMuxer.release();
        }
    }

四,将分离的音频与视频文件融合成原来的MP4文件

其实代码和前面也有相似的地方。

    //将以上分离的视频和音频融合成原来完整的MP4文件
    public void combineVideo(View view) {
        try{
            //创建MediaExtractor(MediaExtractor有助于提取解复用的,通常编码的媒体数据)
            MediaExtractor videoExtractor = new MediaExtractor();
            //设置视频数据源
            videoExtractor.setDataSource(SDCARD_PATH + "/output_video.mp4");
            MediaFormat videoFormat = null;
            int videoTrackIndex = -1;
            int videoTrackCount = videoExtractor.getTrackCount();
            for (int i = 0; i < videoTrackCount; i++) {
                videoFormat = videoExtractor.getTrackFormat(i);
                String mimeType = videoFormat.getString(MediaFormat.KEY_MIME);
                if(mimeType.startsWith("video/")){
                    videoTrackIndex = i;
                    break;
                }
            }


            MediaExtractor audioExtractor = new MediaExtractor();
            audioExtractor.setDataSource(SDCARD_PATH + "/output_audios");
            MediaFormat audioFormat = null;
            int audioTrackIndex = -1;
            int audioTrackCount = audioExtractor.getTrackCount();
            for (int i = 0; i < audioTrackCount; i++) {
                audioFormat = audioExtractor.getTrackFormat(i);
                String mimeType = audioFormat.getString(MediaFormat.KEY_MIME);
                if(mimeType.startsWith("audio/")){
                    audioTrackIndex = i;
                    break;
                }
            }

            videoExtractor.selectTrack(videoTrackIndex);
            audioExtractor.selectTrack(audioTrackIndex);

            MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();
            MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();

            MediaMuxer mediaMuxer = new MediaMuxer(SDCARD_PATH + "/new_input.mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            int writeVideoTrackIndex = mediaMuxer.addTrack(videoFormat);
            int writeAudioTrackIndex = mediaMuxer.addTrack(audioFormat);
            mediaMuxer.start();

            ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);
            long sampleTime = 0;
            {
                videoExtractor.readSampleData(byteBuffer,0);
                if(videoExtractor.getSampleFlags()==MediaExtractor.SAMPLE_FLAG_SYNC){
                    videoExtractor.advance();
                }
                videoExtractor.readSampleData(byteBuffer,0);
                long secondSampleTime = videoExtractor.getSampleTime();
                videoExtractor.advance();
                videoExtractor.readSampleData(byteBuffer,0);
                long thirdSampleTime = videoExtractor.getSampleTime();
                sampleTime = Math.abs(thirdSampleTime-secondSampleTime);

            }
            videoExtractor.unselectTrack(videoTrackIndex);
            videoExtractor.selectTrack(videoTrackIndex);

            while (true){
                int data = videoExtractor.readSampleData(byteBuffer, 0);
                if(data < 0){
                    break;
                }
                videoBufferInfo.size = data;
                videoBufferInfo.flags = videoExtractor.getSampleFlags();
                videoBufferInfo.presentationTimeUs += sampleTime;
                mediaMuxer.writeSampleData(writeVideoTrackIndex,byteBuffer,videoBufferInfo);
                videoExtractor.advance();
            }

            while (true){
                int data = audioExtractor.readSampleData(byteBuffer, 0);
                if(data < 0){
                    break;
                }
                audioBufferInfo.size = data;
                audioBufferInfo.flags = videoExtractor.getSampleFlags();
                audioBufferInfo.presentationTimeUs += sampleTime;
                audioBufferInfo.offset = 0;
                mediaMuxer.writeSampleData(writeAudioTrackIndex,byteBuffer,audioBufferInfo);
                audioExtractor.advance();
            }

            mediaMuxer.stop();
            mediaMuxer.release();
            videoExtractor.release();
            audioExtractor.release();

            Toast.makeText(this, "合成完毕", Toast.LENGTH_SHORT).show();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

 总结:

其实大家仔细看看这四段代码会发现它们的流程都是大同小异的,下面我用自己的话给大家稍微总结一下,首先分离MP4文件

大概是以下步骤:

  1. 创建MediaExtractor设置数据源
  2. 遍历信道总数,得到音频或者视频的信号
  3. 切换到该信号
  4. 根据该信号拿到格式
  5. 创建MediaMuxer来追踪这个格式
  6. 启动MediaMuxer
  7. 拿到样本的时间绝对值
  8. 重新切换此信号
  9. 循环读取数据赋值到缓冲区
  10. MediaMuxer写入
  11. 关闭所有资源

合成MP4文件,步骤大概如下:因为有些步骤和上面一样,我在这里直接跳过,大家看代码也知道

  1. 分别拿到音频和视频的信号
  2. 分别切换到该信号
  3. 分别创建缓冲区数据类
  4. 创建MediaMuxer分别追踪这个格式
  5. 启动MediaMuxer
  6. 拿到样本(只需拿一次)
  7. 重新切换信道(上面操作资源)
  8. 分别循环写入数据
  9. 关闭资源

 总体而言,Android中使用MediaExtractor和MediaMuxer对MP4文件进行分离与融合就这些了。当然有很多细节要靠大家自己多多练习才能体会,好了,大家加油!以上。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值