使用MediaExtractor+MediaCodec+AudioTrack播放音频文件
首先来看一张图:
要想播放一个视频,就得对视频文件解协议、解封装,随后可以得到音频数据和视频数据(分开的),再次分别解码,最后同步播放(因为播放音频和视频的设备不同,所以需要将音视频同步)。
本任务就是实现该图的左半边,进行音频的播放。
(前几个任务中的MediaPlayer是帮我们完成了解封、解码以及同步的操作)
MediaExtractor
用来把视频和音频的数据进行分离
对于一个常见的视频文件,通常有一个视频轨道,和一个音频轨道,通过MediaExtractor来分离音视频轨道数据
当然可能一个视频文件中有多个视频流和多个音频流,
解封装的步骤体现在MediaExtractor中,MediaExtractor从封装格式中分离获取不同format的数据(video/audio)
MediaCodec
解码的步骤体现在MediaCodec中,MediaCodec对特定格式的数据进行解码获取音频原始数据。
MediaCodec有两个ByteBuffer数组,分别是InputBuffers,OutputBuffers用来导入导出解码前后的数据。大致过程是:
- 向MediaCodec中传入待解码的数据
- 解码(自动进行)
- 获取解码后的数据
- 渲染到Surface或载入到AudioTrack
States
Stopped, Executing or Released
首先利用 configure(…)来配置,然后调用start()进入Executing状态,编解码器立即处于刷新子状态,它在其中保存所有缓冲区。
利用stop()来进入Uninitialized状态,如果还需要使用就得再配置。
当时用完后codec后,release()来结束
Create
AudioTrack&Surface
解码后,结合Surface和AudioTrack进行音视频播放。
AudioTrack播放音频是需要获取到音频相关的声道数、采样率,以此来配置。
MediaPlayer可以播放多种格式的声音文件,例如MP3,WAV,OGG,AAC,MIDI等。然而AudioTrack 只能播放PCM数据流
。当然两者之间还是有紧密的联系,MediaPlayer在播放音频时,在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,最后由AudioFlinger进行混音,传递音频给硬件播放出来。利用AudioTrack播放只是跳过Mediaplayer的解码部分而已。参考链接
AudioTrack实现PCM音频播放五步走
-
配置基本参数
-
获取最小缓冲区大小
bufferSizeInBytes参数(构建AudioTrack时的一个入参):
If the track’s creation mode is MODE_STREAM , this should be the desired buffer size for the
AudioTrack
to satisfy the application’s latency requirements. IfbufferSizeInBytes
is less than the minimum buffer size for the output sink, it is increased to the minimum buffer size. The method getBufferSizeInFrames() returns the actual size in frames of the buffer created, which determines the minimum frequency to write to the streamingAudioTrack
to avoid underrun. See getMinBufferSize(int,int,int) to determine the estimated minimum buffer size for an AudioTrack instance in streaming mode. --------来自谷歌
可以利用 getBufferSizeInFrames()和getMinBufferSize(int,int,int)来获取最小缓冲区大小。 -
创建AudioTrack对象
AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId)
此外建议使用Builder来构建AudioTrack---------AudioTrack.Builder | Android Developers (google.cn)Class constructor with AudioAttributes and AudioFormat.
AudioAttributes 封装音频流信息的类
A class to encapsulate a collection of attributes describing information about an audio stream.- usage: “why” you are playing a sound, what is this sound used for. This is achieved with the “usage” information. Examples of usage are
USAGE_MEDIA
andUSAGE_ALARM
. - content type: “what” you are playing. The content type expresses the general category of the content. This information is optional.
CONTENT_TYPE_MOVIE
orCONTENT_TYPE_MUSIC
. - flags: “how” is playback to be affected, see the flag definitions for the specific playback behaviors they control.
AudioFormat 音频帧 此类用于访问音频
格式和通道配置常量
The AudioFormat class is used to access a number of audio format and channel configuration constants.- Sample rate:播放内容的采样率
- Encoding:音频编码方式,对于线性 PCM(因为AudioTrack仅支持PCM编码的数据),音频编码描述样本大小(8 位、16 位或 32 位)以及样本表示形式(整数或浮点数)。
When compressed audio is sent out through a directAudioTrack
, it need not be written in exact multiples of the audio access unit; this differs fromMediaCodec
input buffers. - Channel masks:声道掩码,用于描述样本及其在音频帧中的排列。
有两种类型的声道掩码:- Channel position masks:original。不同的声道有着不同的位置安排 ,CHANNEL_OUTLEFT
- Channel index masks:通过index来
选定特定的声道
,类似于子网掩码的感觉
- usage: “why” you are playing a sound, what is this sound used for. This is achieved with the “usage” information. Examples of usage are
-
获取PCM文件,转成DataInputStream
-
开启/停止播放
If you don’t call write() first, or if you call write() but with an insufficient amount of data, then the track will be in underrun state at play().
只有在利用write()向缓冲区写入数据时,才会播放,否则play()后只会进入一个就绪状态。
Code
Android MediaCodec解码音频,AudioTrack播放音频,PCM数据写入文件-CSDN博客
使用MediaExtractor , MediaCodec对音视频的解码实现简单播放器_mediacode 解码音频播放-CSDN博客
首先需要进行相关的初始化工作,然后在进行解码的操作,解码会把相关数据加载到缓存区然后写入AudioTrack中进行播放,然后再清空该缓存区,随后一直循环到文件结束。
初始化MediaExtractor、AudioTrack、MediaCodec
@RequiresApi(api = Build.VERSION_CODES.M)
private void initPlayer() throws IOException {
// 选取音频轨道
extractor = new MediaExtractor();
try {
//extractor.setDataSource("//sdcard/Movies/big_buck_bunny.mp4");
extractor.setDataSource("//sdcard/Movies/lesson.mp4");
} catch (IOException e) {
e.printStackTrace();
}
int numTracks = extractor.getTrackCount();
int trackIndex = 0;
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 可获取该format所对应的数据类型(以键值对实现,KEY_MIME这个key所对应value记录的是该数据流类型,
// 视频以video/开头,音频以audio/开头)
if (mime.startsWith("audio/")) {
trackIndex = i;
}
}
MediaFormat audioFormat = extractor.getTrackFormat(trackIndex);
Log.i(TAG, String.valueOf(audioFormat));
extractor.selectTrack(trackIndex);
// 获取声道数、采样率 用于计算最小的缓存区大小
int audioChannels = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int audioSampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
Log.i(TAG, "Channels is " + audioChannels + " SamplerRate is " + audioSampleRate);
int minBuffSize = AudioTrack.getMinBufferSize(audioSampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT);
Log.i(TAG, "minBuffSize is " + minBuffSize);
// 配置配置AudioTrack
audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(audioSampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build())
.setBufferSizeInBytes(Math.max(minBuffSize, 2048))
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
// AudioTrack这里的play()是一个就绪状态,它需要从缓冲区中读取数据才能播放,所以只有在MediaCodec将数据解码输入到缓冲区后才可以播放
audioTrack.play();
// MediaCodec 解码器的配置
audioCodec = MediaCodec.createDecoderByType(audioFormat.getString(MediaFormat.KEY_MIME));
Log.i(TAG, "MediaCodec.createDecoderByType‘s audioFormat is " + audioFormat.getString(MediaFormat.KEY_MIME));
audioCodec.configure(audioFormat, null, null, 0);
audioCodec.start();
}
播放音频
private void decodeAudio() {
MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();
long startMs = System.currentTimeMillis();
ByteBuffer inputBuffer;
boolean isEOS = false;
while (!isEOS && !Thread.currentThread().isInterrupted()) {
// !Thread.currentThread().isInterrupted() 是用来获取当前线程是否被中断,如果被中断就直接结束了
// 获取可用的输入缓冲区的索引
int inputBufferId = audioCodec.dequeueInputBuffer(10000);
Log.i(TAG, "The inputBufferId is " + inputBufferId);
if (inputBufferId >= 0) {
// 获取输入缓冲区,并向缓冲区写入数据
inputBuffer = audioCodec.getInputBuffer(inputBufferId);
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize > 0) {
// 将填满数据的inputBuffer提交到编码队列
audioCodec.queueInputBuffer(inputBufferId, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
} else {
isEOS = true;
break;
}
}
// 获取已成功编解码的输出缓冲区的索引
int outputBufferId = audioCodec.dequeueOutputBuffer(audioBufferInfo, 10000);
Log.i(TAG, "the outputBufferId is " + outputBufferId);
byte[] pcmData;
while (outputBufferId >= 0) {
// 获取输出缓冲区
ByteBuffer outputBuffer = audioCodec.getOutputBuffer(outputBufferId);
pcmData = new byte[audioBufferInfo.size];
if (outputBuffer != null) {
outputBuffer.get(pcmData);
outputBuffer.clear();//用完后清空,复用
}
audioTrack.write(pcmData, 0, audioBufferInfo.size);
Log.i(TAG, "audioTrack.write");
//取解码后的数据
int outputIndex = audioCodec.dequeueOutputBuffer(audioBufferInfo, 10000);
audioCodec.releaseOutputBuffer(outputBufferId, false);
outputBufferId = audioCodec.dequeueOutputBuffer(audioBufferInfo, 10000);
}
}
audioCodec.stop();
audioCodec.release();
extractor.release();
audioTrack.stop();
audioTrack.release();
}
中断线程
当线程的run方法执行方法体中最后一条语句执行后再执行return语句返回时,或者出现了方法中没有捕获的异常,线程将会被终止。
Java早期版本中,还有一个stop方法来终止线程,不过此方法已经被弃用。
现在除了已经废弃的stop方法(因为不安全),没有办法可以强制线程终止。
不过可以用 interrupt
方法来请求终止一个线程:
当对一个线程调用interrupt方法时,就会设置线程的中断状态,这是一个boolean值,每个线程都会时不时地检查这个标志,以判断线程是否被中断。
while (!Thread.currentThread().isInterrupted()) { do more work }