0%

Android 音视频任务7

先做任务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当前缓冲,让它能够继续被使用,否则就那么一点缓冲区,不释放的话几下就没了
    @Override
    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
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
outFormat = MediaFormat.createAudioFormat(desMIMEType, SAMPLE_RATE, CHANNEL_COUNT); //根据编码格式,采样率,声道数创建format
outFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000); //比特率
outFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); //aac特有的profile
outFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);//输入缓存的最大值,设置这个防止一帧的数据量太大而超过默认的buffer大小

//添加aac的csd-0,如果是视频,则csd-0和csd-1都要有(这个是干什么的还不清楚)
ByteBuffer csd = ByteBuffer.allocate(2);
csd.put((byte) ((aacObjLC << 3) | (sampleIndex >> 1)));
csd.position(1);
csd.put((byte) ((byte) ((sampleIndex << 7) & 0x80) | (channelCount << 3)));
csd.flip();
outFormat.setByteBuffer("csd-0", csd); // add csd-0
System.out.println(Arrays.toString(csd.array()) + "===++”);
查看设备支持的编码和解码器:
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] infos = list.getCodecInfos();
for (int i = 0; i < infos.length; i++) {
String name = infos[i].getName();
Log.d(tag, "i=" + i + " name=" + name);
}
根据format创建相应的编码器
//根据相应的格式找到是否有合适的编码器,如果有则返回其名称,然后创建相应的编码器
String encodeName = list.findEncoderForFormat(outFormat);
Log.d(tag, "encodeName=" + encodeName);
mECodec = MediaCodec.createByCodecName(encodeName);
} catch (IOException e) {
e.printStackTrace();
}
mECodec.setCallback(mEncodeCallback); //设置回调
mECodec.configure(outFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //编码配置,编码时要设置最后一个参数
mECodec.start(); //开始编码

callback中:

1
onInputBufferAvailable和解码器的并无不同,都是由MediaExtractor读出数据放到缓存队列里等待处理

onOutputBufferAvailable和默认流程和解码器类似,都是从缓冲队列中取出已经处理好的数据(每次取一帧),但编码器不同的一点是,这个数据只是真实数据流的编码,仅仅有这些没办法封装成可读的格式,还必须要加上可识别的部分,比如wav文件是在所有数据头部加上wav头,aac文件是在每一帧数据前面加上adts头,
adts见 https://www.cnblogs.com/lihaiping/p/5284547.html

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
101
102
103
104
105
106
       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();
}
int outIndex = index;
MediaFormat format = codec.getOutputFormat(outIndex);

ByteBuffer outputBuffer = codec.getOutputBuffer(outIndex);

int size = outputBuffer.limit();
outputSize += size;
byte[] packedData = new byte[size + 7];
addADTStoPacket(packedData, packedData.length);
outputBuffer.get(packedData, 7, size);
try {
fos.write(packedData);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
//用完后释放这个buffer,使其可以接着被使用,如果一直不释放,如果文件太大则会导致缓冲区不够用
outputBuffer.clear();
codec.releaseOutputBuffer(outIndex, false);
if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.d(tag, "BUFFER_FLAG_END_OF_STREAM");
stopAndrealseCodec();
}
}




//每一帧前面都要加上ADTS头,可以看做是每一个AAC帧的帧头
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //44.1KHz
int chanCfg = 2; //CPE

// byte[] packet1 = new byte[packetLen];
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;


// //把这些清晰的放出来,但最好还是用上面的方式
// //syncword
// packet[0] = (byte) 0xFF;
// packet[1] |= 0xF << 4;
// //id
// packet[1] |= 0x1 << 3;
// //layer
// packet[1] |= 0x00 << 1;
// //protection_abscent
// packet[1] |= 0x1;
// //profile
// packet[2] |= (profile -1) << 6;
// //sampling_frequency_index
// packet[2] |= freqIdx << 2;
// //private bit
// packet[2] |= 0x0 << 1;
// //channel_config
// packet[2] |= chanCfg >> 2; //高1位
// packet[3] |= chanCfg << 6; //低2位
// //copy and home;
// packet[3] |= 0x00 << 4;
// //cib and cis
// packet[3] |= 0x00 << 2;
// //frame_length,单位是字节。不用管int是几个字节,把它当13位就行了,一帧的长度一般不可能超过13位能表达的最大值(8KB),
// packet[3] |= packetLen >> 11;
// packet[4] = (byte) (packetLen >> 3);
// int x = packetLen << 5;
// packet[5] |= packetLen << 5;
//
// packet[5] |= 0x7FF >> 6;
// packet[6] |= 0x7FF << 2;
//
}

//释放资源
private void stopAndrealseCodec() {
if (mECodec != null) {
mECodec.release();
mECodec = 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);
}
}

代码见 https://github.com/lujianyun06/VATask/tree/master/app/src/main/java/com/example/lll/va/Task7