音视频开发系列(30)MediaExtractor MediaMuxer 实现视频的解封装与合成

本文主要是介绍音视频开发系列(30)MediaExtractor MediaMuxer 实现视频的解封装与合成,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、有什么实际应用

在我们日常使用短视频软件的时候,对视频的裁剪,拼凑,加入背景是很常用的操作,这些功能是如何实现的呐?其实是将视频多信道的分离出来,比如音轨和视频轨道分隔出来,可以做到二次合成。

今天我们通过对来MediaExtractor和MediaMuxer的学习分析和实践来实现 “把视频分离(提取&解封装)出纯音频和纯视频文件”、“替换背景音乐,合成新的视频文件”。

二、视频解封装和合成的API以及流程介绍

2.1 MediaExtractor:视频轨道提取器(解封装)

主要API介绍
setDataSource(path):path本地或者网络文件
getTrackCount:获取轨道数
getTrackFormat(i):对应轨道的格式 MediaFormat
selectTrack(I):切换到(选定)某个轨道
readSampleData(ByteBuffer byteBuff, int offset): 把指定轨道中的样本数据按偏移量读取到ByteBuffer字节缓冲区
advance(): 提取到下一帧数据 作用有点类似于cursor
unselectTrack(i)
release()
getSampleFlags: 获取数据的flag,数据为什么要用Sample来表示,因为音视频的数据是采样数据。
getSampleTime:返回当前的时间戳

数据提取(解封装)流程如下:

    //1. 构造MediaExtractorMediaExtractor mediaExtractor = new MediaExtractor();try {//2.设置数据源mediaExtractor.setDataSource(inputFile.getAbsolutePath());//3. 获取轨道数int trackCount = mediaExtractor.getTrackCount();Log.i(TAG, "demuxerMP4: trackCount=" + trackCount);//遍历轨道,查看音频轨或者视频轨道信息for (int i = 0; i < trackCount; i++) {//4. 获取某一轨道的媒体格式MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);String keyMime = trackFormat.getString(MediaFormat.KEY_MIME);Log.i(TAG, "demuxerMp4: keyMime=" + keyMime);if (TextUtils.isEmpty(keyMime)) {continue;}//5.通过mime信息识别音轨或视频轨道,打印相关信息if (keyMime.startsWith("video/")) {//打印视频的宽高Log.i(TAG, "extractorAndMuxerMP4:                     videoWidth="+trackFormat.getInteger(MediaFormat.KEY_WIDTH)+" videoHeight="+trackFormat.getInteger(MediaFormat.KEY_HEIGHT));} else if (keyMime.startsWith("audio/")) {//打印音轨的通道数以及比特率Log.i(TAG, "extractorAndMuxerMP4: channelCount="+trackFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)+" bitRate="+trackFormat.getInteger(MediaFormat.KEY_BIT_RATE));}}} catch (IOException e) {e.printStackTrace();} finally {mediaExtractor.release();}

2.2 MediaMuxer:合成(封装)

把音轨和视频轨道合成封装为新的视频
主要API介绍
MediaMuxer(path,format):path 输出文件的名称;foramt输出文件的格式,当前只支持mp4
addTrack(trackFormat):添加轨道,通常是使用MediaCodec.getOutputForma()或MediaExtractor.getTrackFormat(int index)来获取MediaFormat
start():开始封装合成
writeSampleData (int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo): 把数据写入到
stop()
release()

封装(合成)流程如下:

{MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);MediaMuxer mediaMuxer;mediaExtractor.selectTrack(i);//1. 构造MediaMuxermediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);//2. 添加轨道信息 参数为MediaFormatmediaMuxer.addTrack(trackFormat);//3. 开始合成mediaMuxer.start();//4. 设置bufferByteBuffer buffer = ByteBuffer.allocate(500 * 1024);MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();//5.通过mediaExtractor.readSampleData读取数据流int sampleSize = 0;while ((sampleSize = mediaExtractor.readSampleData(buffer, 0)) > 0) {bufferInfo.flags = mediaExtractor.getSampleFlags();bufferInfo.offset = 0;bufferInfo.size = sampleSize;bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();int isEOS = bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM;Log.i(TAG, "demuxerMp4:  flags=" + bufferInfo.flags + " size=" + sampleSize + " time=" + bufferInfo.presentationTimeUs + " outputName" + outputName+" isEOS="+isEOS);//6. 把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道mediaMuxer.writeSampleData(0, buffer, bufferInfo);mediaExtractor.advance();}Log.i(TAG, "extractorAndMuxer: " + outputName + "提取封装完成");mediaExtractor.unselectTrack(i);//6.关闭mediaMuxer.stop();mediaMuxer.release();
}

