前言:
博客好久没更新了,小伙伴们非常抱歉。最近本人呢,在研究音视频方面,所以打算写个专栏来记录一下。分享给大家的同时也是和大家共同进步。相信小伙伴们,都能看得出来,现在短视频行业是异常火爆,而且人才还非常稀缺,人的天性就是喜欢看动的东西,所以短视频火爆绝对不是偶然的。随着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文件
大概是以下步骤:
- 创建MediaExtractor设置数据源
- 遍历信道总数,得到音频或者视频的信号
- 切换到该信号
- 根据该信号拿到格式
- 创建MediaMuxer来追踪这个格式
- 启动MediaMuxer
- 拿到样本的时间绝对值
- 重新切换此信号
- 循环读取数据赋值到缓冲区
- MediaMuxer写入
- 关闭所有资源
合成MP4文件,步骤大概如下:因为有些步骤和上面一样,我在这里直接跳过,大家看代码也知道
- 分别拿到音频和视频的信号
- 分别切换到该信号
- 分别创建缓冲区数据类
- 创建MediaMuxer分别追踪这个格式
- 启动MediaMuxer
- 拿到样本(只需拿一次)
- 重新切换信道(上面操作资源)
- 分别循环写入数据
- 关闭资源
总体而言,Android中使用MediaExtractor和MediaMuxer对MP4文件进行分离与融合就这些了。当然有很多细节要靠大家自己多多练习才能体会,好了,大家加油!以上。