aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Android-音视频-视频开发-(四)视频编解码

简介

在学习了Android 音视频的基本的相关知识,并整理了相关的API之后,应该对基本的音视频有一定的轮廓了。

下面是一个Android音视频中相当重要的几个API:MediaCodec(音视频编解码)、MediaExtractor(音视频解封装)、MediaMuxer(音视频封装)。

学习这个API的时候,主要的方向为:

  • 学习 MediaCodec API,完成音频 AAC 硬编、硬解
  • 学习 MediaCodec API,完成视频 H.264 的硬编、硬解

MediaCodec

MediaCodec 介绍

MediaCodec类可以用于使用一些基本的多媒体编解码器(音视频编解码组件),它是Android基本的多媒体支持基础架构的一部分通常和MediaExtractor(解封装音视频), MediaSync(音视频同步), MediaMuxer(封装音视频), MediaCrypto(解码加密的媒体数据), MediaDrm(解密DRM), Image, Surface, AudioTrack 一起使用。

一个编解码器可以处理输入的数据来产生输出的数据,编解码器使用一组输入和输出缓冲器异步处理数据。你可以创建一个空的输入缓冲区,填充数据后发送到编解码器进行处理。编解码器使用输入的数据进行转换,然后输出到一个空的输出缓冲区。最后你获取到输出缓冲区的数据,消耗掉里面的数据,释放回编解码器。如果后续还有数据需要继续处理,编解码器就会重复这些操作。输出流程如下:

编解码器的生命周期:

主要的生命周期为:Stopped、Executing、Released。

  • Stopped的状态下也分为三种子状态:Uninitialized、Configured、Error。
  • Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。

生命周期图:同步(左)与异步(右)

MediaCodec API 说明

MediaCodec可以处理具体的视频流,主要有这几个方法:

  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
  • queueInputBuffer:输入流入队列
  • dequeueInputBuffer:从输入流队列中取数据进行编码操作
  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
  • releaseOutputBuffer:处理完成,释放ByteBuffer数据

MediaCodec 流控

流控基本概念:

流控就是流量控制。涉及到了 TCP 和视频编码:对 TCP 来说就是控制单位时间内发送数据包的数据量,对编码来说就是控制单位时间内输出数据的数据量。

  • TCP 的限制条件是网络带宽,流控就是在避免造成或者加剧网络拥塞的前提下,尽可能利用网络带宽。带宽够、网络好,我们就加快速度发送数据包,出现了延迟增大、丢包之后,就放慢发包的速度(因为继续高速发包,可能会加剧网络拥塞,反而发得更慢)。
    视频编码的限制条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,限制条件变成了传输带宽/文件大小,我们希望在控制数据量的前提下,画面质量尽可能高。
  • 无论是要发送的 TCP 数据包,还是要编码的图像,都可能出现“尖峰”,也就是短时间内出现较大的数据量。TCP 面对尖峰,可以选择不为所动(尤其是网络已经拥塞的时候),这没有太大的问题,但如果视频编码也对尖峰不为所动,那图像质量就会大打折扣了。如果有几帧数据量特别大,但仍要把码率控制在原来的水平,那势必要损失更多的信息,因此图像失真就会更严重。

Android 硬编码流控:

MediaCodec 流控相关的接口并不多,一是配置时设置目标码率和码率控制模式,二是动态调整目标码率(Android 19 版本以上)。

配置时指定目标码率和码率控制模式

1
2
3
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
mVideoCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

码率控制模式有三种:

  • CQ 表示完全不控制码率,尽最大可能保证图像质量;
  • CBR 表示编码器会尽量把输出码率控制为设定值,即我们前面提到的“不为所动”;
  • VBR 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;

Android 流控策略选择:

  • 质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择 CQ 码率控制策略。
  • VBR 输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择。
  • CBR 的优点是稳定可控,这样对实时性的保证有帮助。所以 WebRTC 开发中一般使用的是CBR。

MediaCodec 使用

MediaCodec常用方法

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
createEncoderByType(@NonNul String type):静态构造方法,type为指定的音视频格式,创建指定格式的编码器
createDecoderByType(@NonNull String type):静态构造方法,type为指定的音视频格式,创建指定格式的解码器

