vx 搜索『gjzkeyframe』 关注『关键帧Keyframe』来及时获得最新的音视频技术文章。
塞尚《自助餐》 .jpeg这个公众号会路线图 式的遍历分享音视频技术:音视频基础(完成) → 音视频工具(完成) → 音视频工程示例(进行中) → 音视频工业实战(准备)。
iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
过程,并借助音视频工具来分析和理解对应的音视频数据。
在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是 Android 第六篇:Android 音频渲染 Demo。这个 Demo 里包含以下内容:
- 1)实现一个音频解封装模块;
- 2)实现一个音频解码模块;
- 3)实现一个音频渲染模块;
- 4)实现对 MP4 文件中音频部分的解封装和解码逻辑,并将解封装、解码后的数据送给渲染模块播放;
- 5)详尽的代码注释,帮你理解代码逻辑和原理。
如果你想获得全部源码和参与音视频技术讨论,可以知识星球搜索『关键帧的音视频开发圈』加入我们,当然也可以跳过直接看后续的内容。
1、音频解封装模块
在这个 Demo 中,解封装模块 KFMP4Demuxer
的实现与 《Android 音频解封装 Demo》 中一样,这里就不再重复介绍了,其接口如下:
KFMP4Demuxer.java
public class KFMP4Demuxer {
public KFMP4Demuxer(KFDemuxerConfig config, KFDemuxerListener listener); ///< 构造方法 配置 & 回调。
public void release(); ///< 释放解封装器实例。
public boolean hasVideo(); ///< 是否包含视频。
public boolean hasAudio(); ///< 是否包含音频。
public int duration(); ///< 文件时长。
public int rotation(); ///< 视频旋转角度。
public boolean isHEVC(); ///< 是否为 H265。
public int width(); ///< 视频宽度。
public int height(); ///< 视频高度。
public int samplerate(); ///< 音频采样率。
public int channel(); ///< 音频声道数。
public int audioProfile(); ///< 音频 profile。
public int videoProfile(); ///< 视频 profile。
public MediaFormat audioMediaFormat(); ///< 音频格式描述。
public MediaFormat videoMediaFormat(); ///< 视频格式描述。
public ByteBuffer readAudioSampleData(MediaCodec.BufferInfo bufferInfo); ///< 读取音频帧。
public ByteBuffer readVideoSampleData(MediaCodec.BufferInfo bufferInfo); ///< 读取视频帧。
}
2、音频解码模块
同样的,解码模块 KFByteBufferCodec
的实现与 《Android 音频解码 Demo》 中一样,这里就不再重复介绍了,其接口如下:
KFMediaCodecInterface.java
public interface KFMediaCodecInterface {
public static final int KFMediaCodecInterfaceErrorCreate = -2000;
public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
public static final int KFMediaCodecInterfaceErrorStart = -2002;
public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
public static final int KFMediaCodecInterfaceErrorParams = -2004;
public static int KFMediaCodeProcessParams = -1;
public static int KFMediaCodeProcessAgainLater = -2;
public static int KFMediaCodeProcessSuccess = 0;
///< 初始化 Codec,第一个参数需告知使用编码还是解码。
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
///< 释放Codec。
public void release();
///< 获取输出格式描述。
public MediaFormat getOutputMediaFormat();
///< 获取输入格式描述。
public MediaFormat getInputMediaFormat();
///< 处理每一帧数据,编码前与编码后都可以,支持编解码 2 种模式。
public int processFrame(KFFrame frame);
///< 清空 Codec 缓冲区。
public void flush();
}
3、音频渲染模块
接下来,我们来实现一个音频渲染模块 KFAudioRender
,在这里输入解码后的数据进行渲染播放。
KFAudioRenderListener.java
public interface KFAudioRenderListener {
///< 出错回调。
void onError(int error,String errorMsg);
///< 获取PCM数据。
byte[] audioPCMData(int size);
}
上面是 KFAudioRenderListener
接口的设计,主要是有音频渲染数据输入回调
和错误回调
的接口。
这里重点需要看一下音频渲染数据输入回调
接口,系统的音频渲染单元每次会主动通过回调的方式要数据,我们这里封装的 KFAudioRender
则是用数据输入回调
接口来从外部获取一组待渲染的音频数据送给系统的音频渲染单元。
KFAudioRender.java
public class MainActivity extends AppCompatActivity {
private KFDemuxer mDemuxer; ///< 音频解封装
private KFDemuxerConfig mDemuxerConfig; ///< 音频解封装配置
private KFMediaCodecInterface mDecoder; ///< 音频解码
private FileOutputStream mStream = null;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) this,
new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}
mDemuxerConfig = new KFDemuxerConfig();
mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + "/2.mp4";
mDemuxerConfig.demuxerType = KFGLBase.KFMediaType.KFMediaAudio;
if (mStream == null) {
try {
mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.pcm");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
startParams.gravity = Gravity.CENTER_HORIZONTAL;
Button startButton = new Button(this);
startButton.setTextColor(Color.BLUE);
startButton.setText("开始");
startButton.setVisibility(View.VISIBLE);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
///< 创建解封装器与解码器。
if (mDemuxer == null) {
mDemuxer = new KFDemuxer(mDemuxerConfig,mDemuxerListener);
mDecoder = new KFByteBufferCodec();
mDecoder.setup(false,mDemuxer.audioMediaFormat(),mDecoderListener,null);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
///< 循环读取音频帧进入解码器。
while (nextBuffer != null) {
mDecoder.processFrame(new KFBufferFrame(nextBuffer,bufferInfo));
nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
}
mDecoder.flush();
Log.i("KFDemuxer","complete");
}
}
});
addContentView(startButton, startParams);
}
private KFDemuxerListener mDemuxerListener = new KFDemuxerListener() {
@Override
///< 解封装出错回调。
public void demuxerOnError(int error, String errorMsg) {
Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
}
};
private KFMediaCodecListener mDecoderListener = new KFMediaCodecListener() {
@Override
///< 解码出错回调。
public void onError(int error, String errorMsg) {
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
///< 解码数据回调,存储本地。
public void dataOnAvailable(KFFrame frame) {
KFBufferFrame bufferFrame = (KFBufferFrame)frame;
try {
byte[] dst = new byte[bufferFrame.bufferInfo.size];
bufferFrame.buffer.get(dst);
mStream.write(dst);
} catch (IOException e) {
e.printStackTrace();
}
}
};
}
上面是 KFAudioRender
的实现,从代码上可以看到主要有这几个部分:
- 1)创建音频渲染实例。
- 在
_setupAudioTrack
方法中实现,根据采样率、声道、单次输入数据大小 等几个参数生成。
- 在
- 2)处理音频渲染实例的数据回调,并在回调中通过
KFAudioRender
的对外数据输入回调接口向更外层要待渲染的数据。 - 通过
audioPCMData
回调接口向更外层要数据。
- 通过
- 3)实现开始渲染和停止渲染逻辑。
- 分别在
play
和stop
方法中实现。注意,这里是开始和停止操作都是放在串行队列中通过mHandler.post
异步处理的,这里主要是为了防止主线程卡顿。 - 开启播放后会循环向外层获取 PCM 数据,通过
write
方法写入mAudioTrack
。
- 分别在
- 4)清理音频渲染实例。
- 在
release
方法中实现。
- 在
更具体细节见上述代码及其注释。
4、解封装和解码 MP4 文件中的音频部分并渲染播放
我们在一个 MainActivity
中来实现从 MP4 文件中解封装和解码音频数据进行渲染播放。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private KFDemuxer mDemuxer; ///< 音频解封装实例。
private KFDemuxerConfig mDemuxerConfig; ///< 音频解决封装配置。
private KFMediaCodecInterface mDecoder; ///< 音频解码实例。
private KFAudioRender mRender; ///< 音频渲染实例。
private byte[] mPCMCache = new byte[10*1024*1024]; ///< PCM 数据缓存。
private int mPCMCacheSize = 0;
private ReentrantLock mLock = new ReentrantLock(true);
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
///< 获取音频采集、本地存储权限。
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) this,
new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}
///< 创建音频解封装配置。
mDemuxerConfig = new KFDemuxerConfig();
mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + "/test.aac";
mDemuxerConfig.demuxerType = KFGLBase.KFMediaType.KFMediaAudio;
///< 创建音频解封装实例。
mDemuxer = new KFDemuxer(mDemuxerConfig,mDemuxerListener);
mDecoder = new KFByteBufferCodec();
mDecoder.setup(false,mDemuxer.audioMediaFormat(),mDecoderListener,null);
///< 循环获取解封装数据塞入解码器。
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
while (nextBuffer != null) {
mDecoder.processFrame(new KFBufferFrame(nextBuffer,bufferInfo));
nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
}
///< 创建音频渲染实例。
mRender = new KFAudioRender(mRenderListener,mDemuxer.samplerate(),mDemuxer.channel());
mRender.play();
}
private KFDemuxerListener mDemuxerListener = new KFDemuxerListener() {
@Override
///< 解封装出错。
public void demuxerOnError(int error, String errorMsg) {
Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
}
};
private KFMediaCodecListener mDecoderListener = new KFMediaCodecListener() {
@Override
///< 解码出错。
public void onError(int error, String errorMsg) {
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
///< 解码数据回调存储到本地 PCM 缓存,Demo 处理比较简单,没有考虑到渲染暂停解码不暂停等 case,可能存在缓冲区溢出。
public void dataOnAvailable(KFFrame frame) {
KFBufferFrame bufferFrame = (KFBufferFrame)frame;
if (bufferFrame.buffer != null && bufferFrame.bufferInfo.size > 0) {
byte[] bytes = new byte[bufferFrame.bufferInfo.size];
bufferFrame.buffer.get(bytes);
mLock.lock();
System.arraycopy(bytes,0,mPCMCache,mPCMCacheSize,bytes.length);
mPCMCacheSize += bytes.length;
mLock.unlock();
}
}
};
private KFAudioRenderListener mRenderListener = new KFAudioRenderListener() {
@Override
///< 音频渲染出错。
public void onError(int error, String errorMsg) {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
///< 音频播放模块获取音频 PCM 数据。
public byte[] audioPCMData(int size) {
if (mPCMCacheSize >= size) {
byte[] dst = new byte[size];
mLock.lock();
System.arraycopy(mPCMCache,0,dst,0,size);
mPCMCacheSize -= size;
System.arraycopy(mPCMCache,size,mPCMCache,0,mPCMCacheSize);
mLock.unlock();
return dst;
}
return null;
}
};
}
上面是 MainActivity
的实现,其中主要包含这几个部分:
- 1)在页面加载完成后就启动解封装和解码模块,并且循环读取音频数据传递给解码器。
- 在
onCreate
中实现。
- 在
- 2)在解码模块
KFByteBufferCodec
的数据回调中获取解码后的 PCM 数据缓冲起来等待渲染。 - 在
KFMediaCodecListener
的dataOnAvailable
回调中实现。
- 在
- 3)在渲染模块
KFAudioRender
的输入数据回调中把缓冲区的数据交给系统音频渲染单元渲染。 - 在
KFAudioRenderListener
的audioPCMData
回调中实现。
- 在
更具体细节见上述代码及其注释。
网友评论