ExoPlayer MediaCodec视频解码Buffer模式支持

一、前言

众所周知,ExoPlayer播放架构中,默认使用MediaCodec框架去解码和渲染。但实际上ExoPlayer作为一款开源播放器,具备强大的扩展能力,其本身还支持解码器扩展和渲染器扩展。比如可以使用ExoPlayer + Ffmpeg实现音视频解码和播放,同时也支持vp9、av1、flac等解码器和渲染器。因此,作为开发者,对ExoPlayer的学习不应该局限于MediaCodec的使用。

综上所说,在使用ExoPlayer时,你的选择范围很大,当然这点也取决于你对ExoPlayer的熟悉程度。

企业微信20240930-142251@2x.png

我们知道,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操作也是不会影响的,画面渲染还可,也不见得很卡。

fire_175.gif

五、总结

好了,本篇主要内容就到这里,实际上我们讲解的比较粗略,主要是篇幅内容太多,不适合学习。其实一方面我们实现了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

评论0

显示验证码
没有账号?注册  忘记密码?