三、 实践(以及ffmpeg的实现)

1. 提取视频分离出纯音频和纯视频文件

private void extractorAndMuxerMP4() {tvOut.setText("");File inputFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "forme.mp4");if (!inputFile.exists()) {Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();return;}//数据提取(解封装)//1. 构造MediaExtractorMediaExtractor mediaExtractor = new MediaExtractor();try {//2.设置数据源mediaExtractor.setDataSource(inputFile.getAbsolutePath());//3. 获取轨道数int trackCount = mediaExtractor.getTrackCount();Log.i(TAG, "demuxerMP4: trackCount=" + trackCount);//遍历轨道,查看音频轨或者视频轨道信息for (int i = 0; i < trackCount; i++) {//4. 获取某一轨道的媒体格式MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);String keyMime = trackFormat.getString(MediaFormat.KEY_MIME);Log.i(TAG, "demuxerMp4: keyMime=" + keyMime);if (TextUtils.isEmpty(keyMime)) {continue;}//5.通过mime信息识别音轨或视频轨道,打印相关信息if (keyMime.startsWith("video/")) {File outputFile = extractorAndMuxer(mediaExtractor, i, "/video.mp4");tvOut.setText("纯视频文件路径:" + outputFile.getAbsolutePath());Log.i(TAG, "extractorAndMuxerMP4: videoWidth="+trackFormat.getInteger(MediaFormat.KEY_WIDTH)+" videoHeight="+trackFormat.getInteger(MediaFormat.KEY_HEIGHT));} else if (keyMime.startsWith("audio/")) {File outputFile = extractorAndMuxer(mediaExtractor, i, "/audio.aac");Log.i(TAG, "extractorAndMuxerMP4: channelCount="+trackFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)+" bitRate="+trackFormat.getInteger(MediaFormat.KEY_BIT_RATE));tvOut.setText(tvOut.getText().toString() + "\n纯音频路径:" + outputFile.getAbsolutePath());tvOut.setVisibility(View.VISIBLE);}}} catch (IOException e) {e.printStackTrace();} finally {mediaExtractor.release();}}private File extractorAndMuxer(MediaExtractor mediaExtractor, int i, String outputName) throws IOException {MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);MediaMuxer mediaMuxer;mediaExtractor.selectTrack(i);File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + outputName);if (outputFile.exists()) {outputFile.delete();}//1. 构造MediaMuxermediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);//2. 添加轨道信息 参数为MediaFormatmediaMuxer.addTrack(trackFormat);//3. 开始合成mediaMuxer.start();//4. 设置bufferByteBuffer buffer = ByteBuffer.allocate(500 * 1024);MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();//5.通过mediaExtractor.readSampleData读取数据流int sampleSize = 0;while ((sampleSize = mediaExtractor.readSampleData(buffer, 0)) > 0) {bufferInfo.flags = mediaExtractor.getSampleFlags();bufferInfo.offset = 0;bufferInfo.size = sampleSize;bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();int isEOS = bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM;Log.i(TAG, "demuxerMp4:  flags=" + bufferInfo.flags + " size=" + sampleSize + " time=" + bufferInfo.presentationTimeUs + " outputName" + outputName+" isEOS="+isEOS);//6. 把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道mediaMuxer.writeSampleData(0, buffer, bufferInfo);mediaExtractor.advance();}Log.i(TAG, "extractorAndMuxer: " + outputName + "提取封装完成");mediaExtractor.unselectTrack(i);//6.关闭mediaMuxer.stop();mediaMuxer.release();return outputFile;}

