android 硬解码播放本地音视频 基础demo
前言
个人学习项目,有很多不成熟的地方,期待讨论指正:https://github.com/yang0yyan/MediaPlayer2/tree/dev
首先解码(硬解码),然后播放视频和音频,实现音视频播放,并在此基础上实现在横竖屏切换和后台播放功能,进度条实时显示和进度跳转,倍速快放和慢放
播放结束后点击播放重头开始播放,
支持暂停继续
只讲方法,个人见解
Android音视频开发,主要分为两部分:解码和播放。
介绍如何解码(硬解码),然后播放视频和音频,在此基础上实现在播放音视频过程中横竖屏切换和后台播放功能。
本章介绍如何用MediaCodec(硬)解码,后面会介绍FFmpeg实现软解码和硬解码(涉及到NDK开发)
MediaCodec是Android提供的用于对音视频进行编解码的类,解码后获得音频和视频原始数据,再使用AudioTrack和SurfaceView分别播放音频和视频,视频播放分为原生开发和NDK开发两种
音频和视频播放最重要的是将音视频同步,同步思路:https://hanshuliang.blog.youkuaiyun.com/article/details/104891200
一、MediaCodec硬解码
主要涉及两个类:MediaExtractor和MediaCodec
MediaExtractor负责解析媒体数据,拿到未解码的压缩数据,MediaCodec则将压缩数据解码为原始数据。
MediaExtractor:媒体提取器,从数据源中提取解复用的媒体数据。引用网络文件时,需要android.Manifest.permission#INTERNET权限
MediaFormat:封装描述媒体数据格式的信息,无论是音频还是视频,以及可选的特征元数据
MediaCodec:媒体编解码器
MediaCodec解码数据,将音频数据解码为PCM格式数据,将视频数据解码为NV12格式数据(指定为NV12格式),输入压缩数据,输出PCM格式原始音频数据和NV12格式(需指定)原始视频数据
流程:
- 通过MediaCodecList获取手机支持的音视频硬解码格式
- 分离出音视频文件中的音频和视频
- 获取音频、视频格式,分别判断是否支持硬解码
- 获取音频、视频相关参数,并设置硬解码异步回调
- 通过异步回调解码,onInputBufferAvailable中输入压缩数据,onOutputBufferAvailable输出解码的原始数据
- 音视频同步实现暂停播放(以一个外部时钟为基准)
0. 获取手机支持硬解码的音频、视频格式
private List<String> videoDecoderInfos = new ArrayList<>();
private List<String> audioDecoderInfos = new ArrayList<>();
private void getMediaCodecList() {
videoDecoderInfos.clear();
audioDecoderInfos.clear();
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] mediaCodecInfos = mediaCodecList.getCodecInfos();
for (int i = mediaCodecInfos.length - 1; i >= 0; i--) {
MediaCodecInfo codecInfo = mediaCodecInfos[i];
if (!codecInfo.isEncoder()) {
for (String t : codecInfo.getSupportedTypes()) {
if (t.startsWith("video/")) {
videoDecoderInfos.add(t);
} else if (t.startsWith("audio/")) {
audioDecoderInfos.add(t);
}
}
}
}
Log.d(TAG, "getMediaCodecList: " + videoDecoderInfos.toString());
Log.d(TAG, "getMediaCodecList: " + audioDecoderInfos.toString());
}
例:
支持的视频格式:[video/x-vnd.on2.vp9, video/x-vnd.on2.vp9, video/x-vnd.on2.vp8, video/x-vnd.on2.vp8, video/mp4v-es, video/mp4v-es, video/hevc, video/hevc, video/3gpp, video/3gpp, video/avc, video/avc, video/av01, video/x-vnd.on2.vp9, video/x-vnd.on2.vp8, video/mp4v-es, video/mpeg2, video/hevc, video/3gpp, video/divx4, video/divx, video/avc]
支持的音频格式:[audio/vorbis, audio/raw, audio/opus, audio/opus, audio/mpeg, audio/g711-mlaw, audio/g711-alaw, audio/flac, audio/amr-wb, audio/3gpp, audio/mp4a-latm, audio/vorbis, audio/raw, audio/mpeg, audio/gsm, audio/g711-mlaw, audio/g711-alaw, audio/flac, audio/amr-wb, audio/3gpp, audio/mp4a-latm, audio/flac]
1. 获取音频和视频
public void readMedia(Uri uri) {
MediaExtractor mediaExtractor = new MediaExtractor();
try {
mediaExtractor.setDataSource(context, uri, null);
// 获取通道数
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/") && videoSupport != 1) {
readVideo(mediaFormat, i);
} else if (mime.startsWith("audio/") && audioSupport != 1) {
readAudio(mediaFormat, i);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
mediaExtractor.release();
}
}
2. 获取视频参数并设置解码异步回调
获取视频参数
public void readVideo(MediaFormat mediaFormat, int index) throws IOException {
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
// 判断媒体格式是否支持硬解码
if (!videoDecoderInfos.contains(mime) || audioSupport == -1) {
videoSupport = -1;
return;
}
videoSupport = 1;
//surface上所需的顺时针旋转的视频的度数
rotation = 0;
if (mediaFormat.containsKey(MediaFormat.KEY_ROTATION)) {
rotation = mediaFormat.getInteger(MediaFormat.KEY_ROTATION);
}
// 视频帧数
int videoSampleRateInHz = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
// 宽高
videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
// 视频时长
videoDurationUs = mediaFormat.getLong(MediaFormat.KEY_DURATION);
//设置解码输出格式为NV12(YUV420SemiPlanar)
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
if (null != videoMediaExtractor){
videoMediaExtractor.release();
videoMediaExtractor = null
}
videoMediaExtractor = new MediaExtractor();
videoMediaExtractor.setDataSource(context, fileUri, null);
// 选择视频通道
videoMediaExtractor.selectTrack(index);
// 创建解码器
videoMediaCodec = MediaCodec.createDecoderByType(mime);
// 设置异步回调,输入解码的数据,输出解码的NV12数据
videoMediaCodec.setCallback(videoCallback, new Handler(videoThread.childLooper));
//videoMediaCodec.configure(mediaFormat, null, null, 0);
// 设置surface,用以显示图像,无须手动写入
videoMediaCodec.configure(mediaFormat, surface, null, 0);
}
解码异步回调
private MediaCodec.Callback videoCallback = new MediaCodec.Callback() {
byte[] data;
// 输入解码的数据
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int inIndex) {
// 获取输入缓存区
ByteBuffer inBuffer = codec.getInputBuffer(inIndex);
// 读取压缩数据
int size = videoMediaExtractor.readSampleData(inBuffer, 0);
if (size < 0) {
codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(inIndex, 0, size, videoMediaExtractor.getSampleTime(), 0);
videoMediaExtractor.advance();
inBuffer.clear();
}
}
// 输出解码的NV12数据
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int outIndex, @NonNull MediaCodec.BufferInfo outBufferInfo) {
// 判断是否读完
if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
codec.releaseOutputBuffer(outIndex, true);
releaseVideo();
return;
}
// 添加图像延时
if (outBufferInfo.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
sleep(outBufferInfo.presentationTimeUs / 1000 - (System.currentTimeMillis() - startMs));
}
// 获取解码后的NV12视频数据
// if (outBufferInfo.size > 0) {
// // 获取输出缓存区
// ByteBuffer outBuffer = codec.getOutputBuffer(outIndex);
// outBuffer.position(outBufferInfo.offset);
// outBuffer.limit(outBufferInfo.offset + outBufferInfo.size);
// if (data == null)
// data = new byte[outBufferInfo.size];
// Arrays.fill(data, (byte) 0);
// outBuffer.get(data);
// mediaDecodeCallback.onVideoOutput(data);
// outBuffer.clear();
// }
// videoMediaCodec.configure(mediaFormat, surface, null, 0);
// 在configure中设置了surface,在调用releaseOutputBuffer时会自动显示图像
// 释放输出缓存区
codec.releaseOutputBuffer(outIndex, true);
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
releaseVideo();
releaseAudio();
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
}
};
注意释放资源
private void releaseVideo() {
if (null != videoMediaCodec) {
videoMediaCodec.stop();
videoMediaCodec.release();
videoMediaCodec = null;
}
if (null != videoMediaExtractor) {
videoMediaExtractor.release();
videoMediaExtractor = null;
}
}
3. 获取音频参数并设置解码异步回调
public void readAudio(MediaFormat mediaFormat, int index) throws IOException {
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
// 采样率
sampleRateInHz = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
// 音频数据的格式
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
if (mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
audioFormat = mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING);
}
// 音频通道
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
if (channelCount == 1) {
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
}
// 音频时长
audioDurationUs = mediaFormat.getLong(MediaFormat.KEY_DURATION);
if (null != audioMediaExtractor){
audioMediaExtractor.release();
audioMediaExtractor = null;
}
audioMediaExtractor = new MediaExtractor();
audioMediaExtractor.setDataSource(context, fileUri, null);
// 选择视频通道
audioMediaExtractor.selectTrack(index);
audioMediaCodec = MediaCodec.createDecoderByType(mime);
// 异步回调
audioMediaCodec.setCallback(audioCallback, new Handler(audioThread.childLooper));
audioM