这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
经过前两篇【音频篇】和【视频篇】的学习,我们了解了关于音视频采集和播放的一些知识,接下来将进入应用篇,用之前所学知识来完成具有实际意义的功能
一、知识点回顾
经过前两篇的学习,我们对音频
、视频
的采集
和播放
有了一定的了解,但之前对于音频和视频我们是分开进行播放,而在实际开发中,我们往往需要正常的播放音频和视频
首先,我们回顾下关于音视频的一些相关知识
1.1 音频
- 音频的原始格式是
PCM
,可以使用AudioRecord
对麦克风数据进行采样,从而得到PCM
数据 - 对于
PCM
音频数据,可以使用MediaCodec
对其进行硬编码,最终得到AAC
编码格式的数据 - 反过来,对于
AAC
编码格式的数据,同样可以使用MediaCodec
对其进行硬解码,最终得到PCM
原始格式数据 AAC
编码格式的数据可以直接使用一般的播放器播放,PCM
原始格式数据可以使用AudioTrack
播放
1.2 视频
- 视频的原始格式是
YUV
或RGB
,可以从相机预览中获取NV21
(YUV420
的一种)数据 - 对于
YUV
视频数据,可以使用MediaCodec
对其进行硬编码,最终得到AVC
编码格式的数据 - 反过来,对于
AVC
编码格式的数据,同样可以使用MediaCodec
对其进行硬解码,最终得到YUV
原始格式数据,当然,还可以使用Surface
直接渲染解码的数据
除此之外,还介绍了MediaExtractor
和MediaMuxer
的用法
1.3 MediaExtractor
- 通过
MediaExtractor
可以分离Mp4
(一种封装格式)中的音频和视频轨道 - 分离之后,可以对齐进行一帧一帧的读取
- 读取后的数据,一般用于传入
MediaCodec
进行解码操作
1.4 MediaMuxer
- 通过
MediaMuxer
可以合成Mp4
,即将音频和视频编码格式写入一个文件 - 操作的方式也是通过
轨道id
- 获取编码的数据后,传入对应
轨道id
,写入MediaMuxer
中即可
以上,整体回顾了【音频篇】和【视频篇】介绍的内容,下面我们就来根据学到的知识制作一款简单的播放器
1.5 播放/录制流程图
播放
从上往下看,整个过程就是播放
- 解封装
- 解码
- 渲染
录制
从下往上看,整个过程就是录制
- 采集
- 编码
- 合成
二、视频播放器
在之前的解码音频、视频中,我们均使用一个线程进行操作,那么现在是要做一个能够完整播放视频的功能,则需要两个线程,一个处理音频,一个处理视频
2.1 具体步骤
音频线程
MediaExtractor
从Mp4
中解析音频轨道- 初始化音频的
MediaCodec
- 初始化
AudioTrack
- 从
MediaExtractor
取出待解码的数据,传入MediaCodec
- 从
MediaCodec
取出解码后的数据,传入AudioTrack
- 停止播放,释放资源
视频线程
MediaExtractor
从Mp4中解析视频轨道- 初始化视频的
MediaCodec
- 从
MediaExtractor
取出待解码的数据,传入MediaCodec
- 从
MediaCodec
取出解码后的数据,直接渲染到Surface
- 停止播放,释放资源
2.2 具体实现
音频播放线程
解码的过程,在音频解码这一章有详细的介绍
private static class AudioPlayThread extends Thread {
private static final long TIMEOUT_MS = 2000L;
private String path;
private MediaExtractor mediaExtractor;
private MediaCodec mediaCodec;
private AudioTrack audioTrack;
private MediaFormat format;
private String mime;
private boolean isStopPlay = false;
public AudioPlayThread(String path) {
this.path = path;
}
@Override
public void run() {
super.run();
initMediaExtractor();
initMediaCodec();
initAudioTrack();
play();
}
private void initMediaExtractor() {
if (TextUtils.isEmpty(path)) {
return;
}
try {
mediaExtractor = new MediaExtractor();
mediaExtractor.setDataSource(path);
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
format = mediaExtractor.getTrackFormat(i);
mime = format.getString(MediaFormat.KEY_MIME);
if (!TextUtils.isEmpty(mime) && mime.startsWith("audio/")) {
mediaExtractor.selectTrack(i);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
mediaExtractor = null;
format = null;
mime = null;
}
}
private void initMediaCodec() {
if (format == null || TextUtils.isEmpty(mime)) {
return;
}
try {
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, null, null, 0);
} catch (IOException e) {
e.printStackTrace();
mediaCodec = null;
}
}
private void initAudioTrack() {
if (format == null) {
return;
}
int sampleRateInHz = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSizeInBytes = AudioTrack.getMinBufferSize(
sampleRateInHz,
channelConfig, audioFormat
);
audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRateInHz,
channelConfig,
audioFormat,
bufferSizeInBytes,
AudioTrack.MODE_STREAM
);
}
private void play() {
if (mediaExtractor == null || mediaCodec == null || audioTrack == null) {
return;
}
long startMs = System.currentTimeMillis();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
mediaCodec.start();
audioTrack.play();
for (; ; ) {
if (isStopPlay) {
release();
break;
}
int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
int readSize = -1;
if (inputBuffer != null) {
readSize = mediaExtractor.readSampleData(inputBuffer, 0);
}
if (readSize <= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isStopPlay = true;
} else {
mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
mediaExtractor.advance();
}
}
int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
if (outputBuffer != null && info.size > 0) {
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
byte[] data = new byte[info.size];
outputBuffer.get(data);
outputBuffer.clear();
audioTrack.write(data, 0, info.size);
}
mediaCodec.releaseOutputBuffer(outputBufferId, false);
}
}
}
void stopPlay() {
isStopPlay = true;
try {
join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void release() {
if (mediaExtractor != null) {
mediaExtractor.release();
mediaExtractor = null;
}
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
if (audioTrack != null) {
audioTrack.stop();
audioTrack.release();
audioTrack = null;
}
}
}
视频播放线程 解码的过程,在视频解码这一章有详细的介绍
private static class VideoPlayThread extends Thread {
private static final long TIMEOUT_MS = 2000L;
private String path;
private Surface surface;
private MediaExtractor mediaExtractor;
private MediaCodec mediaCodec;
private MediaFormat format;
private String mime;
private boolean isStopPlay = false;
public VideoPlayThread(String path, Surface surface) {
this.path = path;
this.surface = surface;
}
@Override
public void run() {
super.run();
initMediaExtractor();
initMediaCodec();
play();
}
private void initMediaExtractor() {
if (TextUtils.isEmpty(path)) {
return;
}
try {
mediaExtractor = new MediaExtractor();
mediaExtractor.setDataSource(path);
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
format = mediaExtractor.getTrackFormat(i);
mime = format.getString(MediaFormat.KEY_MIME);
if (!TextUtils.isEmpty(mime) && mime.startsWith("video/")) {
mediaExtractor.selectTrack(i);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
mediaExtractor = null;
format = null;
mime = null;
}
}
private void initMediaCodec() {
if (format == null || TextUtils.isEmpty(mime) || surface == null) {
return;
}
try {
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, surface, null, 0);
} catch (IOException e) {
e.printStackTrace();
mediaCodec = null;
}
}
private void play() {
if (mediaExtractor == null || mediaCodec == null) {
return;
}
long startMs = System.currentTimeMillis();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
mediaCodec.start();
for (; ; ) {
if (isStopPlay) {
release();
break;
}
int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
int readSize = -1;
if (inputBuffer != null) {
readSize = mediaExtractor.readSampleData(inputBuffer, 0);
}
if (readSize <= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isStopPlay = true;
} else {
mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0
mediaExtractor.advance();
}
}
int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
if (outputBuffer != null && info.size > 0) {
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
byte[] data = new byte[info.size];
outputBuffer.get(data);
outputBuffer.clear();
// 得到的data数据就是YUV数据,可以拿去做对应的业务
}
mediaCodec.releaseOutputBuffer(outputBufferId, true);
}
}
}
void stopPlay() {
isStopPlay = true;
try {
join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void release() {
if (mediaExtractor != null) {
mediaExtractor.release();
mediaExtractor = null;
}
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
surface = null;
}
}
外部调用
外部需要调用的话,则需要创建两个线程对象,并启动即可,在不需要时,记得及时停止播放,并释放资源
private AudioPlayThread audioPlayThread;
private VideoPlayThread videoPlayThread;
public void stat(String path, Surface surface) {
stop();
audioPlayThread = new AudioPlayThread(path);
videoPlayThread = new VideoPlayThread(path, surface);
audioPlayThread.start();
videoPlayThread.start();
}
public void stop() {
if (audioPlayThread != null) {
audioPlayThread.stopPlay();
audioPlayThread = null;
}
if (videoPlayThread != null) {
videoPlayThread.stopPlay();
videoPlayThread = null;
}
}
至此,一个简单的视频播放器就完成了!!!