2. 把纯音频文件和纯视频文件(封装)合成为视频文件

/*** 把音轨和视频轨再合成新的视频*/private String muxerMp4(String inputAudio , String outPutVideo) {File videoFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "video.mp4");File audioFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), inputAudio);File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), outPutVideo);if (outputFile.exists()) {outputFile.delete();}if (!videoFile.exists()) {Toast.makeText(this, "视频源文件不存在", Toast.LENGTH_SHORT).show();return "";}if (!audioFile.exists()) {Toast.makeText(this, "音频源文件不存在", Toast.LENGTH_SHORT).show();return "";}MediaExtractor videoExtractor = new MediaExtractor();MediaExtractor audioExtractor = new MediaExtractor();try {MediaMuxer mediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);int videoTrackIndex = 0;int audioTrackIndex = 0;//先添加视频轨道videoExtractor.setDataSource(videoFile.getAbsolutePath());int trackCount = videoExtractor.getTrackCount();Log.i(TAG, "muxerToMp4: trackVideoCount=" + trackCount);for (int i = 0; i < trackCount; i++) {MediaFormat trackFormat = videoExtractor.getTrackFormat(i);String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);if (TextUtils.isEmpty(mimeType)) {continue;}if (mimeType.startsWith("video/")) {videoExtractor.selectTrack(i);videoTrackIndex = mediaMuxer.addTrack(trackFormat);Log.i(TAG, "muxerToMp4: videoTrackIndex=" + videoTrackIndex);break;}}//再添加音频轨道audioExtractor.setDataSource(audioFile.getAbsolutePath());int trackCountAduio = audioExtractor.getTrackCount();Log.i(TAG, "muxerToMp4: trackCountAduio=" + trackCountAduio);for (int i = 0; i < trackCountAduio; i++) {MediaFormat trackFormat = audioExtractor.getTrackFormat(i);String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);if (TextUtils.isEmpty(mimeType)) {continue;}if (mimeType.startsWith("audio/")) {audioExtractor.selectTrack(i);audioTrackIndex = mediaMuxer.addTrack(trackFormat);Log.i(TAG, "muxerToMp4: audioTrackIndex=" + audioTrackIndex);break;}}//再进行合成mediaMuxer.start();ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();int sampleSize = 0;while ((sampleSize = videoExtractor.readSampleData(byteBuffer, 0)) > 0) {bufferInfo.flags = videoExtractor.getSampleFlags();bufferInfo.offset = 0;bufferInfo.size = sampleSize;bufferInfo.presentationTimeUs = videoExtractor.getSampleTime();mediaMuxer.writeSampleData(videoTrackIndex, byteBuffer, bufferInfo);videoExtractor.advance();}int audioSampleSize = 0;MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();while ((audioSampleSize = audioExtractor.readSampleData(byteBuffer, 0)) > 0) {audioBufferInfo.flags = audioExtractor.getSampleFlags();audioBufferInfo.offset = 0;audioBufferInfo.size = audioSampleSize;audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime();mediaMuxer.writeSampleData(audioTrackIndex, byteBuffer, audioBufferInfo);audioExtractor.advance();}//最后释放资源videoExtractor.release();audioExtractor.release();mediaMuxer.stop();mediaMuxer.release();} catch (IOException e) {e.printStackTrace();return "";}return outputFile.getAbsolutePath();}

3. 替换背景音乐,合成新的视频文件

其实和第二步一样了,通过传入不同的aac音频源即可,这里需要注意一点,mediamuxer 只支持 aac 格式的,不支持mp3,否则会报如下异常,所以需要先把mp3转为aac。可以采用ffmpeg如下命令截取和转换

java.lang.IllegalStateException: Failed to add the track to the muxerat android.media.MediaMuxer.nativeAddTrack(Native Method)at android.media.MediaMuxer.addTrack(MediaMuxer.java:638)—> 添加音轨不是aac格式,而是mp3格式时,在medimuter.addTrack(audioFromat)时会报上述错误,解决方案:把mp3转成aacffmpeg -i 输入.mp3 -acodec aac 输出.aac -y