// MediaCodec的设置
configure(
@Nullable MediaFormat format, // 绑定编解码的媒体格式
@Nullable Surface surface, // 绑定surface,可以直接完成数据的渲染
@Nullable MediaCrypto crypto, // 加密算法
@ConfigureFlag int flags) // 加密的格式,如果不需要直接设置0即可

int dequeueInputBuffer(long timeoutUs)
// timeoutUs等待时间,返回可以使用的输入buffer的索引

// 设置指定索引位置的buffer的信息
queueInputBuffer(
int index, // 数组的索引值
int offset, // 写入buffer的起始位置
int size, // 写入的输出的长度
long presentationTimeUs, // 该数据显示的时间戳
int flags // 该数据的标记位,例如关键帧,结束帧等等
)

int dequeueOutputBuffer(
@NonNull BufferInfo info, // 这个BufferInfo需要自己手动创建,调用后,会把该索引的数据的信息写在里面
long timeoutUs // 等待时间
)
// timeoutUs等待时间,返回可以读取的buffer的索引

releaseOutputBuffer(int index, boolean render):释放指定索引位置的buffer
// index:索引
// render:如果绑定了surface,该数据是否要渲染到画布上

MediaCodec是系统级别的编解码库,底层还是调用native方法,使用MediaCodec的基本流程是:

创建与文件相匹配的MediaCodec -> MediaCodec写入数据,进行编码/解码 -> 读取MediaCodec编/解码结果

【音频MediaCodec的具体使用详见音频篇编解码】


MediaExtractor

MediaExtractor 介绍

MediaExtractor字面意思是多媒体提取器,它在Android的音视频开发里主要负责提取视频或者音频中的信息和数据流(例如将视频文件,解析头信息,剥离出音频与视频)。

MediaExtractor API 说明

  • setDataSource(String path):即可以设置本地文件又可以设置网络文件
  • getTrackCount():得到源文件通道数
  • getTrackFormat(int index):获取指定(index)的通道格式
  • getSampleTime():返回当前的时间戳
  • readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
  • advance():读取下一帧数据
  • release(): 读取结束后释放资源

MediaExtractor 使用

  1. 通过setDataSource()设置数据源,数据源可以是本地文件地址,也可以是网络地址

    1
    2
    MediaExtractor mVideoExtractor = new MediaExtractor();
    mVideoExtractor.setDataSource(mVideoPath);
  2. 可以通过getTrackFormat(int index)来获取各个track的MediaFormat,通过MediaFormat来获取track的详细信息,如:MimeType、分辨率、采样频率、帧率等等

    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
    for (int i = 0; i < mVideoExtractor.getTrackCount(); i++) {
    MediaFormat format = mVideoExtractor.getTrackFormat(i);
    }

    // MediaFormat: 封装描述媒体数据格式的信息,无论是音频还是视频。媒体数据的格式被指定为字符串/值对。

    // 【肯定可以获取到的】
    // 获取MIME信息
    MediaFormat mediaFormat = extractor.getTrackFormat(0);//获取多媒体格式,因为是demo已经确定自己的视频文件没问题,所以直接获取0位轨道
    String mimeFormat = mediaFormat.getString(MediaFormat.KEY_MIME);//获取MIME格式内容
    Log.e(TAG, "mediaExtractor: 获取MIME格式内容="+mimeFormat);
    // 获取语言格式(大多数情况是获取到空的字符串,但是至少不会报null)
    MediaFormat mediaFormat = extractor.getTrackFormat(0);
    String language = mediaFormat.getString(MediaFormat.KEY_LANGUAGE);//获取语言格式内容
    Log.e(TAG, "mediaExtractor: 获取语言格式内容="+language);
    // 视频的高度与宽度
    MediaFormat mediaFormat = extractor.getTrackFormat(0);
    int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);//获取高度
    int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);//获取高度
    // 播放总时长
    long durationTime = mediaFormat.getLong(MediaFormat.KEY_DURATION);//总时间
    // 获取MediaFormat描述的数据缓冲区的最大字节数
    int maxByteCount = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);//获取视频缓存输出的最大大小

    // 【不一定能获取到,没有则会空指针】
    // 获取采样率
    int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);//获取采样率
    // 获取比特率
    int bitRate = mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE);//获取比特
    // 获取声道数量
    int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);//获取声道数量
    // 获取最大高度与最大宽度
    int maxWidth = mediaFormat.getInteger(MediaFormat.KEY_MAX_WIDTH);//最大宽度
    int maxHeight = mediaFormat.getInteger(MediaFormat.KEY_MAX_HEIGHT);//最大高度
    // 获取颜色格式
    int colorFormat = mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);//颜色格式
    // 获取帧率
    int frameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);//帧率
  3. 获取到track的详细信息后,通过selectTrack(int index)选择指定的通道

    1
    2
    3
    4
    if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
    mVideoExtractor.selectTrack(i);
    break;
    }
  4. 指定通道之后就可以从MediaExtractor中读取数据了

    1
    2
    3
    4
    5
    6
    7
    8
    while (true) {
    int sampleSize = mVideoExtractor.readSampleData(buffer, 0);
    if (sampleSize < 0) {
    break;
    }
    // do something
    mVideoExtractor.advance(); // 移动到下一帧
    }
  5. 在读取结束之后,记得释放资源

    1
    mVideoExtractor.release();

