了解了音视频的编解码过程,我们接下来使用一下经常跟MediaCodec一起搭配的MediaExtractor和MediaMuxer。最后会使用一个简单的demo来了解具体了解这两个工具类的使用过程。这一节我们就先不讲MediaCodec了,放到下节的demo。
一、MediaExtractor
Android提供了一个MediaExtractor类,可以用来分离容器中的视频track和音频track。
主要API介绍:
- setDataSource(String path):即可以设置本地文件又可以设置网络文件
- getTrackCount():得到源文件通道数?
- getTrackFormat(int index):获取指定(index)的通道格式
- getSampleTime():返回当前的时间戳?
- readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
- advance():读取下一帧数据
- release(): 读取结束后释放资源
MediaExtractor 的使用主要有这么几步:
- 设置数据源
- 获取通道数,切换到想要的轨道
- 循环读取每帧的样本数据
- 完成后释放资源
二、MediaMuxer
MediaMuxer的作用是生成音频或视频文件;还可以把音频与视频混合成一个音视频文件。
相关API介绍:
- MediaMuxer(String path, int format):path:输出文件的名称 ?format:输出文件的格式;当前只支持MP4格式;
- addTrack(MediaFormat format):添加通道;我们更多的是使用MediaCodec.getOutpurForma()或Extractor.getTrackFormat(int index)来获取MediaFormat;也可以自己创建;
- start():开始合成文件
- writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把ByteBuffer中的数据写入到在构造器设置的文件中;
- stop():停止合成文件
- release():释放资源
MediaMuxer的使用步骤:
- 设置目标文件路径和音视频格式
- 添加要合成的轨道,包括音轨和视轨
- 开始合成,循环写入每帧样本数据
- 完成后释放
三、MediaFormat
????????用MediaCodec来进行编解码,在创建MediaCodec时需要调用configure方法进行配置,Mediaformat则是configure需要传入的一个参数。
?3.1 视频类型的Mediaformat
可以通过如下代码创建视频类型Mediaformat:
MediaFormat videoFormat = MediaFormat.createVideoFormat(videoType, width, height);
方法的参数类型:
????????MediaFormat.MIMETYPE_VIDEO_AVC(H.264) ????????MediaFormat.MIMETYPE_VIDEO_HEVC(H.265)
- width和height需要根据底层支持的分辨率来设置,如果width和height设置的不符合要求会出现如下错误:
E/CameraCaptureSession: Session 1: Failed to create capture session; configuration failed
对于视频类型而言有下列四个配置是必须指定的:手动配置和直接获取原视频的配置
// 指定编码器颜色格式
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 指定帧率
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// 指定比特率
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 10000000);
//指定关键帧时间间隔,一般设置为每秒关键帧
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
?3.2 音频类型的Mediaformat
可以通过如下代码创建音频类型Mediaformat:
MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channelCount);
方法的参数类型:
- audioType:常用的是MediaFormat.MIMETYPE_AUDIO_AAC
- sampleRate:采样率
- channelCount:声道数量
单声道 channelCount=1 , 双声道 channelCount=2
对于音频类型而言有一个配置是必须指定的:
//音频比特率(码率)
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
四、MediaCodec.BufferInfo?
????????用于描述解码得到的byte[]数据的相关信息,每缓冲区元数据包括指定相关编解码器(输出)缓冲区中有效数据范围的偏移量和大小。
主要有四个属性:
- int?flags :与缓冲区关联的缓冲区标志。
- int?offset :缓冲区中数据的起始偏移量。
- long?presentationTimeUs :缓冲区的显示时间戳,以微秒计。这是从相应的输入缓冲区传入的表示时间戳中获得的。对于大小为0的缓冲区,应该忽略这一点。
- int?size :缓冲区中的数据量(以字节为单位)。如果这是
0 缓冲区中没有数据,可以丢弃。大小为0的缓冲区的唯一用途是携带流结束标记。
flags详解:
值是0 或以下各项的组合MediaCodec.BUFFER_FLAG_SYNC_FRAME ,?MediaCodec.BUFFER_FLAG_KEY_FRAME ,?MediaCodec.BUFFER_FLAG_CODEC_CONFIG ,?MediaCodec.BUFFER_FLAG_END_OF_STREAM ,?MediaCodec.BUFFER_FLAG_PARTIAL_FRAME 、以及Android . media . media codec . buffer _ FLAG _ MUXER _ DATA
- ?BUFFER_FLAG_CODEC_CONFIG?? 常数值:2:这表明如此标记的缓冲区包含编解码器初始化/编解码器特定数据,而不是媒体数据。
- BUFFER_FLAG_END_OF_STREAM 常数值:4:这表示流的结束,即在此之后将没有缓冲器可用,当然,除非,flush()如下。
- BUFFER_FLAG_KEY_FRAME 常数值:1:这表明如此标记的(编码的)缓冲区包含关键帧的数据。
- BUFFER_FLAG_PARTIAL_FRAME 常数值:8:这表示缓冲区只包含一帧的一部分,解码器应该对数据进行批处理,直到在解码该帧之前出现一个没有该标志的缓冲区。
- BUFFER_FLAG_SYNC_FRAME 常数值:1:这表明如此标记的(编码的)缓冲区包含关键帧的数据。API 21中不赞成使用此常量。 使用BUFFER_FLAG_KEY_FRAME相反,都是关键帧。
五、MediaExtractor和MediaMuxer结合的demo
实现音视频的解封装和封装的过程:
//实现音视频的解封装和封装的过程
public class MediaCodecDemo extends Activity {
//显示解封装后的视频和音频在SD卡保存的位置
private TextView tv_out;
private final String mVideoPath = Environment.getExternalStorageDirectory()
+ "/Pictures/送孟浩然之广陵.mp4";
//解封装和封装在本地使用文件名
private final String inputAudio = "audio1.aac";
private final String outPutVideo = "video1.mp4";
private static final String TAG1 ="解封装MediaExtractor:" ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_codec);
initView();
//提取视频分离出纯音频和纯视频文件
extractorAndMuxerMP4(mVideoPath);
//重新合成成音视频文件
muxerMp4(inputAudio,outPutVideo);
}
private void initView() {
tv_out = findViewById(R.id.tv_out);
}
}
//提取视频分离出纯音频和纯视频文件
private void extractorAndMuxerMP4(String url){
//提取数据(解封装)
//1. 构造MediaExtractor
MediaExtractor mediaExtractor = new MediaExtractor();
try {
//2.设置数据源,数据源可以是本地文件地址,也可以是网络地址:
mediaExtractor.setDataSource(url);
//3.获取轨道数
int trackCount = mediaExtractor.getTrackCount();
//遍历轨道,查看音频轨或者视频轨道信息
for (int i = 0; i < trackCount; i++) {
//4. 获取某一轨道的媒体格式
MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
String keyMime = trackFormat.getString(MediaFormat.KEY_MIME);
if (TextUtils.isEmpty(keyMime)) {
continue;
}
//5.通过mime信息识别音轨或视频轨道,打印相关信息
//(默认的是先扫描到视频,在扫描到音频)
if (keyMime.startsWith("video/")) {
File outputFile = extractorAndMuxer(mediaExtractor, i, "/video.mp4");
tv_out.setText("纯视频文件路径:" + outputFile.getAbsolutePath());
} else if (keyMime.startsWith("audio/")) {
File outputFile = extractorAndMuxer(mediaExtractor, i, "/audio.aac");
tv_out.setText(tv_out.getText().toString() + "\n纯音频路径:"
+ outputFile.getAbsolutePath());
tv_out.setVisibility(View.VISIBLE);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
????????通过getTrackFormat(int index)来获取各个track的MediaFormat,通过MediaFormat来获取track的详细信息,如:MimeType、分辨率、采样频率、帧率等等
//确定是音轨或视频轨道后,文件输出
private File extractorAndMuxer(MediaExtractor mediaExtractor, int i, String outputName) throws IOException{
//获取传过来的MediaExtractor对应轨道的trackFormat
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. 构造MediaMuxer
mediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
//2. 添加轨道信息 参数为MediaFormat
mediaMuxer.addTrack(trackFormat);
//3. 开始合成
mediaMuxer.start();
//4. 设置buffer
ByteBuffer buffer = ByteBuffer.allocate(500 * 1024);//设置每一帧的大小
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//5.通过mediaExtractor.readSampleData读取数据流
int sampleSize = 0;
//循环读取每帧的样本数据
//mediaExtractor.readSampleData(buffer, 0)把指定通道中的数据按偏移量读取到ByteBuffer中
while ((sampleSize = mediaExtractor.readSampleData(buffer, 0)) > 0) {
bufferInfo.flags = mediaExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();
//所有解码的帧都已渲染,我们现在可以停止播放了,虽然这里没有用到
//一般的使用方法是判断 isEOS是否等于0;
//int isEOS = bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM;
//判断输出数据是否为关键帧的方法:
//boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
//6. 把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道
mediaMuxer.writeSampleData(0, buffer, bufferInfo);
//读取下一帧数据
mediaExtractor.advance();
}
Log.i(TAG1, "extractorAndMuxer: " + outputName + "提取封装完成");
mediaExtractor.unselectTrack(i);
//6.关闭
mediaMuxer.stop();
mediaMuxer.release();
return outputFile;
}
这里需要科普一下两个正数进行&运算:两个正数进行&运算的值永远小于或等于最小的数。
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
????????我们知道BUFFER_FLAG_END_OF_STREAM代表的是2^2,也就是0100。因为是&运算,我们只用关注info.flags二进制第三位即可:为0即上面判断为false,为1即上面判断为true。
- 正数与负数的与运算:负数的异或操作需要先把数转换成补码才行(头不变取反+1)
两个数互为相反数位与操作可有特殊用途,位与所剩恰为最低位。
两个数互为相反数异或可能有特殊用途,异或后,所剩最低位左移一位。
到此我们就将音视频解封装成了音频和视频,并且保存在了指定文件当中,我们分析一下流程:
- 构造MediaExtractor(不需要参数) —> 之后的操作使用try/catch包围 —> setDateSource(url)设置本地或者网络资源?—> getTrackConut()获取该资源的通道数 —> for循环通道数 —>?获取某一轨道的媒体格式:getTrackFormat(i)返回一个MediaFormat?—>?判断是什么通道根据trackFormat.getString(MediaFormat.KEY_MIME)返回ketMime的startsWith("?")?。
- 接下来的操作就确定了音轨和视频轨道,同时确定文件的输出地点。
- 构造MediaMuxer (需要指定文件和格式)—>?addTrack(trackFormat)添加轨道信息 参数为MediaFormat,注意这里的MediaFormat要是对应的轨道?—> start()开始合成?—>?设置ByteBuffer,用于缓存一帧数据?—>?MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo()获取bufferInfo?—>?通过mediaExtractor.readSampleData读取数据流,同时也作为一个while循环的判断条件?—> 配置bufferInfo的四个属性?—> 把通过mediaExtractor解封装的数据通过mediaMuxer.writeSampleData写入到对应的轨道?—> 读取下一帧audioExtractor.advance()。
- 循环结束之后mediaExtractor.unselectTrack(i)释放选择?—>?mediaMuxer.stop()停止?—> 最后释放mediaMuxer和mediaExtractor。
接下来我们开始合成操作。
//把音轨和视频轨再合成新的视频
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();
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);
break;
}
}
//再添加音频轨道
audioExtractor.setDataSource(audioFile.getAbsolutePath());
int trackCountAduio = audioExtractor.getTrackCount();
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(TAG1, "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();
}
????????因为这个与上面例子的流程大致相同,上面看懂了,下面基本上没什么问题,所以注释相对比较少。至于过程也就懒得分析了。
????????我们在解封装的过程中同时使用到了MediaExtractor和MediaMuxer,包括合成的时候也用了这两个。不要想当然的认为MediaExtractor解封装出来两文件,两文件根据MediaMuxer就可以合成!!!
????????最后遗留两个问题:
????????1.解封装出来的是不同轨道的资源,可是当做文件输出时,除了文件名不同其他的操作都是一模一样,就连mediaMuxer的参数格式都是MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,那音频文件是怎样合成成功的???
????????AAC代表Advanced Audio Coding(高级音频编码),是一种由MPEG-4标准定义的有损音频压缩格式。而且解封装出来的音频acc和视频MP4改了后缀都可以正常播放。
????????
? ? ? ? 音频文件同样可以理解为一帧帧的说法,之后我回使用实时AAC音频帧并通过AudioTrack来播放,尽情期待。
? ? ? ? 2.分解出来的轨道是固定的吗?还是根据自定义来的?他的个数只能是一个音频一个视频吗?
- 分解出来的轨道不是固定的但一般是两个轨道(一个音频一个视频)
E/测试Demo: 轨道数量 = 2
E/测试Demo: 0编号通道格式 = video/avc
E/测试Demo: 1编号通道格式 = audio/mp4a-latm
- 这个具体的顺序就是根据你使用mediaMuxer添加合成的顺序
- 当然也可能有多个音频和视频在一个盒子里
|