关注+后台私信我,领取2022最新最全学习提升资料+面试题,内容包括(C/C++,Linux,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)

 

 

四、遇到的问题

4.1 在合成写入数据时报 IllegalArgumentException: trackIndex is invalid

java.lang.IllegalArgumentException: trackIndex is invalidat android.media.MediaMuxer.writeSampleData(MediaMuxer.java:669)—>  mediaMuxer.writeSampleData(0,buffer,bufferInfo);
原因和方案:解封装时候输出的trackIndex不对导致,因为不管是纯音轨还是纯视频轨道文件只有一个轨道。

4.2 解码出来的纯视频文件的长度比原视频少了,而音频的长度一致。

和视频源有关系,有的原视频最后几秒只有音频播放画面不动,就是这种情况,刚开时不知道,还以为是什么bug,最后通过ffmpeg直接对原视频进行提取,得到的结果一样。用ffmpeg命令提取纯视频 对比看下
ffmpeg -i 输入.mp4 -vcodec copy -an 输出.mp4 -y 查看生成的视频也是一样。
说明这个视频中视频流就是比音频流要短。ffmpeg -i 输入.mp4 -acodec copy -vn 输出.aac -y 查看生成的音频流。和通过medieExtractor和mediamuxter提取的一致。

4.3 mediaExtractor.advance()时报IllegalArgumentException: bufferInfo must specify a valid buffer

通过查看bufferinfo的信息此时flags和presntationTimeUs都为-1,是advance调用时间不对引起java.lang.IllegalArgumentException: bufferInfo must specify a valid buffer offset, size and presentation timeat android.media.MediaMuxer.writeSampleData(MediaMuxer.java:682)解决方案:先调mediaMuxer.writeSampleData 后再mediaExtractor.advance();

4.4 合成时报如下错误,这个mediaMuxer.start之前没有添加轨道导致(流程不熟导致)

java.lang.IllegalStateException: Failed to start the muxerat android.media.MediaMuxer.nativeStart(Native Method)at android.media.MediaMuxer.start(MediaMuxer.java:452)start之前只是构造了mediaMuxer但没有mediaMuxer.addTrack(trackFormat);

4.5 在把音轨和视频轨道合成新视频时,复用了MediaTractor导致异常

java.io.IOException: Failed to instantiate extractor.
com.av.mediajourney W/System.err:     at android.media.MediaExtractor.nativeSetDataSource(Native Method)
com.av.mediajourney W/System.err:     at android.media.MediaExtractor.setDataSource(MediaExtractor.java:203)解决:在把视频轨道的源文件路径通过setDataSource设置到mediaextractor后,再把音轨的源文件setdataSource就报了这个错误
正确的做法是针对每一个源设置一个MediaExtractor,不同共用

4.6 把纯音轨和纯视频轨道合成新视频后,播放视频没有声音 时间是对的,但是没有声音

猜测 会不会是因为轨道0是视频,轨道1是音频的原因?用ffmpeg对比查看了下原视频和合成的视频这点有些差异,尝试调下顺序看下。
—>调整后没有效果。。。继续通过两证ffmpeg -i输出信息定位,发现metaChange不同
—>折腾了半天也没结果,最后通过ffplay来播放合成的视频,一切正常,只能说播放器的原因吧,也可能在合成时设置不全导致部分播放器无法播放,暂时不得结果

4.7 还出现一种情况是合成后的时长变成了音频加视频的时长总和了