MediaMuxer

MediaMuxer 介绍

利用MediaExtractor提取的aac和.h264文件不经过处理没办法播放,需要MediaMuxer合并生成可以播放的文件(aac文件和.h264需要首先利用MediaMuxer生成MP4文件,才能进行合并)。

MediaMuxer从api18开始提供,可以封装编码后的视频流和音频流到视频文件中。目前MediaMuxer支持的文件输出格式包括MP4,webm和3gp。

MediaMuxer API 说明

  • MediaMuxer(String path, int format)
  • addTrack(MediaFormat format):利用MediaFormat添加音频或视频轨道
  • release():释放MediaMuxer的资源
  • setLocation(float latitude,float longitude):设置并存储地理位置信息到生成文件中
  • setOrientationHint(int degrees):设置输出视频回放的方向提示
  • start():开始muxer,等待数据的输入
  • stop():停止muxer,调用这个函数后将生成合成的文件
  • writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):往muxer中写入编码的数据。

MediaMuxer 使用

  1. 生成MediaMuxer对象:

    通过new MediaMuxer(String path, int format)指定视频文件输出路径和文件格式:
    MediaMuxer mMediaMuxer = new MediaMuxer(mOutputVideoPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

  2. addTrack

    addTrack(MediaFormat format),添加媒体通道,传入MediaFormat对象,通常从MediaExtractor或者MediaCodec中获取,也可以自己创建
    addTrack会返回trackindex

  3. 调用start函数

    MediaMuxer.start();

  4. 写入数据

    调用MediaMuxer.writeSampleData()向mp4文件中写入数据了。每次只能添加一帧视频数据或者单个Sample的音频数据,需要BufferInfo对象作为参数。
    BufferInfo info = new BufferInfo();
    info.offset = 0;
    info.size = sampleSize;
    info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
    info.presentationTimeUs = mVideoExtractor.getSampleTime();
    mMediaMuxer.writeSampleData(videoTrackIndex, buffer, info);
    info.size 必须填入数据的大小
    info.flags 需要给出是否为同步帧/关键帧
    info.presentationTimeUs 必须给出正确的时间戳,注意单位是 us,第二次getSampleTime()和首次getSampleTime()的时间差。

  5. 释放关闭资源

    结束写入后关闭以及释放资源:
    MediaMuxer.stop();
    MediaMuxer.release();

官网say that it is generally used like this:

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
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();

muxer.start();
while(!finished) {
// getInputBuffer() will fill the inputBuffer with one frame of encoded
// sample from either MediaCodec or MediaExtractor, set isAudioSample to
// true when the sample is audio data, set up all the fields of bufferInfo,
// and return true if there are no more samples.
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
}
};
muxer.stop();
muxer.release();

综合例子

视频换音

