参考文章:http://blog.51cto.com/ticktick/1710743
1.MP4基本概念
MP4文件中的所有数据都装在box(QuickTime中为atom)中,也就是说MP4文件由若干个box组成,每个box有类型和长度,可以将box理解为一个数据对象块。box中可以包含另一个box,这种box称为container box。一个MP4文件首先会有且只有一个“ftyp”类型的box,作为MP4格式的标志并包含关于文件的一些信息;之后会有且只有一个“moov”类型的box(Movie Box),它是一种container box,子box包含了媒体的metadata信息;MP4文件的媒体数据包含在“mdat”类型的box(Midia
Data Box)中,该类型的box也是container box,可以有多个,也可以没有(当媒体数据全部引用其他文件时),媒体数据的结构由metadata进行描述。
下面是一些概念:
track 表示一些sample的集合,对于媒体数据来说,track表示一个视频或音频序列。
hint track 这个特殊的track并不包含媒体数据,而是包含了一些将其他数据track打包成流媒体的指示信息。
sample 对于非hint track来说,video sample即为一帧视频,或一组连续视频帧,audio sample即为一段连续的压缩音频,它们统称sample。对于hint track,sample定义一个或多个流媒体包的格式。
sample table 指明sampe时序和物理布局的表。
chunk 一个track的几个sample组成的单元。
2. h264
h264帧的类型主要有
typedef enum {
NALU_TYPE_SLICE = 1,
NALU_TYPE_DPA = 2,
NALU_TYPE_DPB = 3,
NALU_TYPE_DPC = 4,
NALU_TYPE_IDR = 5,
NALU_TYPE_SEI = 6,
NALU_TYPE_SPS = 7,
NALU_TYPE_PPS = 8,
NALU_TYPE_AUD = 9,
NALU_TYPE_EOSEQ = 10,
NALU_TYPE_EOSTREAM = 11,
NALU_TYPE_FILL = 12,
#if (MVC_EXTENSION_ENABLE)
NALU_TYPE_PREFIX = 14,
NALU_TYPE_SUB_SPS = 15,
NALU_TYPE_SLC_EXT = 20,
NALU_TYPE_VDRD = 24 // View and Dependency Representation Delimiter NAL Unit
#endif
} NaluType;
在Android中能够涉及到的就SPS、PPS和IDR三种类型的帧数据,其中
SPS(序列参数集Sequence Parameter Set)是H.264码流第一个 NALU
PPS(图像参数集Picture Parameter Set)是H.264码流第二个 NALU是
IDR(即时解码器刷新)是H.264码流第三个 NALU
3.Android音视频处理
MediaMuxer类主要用于将音频和视频数据进行混合生成多媒体文件(如:mp4文件),而MediaExtractor则刚好相反,主要用于多媒体文件的音视频数据的分离。
Demo下载:https://github.com/haibaobao9/MediaDemo
该Demo主要功能是通过MediaExtractor将MP4文件分离成音频和视频然后使用MediaMuxer将视频数据合成单独的MP4,使用是需要将project中input.mp4文件放入SD卡根目录。
1)MediaExtractor
该类主要用于音视频混合数据的分离,接口比较简单,首先要通过setDataSource(String path)函数设置数据源,数据源可以是本地文件地址,也可以使用HTTP协议的网络码流地址。
然后,可以通过下面的代码块,来获取码流的详细信息,如:MimeType,分辨率、编码格式、码率、帧率等等。
int videoTrackIndex = -1;
int audioTrackIndex = -1;
for(int i = 0; i < mMediaExtractor.getTrackCount(); i++) {
//获取码流的详细格式/配置信息
MediaFormat format = mMediaExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if(mime.startsWith("video/")) {
videoTrackIndex = i;
}
else if(mime.startsWith("audio/")) {
audioTrackIndex = i;
}
....
}
获取到媒体文件的详细信息之后,就可以选择指定的通道,并分离和读取数据了:
mMediaExtractor.selectTrack(videoTrackIndex); //选择读取视频数据
while(true) {
int sampleSize = mMediaExtractor.readSampleData(buffer, 0); //读取一帧数据
if(sampleSize < 0) {
break;
}
mMediaExtractor.advance(); //移动到下一帧
}
mMediaExtractor.release(); //读取结束后,要记得释放资源
2)MediaMuxer
该类主要用于将音频和视频进行混合生成多媒体文件,创建该类对象,需要传入输出的文件位置以及格式,构造函数如下:
public MediaMuxer(String path, int format);
创建对象之后,一个比较重要的操作就是addTrack(),添加数据通道,该函数需要传入MediaFormat对象,MediaFormat即媒体格式类,用于描述媒体的格式参数,如视频帧率、音频采样率等。
核心代码:
protected boolean process() throws IOException {
mMediaExtractor = new MediaExtractor();
mMediaExtractor.setDataSource(SDCARD_PATH+"/input.mp4");
int mVideoTrackIndex = -1;
int framerate = 0;
for(int i = 0; i < mMediaExtractor.getTrackCount(); i++) {
MediaFormat format = mMediaExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if(!mime.startsWith("video/")) {
continue;
}
framerate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
mMediaExtractor.selectTrack(i);
mMediaMuxer = new MediaMuxer(SDCARD_PATH+"/ouput.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
mVideoTrackIndex = mMediaMuxer.addTrack(format);
mMediaMuxer.start();
}
if(mMediaMuxer == null) {
return false;
}
BufferInfo info = new BufferInfo();
info.presentationTimeUs = 0;
ByteBuffer buffer = ByteBuffer.allocate(500*1024);
while(true) {
int sampleSize = mMediaExtractor.readSampleData(buffer, 0);
if(sampleSize < 0) {
break;
}
mMediaExtractor.advance();
info.offset = 0;
info.size = sampleSize;
info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
info.presentationTimeUs += 1000*1000/framerate;
mMediaMuxer.writeSampleData(mVideoTrackIndex,buffer,info);
}
mMediaExtractor.release();
mMediaMuxer.stop();
mMediaMuxer.release();
return true;
}
但是实际开发过程中绝大多数情况下都是直接通过h264的frame数据合成MP4,这就要求我们不能通过MediaExtractor.getTrackFormat()解析得到的MediaFormat对象,而是只能手动创建MediaFormat对象。
MediaFormat format = MediaFormat.createVideoFormat("video/avc",320,240);
手动创建MediaFormat对象后必须在writeSampleData之前将SPS和PPS的数据分别设置成"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));
设置完成后开始调用writeSampleData开始写入文件数据。
BufferInfo info = new BufferInfo();
info.offset = 0;
info.size = sampleSize;
info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
info.presentationTimeUs = timestamp;
mMediaMuxer.writeSampleData(mVideoTrackIndex,buffer,info);
info.size是填入数据的大小,即拿到帧数据byte数组的长度
info.flags是判断当前帧是否为关键帧,即当frame type == 5时填入true即可
info.presentationTimeUs 为正确的时间戳。