原因是bufferInfo.presentationTimeUs的值不对导致。
异常实现如下:
`  ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);//先计算出视频帧间隔时间long smapleTime = 0;videoExtractor.readSampleData(byteBuffer, 0);if (videoExtractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC) {videoExtractor.advance();}videoExtractor.readSampleData(byteBuffer, 0);long secondTime = videoExtractor.getSampleTime();videoExtractor.advance();long thirdtime = videoExtractor.getSampleTime();smapleTime = Math.abs(thirdtime - secondTime);Log.i(TAG, "muxerStart: smapleTime=" + smapleTime);videoExtractor.unselectTrack(videoTrackIndex);videoExtractor.selectTrack(videoTrackIndex);MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();int sampleSize = 0;while ((sampleSize = videoExtractor.readSampleData(byteBuffer, 0)) > 0) {bufferInfo.flags = videoExtractor.getSampleFlags();bufferInfo.offset = 0;bufferInfo.size = sampleSize;bufferInfo.presentationTimeUs += smapleTime;//bufferInfo.presentationTimeUs = videoExtractor.getSampleTime();mediaMuxer.writeSampleData(videoTrackIndex, byteBuffer, bufferInfo);videoExtractor.advance();}int audioSampleSize = 0;ByteBuffer audioByteBuffer = ByteBuffer.allocate(500 * 1024);MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();while ((audioSampleSize = audioExtractor.readSampleData(byteBuffer, 0)) > 0) {audioBufferInfo.flags = audioExtractor.getSampleFlags();audioBufferInfo.offset = 0;audioBufferInfo.size = audioSampleSize;audioBufferInfo.presentationTimeUs += smapleTime;// audioBufferInfo.presentationTimeUs = videoExtractor.getSampleTime();mediaMuxer.writeSampleData(audioTrackIndex, audioByteBuffer, audioBufferInfo);audioExtractor.advance();}

这些遇到的问题一部分是对mediaExtractor和mediaMuxer的流程不熟悉导致。而有些需要借助ffpmpeg和ffplay进行协助分析排查。

收获

  1. 了解MediaExtractor和Mediamuxer的作用

  2. MediaExtractor和Mediamuxer熟悉API和使用流程

  3. 提取音轨和视频轨然后进行再合成或者替换音轨实现换背景音乐

  4. 遇到问题的分析解决以及复盘。

感谢你的阅读。

这篇关于音视频开发系列(30)MediaExtractor MediaMuxer 实现视频的解封装与合成的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/452353

相关文章

java实现docker镜像上传到harbor仓库的方式

《java实现docker镜像上传到harbor仓库的方式》:本文主要介绍java实现docker镜像上传到harbor仓库的方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 前 言2. 编写工具类2.1 引入依赖包2.2 使用当前服务器的docker环境推送镜像2.2

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Java easyExcel实现导入多sheet的Excel

《JavaeasyExcel实现导入多sheet的Excel》这篇文章主要为大家详细介绍了如何使用JavaeasyExcel实现导入多sheet的Excel,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录1.官网2.Excel样式3.代码1.官网easyExcel官网2.Excel样式3.代码

python实现对数据公钥加密与私钥解密

《python实现对数据公钥加密与私钥解密》这篇文章主要为大家详细介绍了如何使用python实现对数据公钥加密与私钥解密,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录公钥私钥的生成使用公钥加密使用私钥解密公钥私钥的生成这一部分,使用python生成公钥与私钥,然后保存在两个文

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

Golang如何用gorm实现分页的功能

《Golang如何用gorm实现分页的功能》:本文主要介绍Golang如何用gorm实现分页的功能方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录背景go库下载初始化数据【1】建表【2】插入数据【3】查看数据4、代码示例【1】gorm结构体定义【2】分页结构体

在Golang中实现定时任务的几种高效方法

《在Golang中实现定时任务的几种高效方法》本文将详细介绍在Golang中实现定时任务的几种高效方法,包括time包中的Ticker和Timer、第三方库cron的使用,以及基于channel和go... 目录背景介绍目的和范围预期读者文档结构概述术语表核心概念与联系故事引入核心概念解释核心概念之间的关系

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函

C++11作用域枚举(Scoped Enums)的实现示例

《C++11作用域枚举(ScopedEnums)的实现示例》枚举类型是一种非常实用的工具,C++11标准引入了作用域枚举,也称为强类型枚举,本文主要介绍了C++11作用域枚举(ScopedEnums... 目录一、引言二、传统枚举类型的局限性2.1 命名空间污染2.2 整型提升问题2.3 类型转换问题三、C