一、前言
众所周知,ExoPlayer播放架构中,默认使用MediaCodec框架去解码和渲染。但实际上ExoPlayer作为一款开源播放器,具备强大的扩展能力,其本身还支持解码器扩展和渲染器扩展。比如可以使用ExoPlayer + Ffmpeg实现音视频解码和播放,同时也支持vp9、av1、flac等解码器和渲染器。因此,作为开发者,对ExoPlayer的学习不应该局限于MediaCodec的使用。
综上所说,在使用ExoPlayer时,你的选择范围很大,当然这点也取决于你对ExoPlayer的熟悉程度。
我们知道,MediaCodec支持两种模式——Buffer模式(兼容性好)和Surface模式(性能好),但是ExoPlayer中的使用MediaCodec视频解码时仅支持Surface模式,这种可能是出于性能考虑。
但是有一些比较特殊的情况,需要对画面加工、检测调试,或者提高兼容性的考虑,需要实现Buffer模式。
1.1 意义
ExoPlayer中,视频解码部分,出于性能原因,MediaCodec不支持Buffer模式,即便不传入Surface,其内部也会创建PlaceHolderSurface用于兜底。
但是实现Buffer模式的方式也是有多种的,最简单的是通过ImageReader去实现YUV读取,但是作为开发者,仍然要做的是需要设置Color-Format的,不然有些设备无法拿到YUV数据.
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatYUV420Flexible);
不过,本篇我们会对ExoPlayer进行改造,这里我们应该思考,我们对于ExoPlayer的改造意义何在呢?
相比而言,ImageReader的性能会稍微差一些,实现流程也比较复杂,如果将ImageReader的数据用于渲染,这个链路和流程相比也会多一点。
因此选择直接处理,反而性能可以有所保证,这就是我们改造ExoPlayer而不是使用imageReader的原因。
1.2 目标
我们这里就不用ImageReader或者egl的GetPixels方式了,这里我们选择使用MediaCodec#getOutputBuffer后直接处理数据,使得ExoPlayer中的MediaCodec既能支持Surface模式,又能支持Buffer模式。
二、渲染器和解码器扩展
2.1 约束
ExoPlayer内部提供了扩展解码器的一些约束和规范
顶层规范是com.google.android.exoplayer2.BaseRenderer,其内部约定了基础的调用流程,次一级的DecoderVideoRenderer和DecoderAudioRenderer,提供了常用的渲染器扩展流程。比较经典的是vp9和ffmpeg的实现,具体demo可以参考下面的实现。
com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer
当然,官方的扩展中并不包含FfmpegVideoRenderer的实现,当然更早期的ExoPlayer有Ffmpeg视频解码的实现,后来完全删除了,可能原因和视频解码的开源协议(LGPL)有关,因此,这部需求可能需要自行实现。
2.2 输出模式
在ExoPlayer音频解码本身就是Buffer模式,但是对于视频而言,这点有所区别,我们知道,MediaCode视频解码支持两种模式,Buffer模式和Surface模式,区别是MediaCodec#configure(…)方法中有没有传入Surface,有的话就是Surface模式,没有就是Buffer模式。Surface模式时MediaCodec#getOutputBuffer(…)拿到的Buffer中的所有数据都是“0”填充的。
当然ExoPlayer内部也有定义了相关标记
public static final int VIDEO_OUTPUT_MODE_NONE = -1;
public static final int VIDEO_OUTPUT_MODE_YUV = 0;
public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;
但是ExoPlayer只支持MediaCodecVideoRenderer只Surface模式,那么如果要实现Buffer模式支持,该如何做呢?
2.3 扩展方案
我们前面说过,使用DecoderVideoRenderer就是实现视频解码的模式扩展,这种方法理论上是可以的,但是官方做提供的MediaCodecVideoRenderer做了很多相关的优化,如果单纯使用DecoderVideoRenderer去实现,会发现有很多重复性的冗余工作,而且SimpleDecoder适配起来反而有些复杂和啰嗦。
因此,这里我们建议改造MediaCodecVideoRenderer,但是作为官方的代码,虽然继承其可以实现自己的Renderer,但是仍然不够巧妙,毕竟有些逻辑依赖了Surface。
我们这里直接复制一份MediaCodecVideoRenderer代码,命名成MediaCodecVideoAdaptiveRenderer,在其基础上改造。
三、逻辑
3.1 定义变量
首先新增两个变量,用于保存要输出到的目标,这里我们沿用官方的VideoDecoderOutputBufferRenderer,其主要实现子类是VideoDecoderGLSurfaceView,该组件主要通过YUV数据进行UI渲染。
我们MediaCodecVideoAdaptiveRenderer类中添加如下代码
@Nullable private Object output;
@Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer;
3.2 改造setOutput方法
默认的该方法只支持setOutput,我们对其进行修改,使得其支持VideoDecoderOutputBufferRenderer
调整MediaCodecVideoAdaptiveRenderer的setOutput方法。
private void setOutput(@Nullable Object output) throws ExoPlaybackException {
@Nullable Surface surface = null;
@Nullable VideoDecoderOutputBufferRenderer outputBufferRenderer = null;
if(output instanceof Surface){
surface = (Surface) output;
outputBufferRenderer = null;
outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV;
}else if(output instanceof VideoDecoderOutputBufferRenderer){
surface = null;
outputBufferRenderer = (VideoDecoderOutputBufferRenderer) output;
outputMode = C.VIDEO_OUTPUT_MODE_YUV;
}else{
output = null;
surface = null;
outputBufferRenderer = null;
outputMode = C.VIDEO_OUTPUT_MODE_NONE;
}
this.output = output;
if (surface == null && outputBufferRenderer == null) {
if (placeholderSurface != null) {
surface = placeholderSurface;
} else {
MediaCodecInfo codecInfo = getCodecInfo();
if (codecInfo != null && shouldUsePlaceholderSurface(codecInfo)) {
placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure);
surface = placeholderSurface;
outputMode = C.VIDEO_OUTPUT_MODE_YUV;
}
}
}
if(this.videoDecoderOutputBufferRenderer != outputBufferRenderer){
this.videoDecoderOutputBufferRenderer = outputBufferRenderer;
maybeRenotifyVideoSizeChanged();
maybeRenotifyRenderedFirstFrame();
}
}
2.3 支持Color-Format
如果MediaCodec不用Surface渲染,那么就是Buffer模式,然而,这里有个和ImageReader#getSurface都可能出现的问题,就是部分设备读取不到合适的YUV数据,因此,在Buffer模式下,需要设置Color-Format。
调整MediaCodecVideoAdaptiveRenderer的getMediaFormat方法。
protected MediaFormat getMediaFormat(
Format format,
String codecMimeType,
CodecMaxValues codecMaxValues,
float codecOperatingRate,
boolean deviceNeedsNoPostProcessWorkaround,
int tunnelingAudioSessionId) {
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatYUV420Flexible);
}
2.4 渲染
下面我们修改两个渲染方法,使得MediaCodec#在Buffer模式时丢帧,防止无法渲染。
另外我们需要定义一个方法onDrainOutputBuffer用于处理Buffer数据
protected void renderOutputBuffer(MediaCodecAdapter codec,ByteBuffer buffer, int index, long presentationTimeUs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
if(outputMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV){
codec.releaseOutputBuffer(index, true);
}else{
onDrainOutputBuffer(codec, buffer, index, presentationTimeUs);
codec.releaseOutputBuffer(index, false);
}
TraceUtil.endSection();
lastRenderRealtimeUs = SystemClock.elapsedRealtime() * 1000;
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
}
@RequiresApi(21)
protected void renderOutputBufferV21(
MediaCodecAdapter codec, ByteBuffer buffer, int index, long presentationTimeUs, long releaseTimeNs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
if(outputMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV){
codec.releaseOutputBuffer(index, releaseTimeNs);
}else{
onDrainOutputBuffer(codec, buffer, index, presentationTimeUs);
codec.releaseOutputBuffer(index, false);
}
TraceUtil.endSection();
lastRenderRealtimeUs = SystemClock.elapsedRealtime() * 1000;
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
}
接下来我们实现一下onDrainOutputBuffer,下面有所区别的是,有些设备解析处理的是YUV420P,有些是YUV420SP,如果是YUV420SP (可能是Nv21)我们需要进行处理,将其转为YUV420P (I420格式)。另外,下面的代码中我们还需要注意一点的是,如果使用VideoDecoderGLSurfaceView渲染是需要将数据封装成VideoDecoderOutputBuffer的,但是VideoDecoderOutputBuffer内部的data是Direct Buffer,而不是Heap Buffer,因此这里我直接将数据塞入了,防止其创建DirectBuffer。因此而言,这部分显然有性能问题,后续再优化吧。
private final BufferPool byteBufferPool = new BufferPool("BufferMode",3,false);
private void onDrainOutputBuffer(MediaCodecAdapter codec,ByteBuffer outputBuffer, int index, long presentationTimeUs) {
if(outputBuffer != null){
MediaFormat outputFormat = codec.getOutputFormat();
int colorFormat = outputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
int width = outputFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = outputFormat.getInteger(MediaFormat.KEY_HEIGHT);
int alignWidth = width;
int alignHeight = height;
int stride = outputFormat.getInteger(MediaFormat.KEY_STRIDE);
int sliceHeight = outputFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
if (stride > 0 && sliceHeight > 0) {
alignWidth = stride;
alignHeight = sliceHeight;
}
alignWidth = alignTo16(alignWidth);
alignHeight = alignTo16(alignHeight);
Buffer yuvDataBuffer = byteBufferPool.obtain(outputBuffer.remaining());
outputBuffer.get(yuvDataBuffer.getBuffer());
yuvDataBuffer.setDataSize(outputBuffer.remaining());
switch (colorFormat){
case CodecCapabilities.COLOR_FormatYUV420Planar:
case CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
break;
case CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
Buffer yuvData420P = byteBufferPool.obtain(alignWidth * alignHeight * 3 / 2);
YuvTools.yuv420spToYuv420P(yuvDataBuffer.getBuffer(),yuvData420P.getBuffer(),alignWidth,alignHeight);
yuvData420P.setDataSize(alignWidth * alignHeight * 3 / 2);
yuvDataBuffer.recycle();
yuvDataBuffer = yuvData420P;
break;
}
VideoDecoderOutputBuffer decoderOutputBuffer = new VideoDecoderOutputBuffer(new DecoderOutputBuffer.Owner() {
@Override
public void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer) {
}
});
decoderOutputBuffer.init(presentationTimeUs,C.VIDEO_OUTPUT_MODE_YUV,null);
boolean isDebug = false;
byte[] yuvData = yuvDataBuffer.getBuffer();
decoderOutputBuffer.data = ByteBuffer.allocateDirect(yuvDataBuffer.effectiveSize);
decoderOutputBuffer.data.put(yuvData);
decoderOutputBuffer.initForYuvFrame(width,height,stride,stride / 2,0);
if(isDebug) {
Bitmap bitmap = YuvTools.toBitmap(yuvData,width, height);
Log.d(TAG,"Bitmap = " + bitmap);
}
yuvDataBuffer.recycle();
VideoDecoderOutputBufferRenderer bufferRenderer = videoDecoderOutputBufferRenderer;
if(bufferRenderer != null){
bufferRenderer.setOutputBuffer(decoderOutputBuffer);
}
outputBuffer = null;
}
}
private int alignTo16(int value) {
return (value + 15) & (~15);
}
2.5 问题补充
这里一些关注点,改造时可能遇到的问题,方便大家阅读。
2.5.1 colorSpace
initForYuvFrame方法最后一个参数是colorspace,用于调整画质,可以理解为色彩的饱和度、亮度等调整,这里我们无法从MediaCodec拿到这个,这个参数传入0,直接使用COLORSPACE_BT709画质即可。
2.5.2 VideoDecoderGLSurfaceView
这个是官方的YUV渲染实现,代码就不贴出来了
2.5.3 COLOR_FormatYUV420Flexible
设置的是COLOR_FormatYUV420Flexible,为什么解码出来时420sp或者420p呢?
主要是COLOR_FormatYUV420Flexible是用于兼容原有格式,原来的格式google都废弃掉了,还有个原因是一些解码器并不会因为你设置了例如COLOR_FormatYUV420Planar就会给你COLOR_FormatYUV420Planar,因此官方最终统一了实现逻辑。
2.5.3 Buffer帧完整性
可能有人会比较疑惑,解码后帧是不是完整的,实际上解码后的是完整的帧,并不是B帧或者P帧,因此每一帧都可以看做是IDR帧
2.5.3 YUV数据校验
很多时候,对于处理一些转换逻辑,需要查验帧的正确性,这个时候就需要转成Bitmap,当然也有更好的工具,不过大部分收费。
四、使用
下面我们将MediaCodecVideoAdaptiveRenderer接入播放器内部
4.1 接入
上面的核心逻辑实现了,那么怎么才能接入呢?
这里我们需要改造
com.google.android.exoplayer2.RenderersFactory代码,当然,继承DefaultRenderersFactory更加方便
@Override
protected void buildVideoRenderers(Context context,
@ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector,
boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener,
long allowedVideoJoiningTimeMs, ArrayList out) {
MediaCodecVideoAdaptiveRenderer videoRenderer =
new MediaCodecVideoAdaptiveRenderer(
context,
getCodecAdapterFactory(),
mediaCodecSelector,
allowedVideoJoiningTimeMs,
enableDecoderFallback,
eventHandler,
eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
out.add(videoRenderer);
}
通过下面方法接入到播放器内部
private void setRenderersFactory(
ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) {
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory( this, preferExtensionDecoders);
playerBuilder.setRenderersFactory(new DemoDefaultRendererFactory(getApplicationContext()));
}
4.2 效果
下面是渲染效果,同样seek操作也是不会影响的,画面渲染还可,也不见得很卡。
五、总结
好了,本篇主要内容就到这里,实际上我们讲解的比较粗略,主要是篇幅内容太多,不适合学习。其实一方面我们实现了ExoPlayer+MediaCodec视频解码Buffer模式支持,另一方面我们可以看到ExoPlayer高度的可扩展性,相比而言,非常适合Android开发者学习。
通过本篇我们了解MediaCodec、ExoPlayer一些模式,其实MediaCodec和Ffmpeg本质上是同一级别的多媒体框架,而ExoPlayer属于产品几遍了,后续我们实现下ExoPlayer+Ffmpeg视频解码,方便大家进一步对比MediaCodec。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21171,转载请注明出处。
评论0