Android(安卓)音视频开发(五) -- 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
音视频 系列文章
Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
音视频工程
前几章,我们已经为音视频学习打下了一定的基础。
这一章,我们来学习如何使用 MediaExtractor 对视频流进行分离,比如视频轨,音频轨,并通过 MediaMuxer 把音频轨和视频轨重新合成新的视频。
通过这章,你将学习到:
- MediaExtractor 的基础使用,并分离视频轨和音频轨
- 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
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- 一句话锁定MySQL数据占用元凶
- Android(安卓)ionic工程中调用webrtc获取视频流
- MVVM实现数据双向绑定
- Android中保存和恢复Fragment状态的最好方法
- java读取文本文件内容2
- Android(安卓)Volley 完全解析(三),定制自己的Request
- 视频教程-MongoDB数据库从入门到精通-Mongo DB
- android中-----JSON数据解析