使用MediaExtractor和MediaMuxer来实现视频的换音

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
private void muxingAudioAndVideo() throws IOException {
MediaMuxer mMediaMuxer = new MediaMuxer(mOutputVideoPath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

// 视频的MediaExtractor
MediaExtractor mVideoExtractor = new MediaExtractor();
mVideoExtractor.setDataSource(mVideoPath);
int videoTrackIndex = -1;
for (int i = 0; i < mVideoExtractor.getTrackCount(); i++) {
MediaFormat format = mVideoExtractor.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
mVideoExtractor.selectTrack(i);
videoTrackIndex = mMediaMuxer.addTrack(format);
break;
}
}

// 音频的MediaExtractor
MediaExtractor mAudioExtractor = new MediaExtractor();
mAudioExtractor.setDataSource(mAudioPath);
int audioTrackIndex = -1;
for (int i = 0; i < mAudioExtractor.getTrackCount(); i++) {
MediaFormat format = mAudioExtractor.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
mAudioExtractor.selectTrack(i);
audioTrackIndex = mMediaMuxer.addTrack(format);
}
}

// 添加完所有轨道后start
mMediaMuxer.start();

// 封装视频track
if (-1 != videoTrackIndex) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.presentationTimeUs = 0;
ByteBuffer buffer = ByteBuffer.allocate(100 * 1024);
while (true) {
int sampleSize = mVideoExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
break;
}

info.offset = 0;
info.size = sampleSize;
info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
info.presentationTimeUs = mVideoExtractor.getSampleTime();
mMediaMuxer.writeSampleData(videoTrackIndex, buffer, info);

mVideoExtractor.advance();
}
}

// 封装音频track
if (-1 != audioTrackIndex) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.presentationTimeUs = 0;
ByteBuffer buffer = ByteBuffer.allocate(100 * 1024);
while (true) {
int sampleSize = mAudioExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
break;
}

info.offset = 0;
info.size = sampleSize;
info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
info.presentationTimeUs = mAudioExtractor.getSampleTime();
mMediaMuxer.writeSampleData(audioTrackIndex, buffer, info);

mAudioExtractor.advance();
}
}

// 释放MediaExtractor
mVideoExtractor.release();
mAudioExtractor.release();

// 释放MediaMuxer
mMediaMuxer.stop();
mMediaMuxer.release();
}

硬解码h.265视频及音频进行播放

视频解码

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
// 【设置数据源】
MediaExtractor mediaExtractor = new MediaExtractor();
try {
mediaExtractor.setDataSource(path); // 设置数据源
} catch (IOException e1) {
e1.printStackTrace();
}

// 【根据视频的编码信息来初始化MediaCodec: 视频的mimeType是video类型。】
String mimeType = null;
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { // 信道总数
MediaFormat format = mediaExtractor.getTrackFormat(i); // 音频文件信息
mimeType = format.getString(MediaFormat.KEY_MIME);
if (mimeType.startsWith("video/")) { // 视频信道
mediaExtractor.selectTrack(i); // 切换到视频信道
try {
mediaCodec = MediaCodec.createDecoderByType(mimeType); // 创建解码器,提供数据输出
} catch (IOException e) {
e.printStackTrace();
}
mediaCodec.configure(format, surface, null, 0);
break;
}
}
mediaCodec.start(); // 启动MediaCodec ,等待传入数据

// 【获取缓存器】
// 输入
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); // 用来存放目标文件的数据
// 输出
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers(); // 解码后的数据
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); // 用于描述解码得到的byte[]数据的相关信息

// 【开始解码】
while (!Thread.interrupted()) {
if (!bIsEos) {
int inIndex = mediaCodec.dequeueInputBuffer(0);
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int nSampleSize = mediaExtractor.readSampleData(buffer, 0); // 读取一帧数据至buffer中
if (nSampleSize < 0) {
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
bIsEos = true;
} else {
// 填数据
mediaCodec.queueInputBuffer(inIndex, 0, nSampleSize, mediaExtractor.getSampleTime(), 0); // 通知MediaDecode解码刚刚传入的数据
mediaExtractor.advance(); // 继续下一取样
}
}
}
int outIndex = mediaCodec.dequeueOutputBuffer(info, 0);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers = mediaCodec.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.d(TAG, "New format " + mediaCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "dequeueOutputBuffer timed out!");
break;
default:
ByteBuffer buffer = outputBuffers[outIndex];
Log.v(TAG, "We can't use this buffer but render it due to the API limit, " + buffer);

mediaCodec.releaseOutputBuffer(outIndex, true);
break;
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
}

// 【解码完成后释放资源】
mediaCodec.stop();
mediaCodec.release();
mediaExtractor.release();

