音视频 系列文章
Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
音视频工程
前几章,我们已经为音视频学习打下了一定的基础。
这一章,我们来学习如何使用 MediaExtractor 对视频流进行分离,比如视频轨,音频轨,并通过 MediaMuxer 把音频轨和视频轨重新合成新的视频。
通过这章,你将学习到:
MediaExtractor 便于从数据源中提取已解压的(通常是编码的) 媒体数据。比如一个MP4格式的视频,其实已经是编码过,多媒体能识别的数据。这样,我们就可以通过 MediaExtactor 对它进行解析和分离。
它的使用非常简单。
首先,初始化:
mMediaExtractor = new MediaExtractor();
接着,需要设置你需要解析的数据源,通过 setDataSource() 方法:
mMediaExtractor.setDataSource("/sdcard/test.mp4");
除了设置路径,还可以设置 AssetFileDescriptor,网络等
通过 getTrackCount() 就可以获取该视频包含多少个轨道,一般视频都有 视频轨和音频轨
int count = mMediaExtractor.getTrackCount();
当拿到轨道 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,如下:
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() 之前添加。
添加完通道之后,就可以使用 MediaMuxer.writeSampleData() 向视频(音频)文件写入数据了,这里需要用到 MediaCodec.BufferInfo 写入信息。
它有4个参数:
如果你使用 MediaExtractor 解析出的轨道,那么上面的写法,可以这样去写:
int videoSize = videoExtractor.readSampleData(buffer, 0);
info.offset = 0;
info.size = videoSize;
info.presentationTimeUs = videoExtractor.getSampleTime();
info.flags = videoExtractor.getSampleFlags();
在数据写入之后,需要使用 MediaMuxer.stop() 停止合成,并生成视频。
使用 MediaMuxer.release() 释放资源。
了解了上面的知识之后,我们可以这样实践,分离一个视频的视频轨和音视频,并合成新的视频。
首先,我们创建一个 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();
}
这里,我们也新建一个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