先做任务7,是因为做完任务4感觉想要稍微深入了解一下MediaCodec,感觉这样更能把刚学到的知识连贯一下。
任务7. 学习 MediaCodec API,完成音频 AAC 硬编、硬解
MediaCodec的作用是转换编码或解码文件,支持已编码的特定格式转成原始raw数据,
原始raw数据转成特定编码的格式
解码时,mediaCodec设定的格式是输入文件的格式,输出原始raw数据
编码时,mediaCodec设定的格式是输出文件的格式,输入为原始raw数据
解码时文件的格式和解码器设置的格式一定要一样,否则报错
音频编解码:
解码:
- 解码是吧audio/XXX给变成audio/raw,即pcm文件,最原始的未经压缩编码处理的文件。所以解码后的文件一般会比被压缩文件大得多
流程: - 首先要使用MediaExtractor把音轨从文件中提取出来,把format也提取出来,根据要解码的mime创建codec:
mDCodec = MediaCodec.createDecoderByType(srcMIMTType); - 把format设置到codec:mDCodec.configure(mFormat, null, null, 0); //视频文件如果要播放解码出的数据,则第二个参数选择要用于播放的surface
- 数据回调监听:mDCodec.setCallback(mDecodeCallback); (异步处理,感觉异步的好一点)
- 开始解码:mDCodec.start();
callback中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100//输入
//onInputBufferAvailable:当有可用的缓冲区时,会转到这里,在该函数中往可用的缓冲区写入要解码的数据,然后把缓冲区加入待处理队列
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
//获得可用的缓冲
ByteBuffer inputBuffer = codec.getInputBuffer(index);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int size = mExtractor.readSampleData(inputBuffer, 0);
int flag = mExtractor.getSampleFlags();
long presentation = mExtractor.getSampleTime();
int offset = 0;
Log.d(tag, "size= " + size + " flag=" + flag + " presentation=" + presentation);
//要对文件是否读到末尾单独处理,若读到末尾了,size,presentation都要置为0,flag要标注为 MediaCodec.BUFFER_FLAG_END_OF_STREAM
if (size != -1) {
inputSize += size;
codec.queueInputBuffer(index, offset, size, presentation, flag);
mExtractor.advance();
} else { //当文件达到末尾时,用flag
size = 0;
presentation = 0;
flag = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
codec.queueInputBuffer(index, offset, size, presentation, flag);
}
}
//输出
//onOutputBufferAvailable:当有待处理数据完成解码时,会放入输出缓冲区,并转到这里,在该函数中获得解码后的raw数据,并且每次读出数据后要release当前缓冲,让它能够继续被使用,否则就那么一点缓冲区,不释放的话几下就没了
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
if (fos == null) try {
throw new Exception("fos == null!");
} catch (Exception e) {
e.printStackTrace();
}
MediaFormat format = codec.getOutputFormat(index);
Log.d(tag, "out format=" + format.getString(MediaFormat.KEY_MIME));
//获得可用的输出缓冲区(也就是解码好的数据)
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
//如果直接定义 data = new byte[100 * 1024]; 然后调用outputBuffer.get(data),
// 则会使用data的长度去操作buffer,但buffer没这么大,导致BufferUnderflowException
//所以下面两种方式选一种
byte[] data = new byte[outputBuffer.limit()];
outputBuffer.get(data);
// byte[] data = new byte[100 * 1024];
// outputBuffer.get(data,0, outputBuffer.limit());
//
outputSize += data.length;
try {
fos.write(data); //写入流中
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
Log.d(tag, "flag=" + info.flags);
Log.d(tag, "outputSize= " + outputSize);
//用完后释放这个buffer,使其可以接着被使用
codec.releaseOutputBuffer(index, false);
//当到达末尾时,释放资源
if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.d(tag, "BUFFER_FLAG_END_OF_STREAM");
stopAndrealseCodec();
exPCM2WAV();
}
}
//释放资源
private void stopAndrealseCodec() {
if (mDCodec != null) {
mDCodec.release();
mDCodec = null;
mExtractor.release();
mExtractor = null;
Log.d(tag, "outputBuffer BUFFER_FLAG_END_OF_STREAM");
try {
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
Log.d(tag, "stopAndrealseCodec");
Log.d(tag, "inputSize=" + inputSize);
Log.d(tag, "outputSize=" + outputSize);
}
}
//把pcm文件加入wav头变成wav文件,wav文件会被识别成audio/raw,而原始的pcm文件会被识别错误
private void exPCM2WAV(){
//为什么从视频里(只试了mp4和3gp)剥离出来的要 原本的sampleRate/2才是正常速度?
int sampleRate = mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
PcmToWavUtil util = new PcmToWavUtil(sampleRate, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
AudioFormat.ENCODING_PCM_16BIT);
Log.d(tag, "sampleRate=" + sampleRate);
util.pcmToWav(outputFileName, outputFileName + ".wav");
}
编码:
编码是吧audio/raw给变成audio/XXX,是对原始数据压缩编码,但这个有一个问题要注意的是硬件是否支持这种编码方式,比如nexus6p,由于mp3格式编码是不开源的,所以不能编码成mp3文件。下面以把wav的原生格式编码成aac
流程:
编码要比解码多一个步骤,就是设置要编码的格式的各项数据,比如
1 | outFormat = MediaFormat.createAudioFormat(desMIMEType, SAMPLE_RATE, CHANNEL_COUNT); //根据编码格式,采样率,声道数创建format |
callback中:
1 | onInputBufferAvailable和解码器的并无不同,都是由MediaExtractor读出数据放到缓存队列里等待处理 |
onOutputBufferAvailable和默认流程和解码器类似,都是从缓冲队列中取出已经处理好的数据(每次取一帧),但编码器不同的一点是,这个数据只是真实数据流的编码,仅仅有这些没办法封装成可读的格式,还必须要加上可识别的部分,比如wav文件是在所有数据头部加上wav头,aac文件是在每一帧数据前面加上adts头,
adts见 https://www.cnblogs.com/lihaiping/p/5284547.html
1 | public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) { |
代码见 https://github.com/lujianyun06/VATask/tree/master/app/src/main/java/com/example/lll/va/Task7