这样视频的解码就已经完成了,此时surfaceView已经可以播放视频了,接下来是音频解码。

音频解码的过程和上面大同小异,主要区别在于,视频是用surfaceView播放显示的,而音频我们需要使用AudioTrack来播放。

音频解码

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// 【创建一个AudioPlayer类用于播放音频】
public class AudioPlayer {
private int mFrequency;// 采样率
private int mChannel;// 声道
private int mSampBit;// 采样精度
private AudioTrack mAudioTrack;

public AudioPlayer(int frequency, int channel, int sampbit) {
this.mFrequency = frequency;
this.mChannel = channel;
this.mSampBit = sampbit;
}

/**
* 初始化
*/
public void init() {
if (mAudioTrack != null) {
release();
}
// 获得构建对象的最小缓冲区大小
int minBufSize = AudioTrack.getMinBufferSize(mFrequency, mChannel, mSampBit);
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
mFrequency, mChannel, mSampBit, minBufSize, AudioTrack.MODE_STREAM);
mAudioTrack.play();
}

/**
* 释放资源
*/
private void release() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
}
}

/**
* 将解码后的pcm数据写入audioTrack播放
*
* @param data 数据
* @param offset 偏移
* @param length 需要播放的长度
*/
public void play(byte[] data, int offset, int length) {
if (data == null || data.length == 0) {
return;
}
try {
mAudioTrack.write(data, offset, length);
} catch (Exception e) {
e.printStackTrace();
}
}
}

// 【初始化音频解码器:音频的mineType是audio类型,我们根据这个来去音频信息即可。】
String mimeType;
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { // 信道总数
MediaFormat format = mediaExtractor.getTrackFormat(i); // 音频文件信息
mimeType = format.getString(MediaFormat.KEY_MIME);
if (mimeType.startsWith("audio/")) { // 音频信道
mediaExtractor.selectTrack(i); // 切换到 音频信道
try {
mediaCodec = MediaCodec.createDecoderByType(mimeType); // 创建解码器,提供数据输出
} catch (IOException e) {
e.printStackTrace();
}
mediaCodec.configure(format, null, null, 0);
mPlayer = new AudioPlayer(format.getInteger(MediaFormat.KEY_SAMPLE_RATE), AudioFormat
.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
mPlayer.init();
break;
}
}
if (mediaCodec == null) {
Log.e(TAG, "Can't find video info!");
return;
}

mediaCodec.start(); // 启动MediaCodec ,等待传入数据

// 【音频解码:音频解码过程与视频解码大同小异,只需要额外调用一下我们创建的AudioPlayer来播放音频即可。】
while (!Thread.interrupted()) {

if (!bIsEos) {
int inIndex = mediaCodec.dequeueInputBuffer(0);
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int nSampleSize = mediaExtractor.readSampleData(buffer, 0); // 读取一帧数据至buffer中
if (nSampleSize < 0) {
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
bIsEos = true;
} else {
// 填数据
mediaCodec.queueInputBuffer(inIndex, 0, nSampleSize, mediaExtractor.getSampleTime(), 0); // 通知MediaDecode解码刚刚传入的数据
mediaExtractor.advance(); // 继续下一取样
}
}
}

int outIndex = mediaCodec.dequeueOutputBuffer(info, 0);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers = mediaCodec.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.d(TAG, "New format " + mediaCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "dequeueOutputBuffer timed out!");
break;
default:
ByteBuffer buffer = outputBuffers[outIndex];
Log.v(TAG, "We can't use this buffer but render it due to the API limit, " + buffer);

while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
//用来保存解码后的数据
byte[] outData = new byte[info.size];
buffer.get(outData);
//清空缓存
buffer.clear();
//播放解码后的数据
mPlayer.play(outData, 0, info.size);
mediaCodec.releaseOutputBuffer(outIndex, true);
break;
}

// All decoded frames have been rendered, we can stop playing
// now
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
}

参考与鸣谢

https://www.cnblogs.com/renhui/p/7478527.html
https://www.jianshu.com/p/e7eae2541e01
https://www.cnblogs.com/guanxinjing/p/11378133.html
https://www.jianshu.com/p/66acab100e4b
https://blog.csdn.net/qq_25412055/article/details/78990538
https://blog.csdn.net/u010126792/article/details/86510903