ExoPlayer架构详解与源码分析(15)——Renderer

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor
ExoPlayer架构详解与源码分析(10)——H264Reader
ExoPlayer架构详解与源码分析(11)——DataSource
ExoPlayer架构详解与源码分析(12)——Cache
ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource
ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod
ExoPlayer架构详解与源码分析(15)——Renderer
ExoPlayer架构详解与源码分析(16)——LoadControl
ExoPlayer架构详解与源码分析(17)——TrackSelector



前言

大家都在电影院看过电影,以前的电影采用胶卷拍摄,剪辑(物理意义上的),复制,然后再送到各个影院播放。
默片时代胶卷只包含图像信息,所以放映机不需要考虑同步问题,但这也不意味着不需要音画同步。大家经常看的猫和老鼠就是默片,你会说明明就有声音,其实那些背景交响乐都是交先乐团人工现场演奏的,指挥盯着电影屏幕根据预设指挥演奏。这里可以看成是最原始的音画同步,人工智能实时同步,实时纠错。
网图侵删
后来有声电影出现,曾经风光一时的交响乐演奏从业者大批失业(历史不断重演,像不像现在的AI)。有声电影的胶卷不光承载了视频信息还承载了音频信息,早期的音频信息就写在胶卷的侧面如下图的模拟声轨,声轨也是物理意义上的声轨。胶片投影的位置下方会有一个光电传感器,将图像声轨信息转化成电信号最终放大出声音。这个光电传感器是可以移动位置的,胶卷的两侧孔洞叫做齿孔,故名思意就是齿轮会卡在上面,齿孔与放映机的齿轮啮合,以恒定的速度移动胶片保证音画同步。某些情况下会发生音画不同步的情况,这个时候通过调整声轨光电传感器的位置来微调,离胶片投影越远声音就延后,越近声音就提前。

在这里插入图片描述
数字时代在模拟声轨基础上有了数字声轨,下图的杜比声轨可以想象成一个二维码里面存储了声音的数字信息,通过读取解码就可以获取当前的声音,同时为了保证音画同步也不需要以前那么复杂了,胶卷上同时还存储了DTS时间相关信息,声轨中同样也有时间戳,数字播放机通过比较两者保证时间同步。
现在大部分电影院都是数字片使用播放软硬件播放胶卷不复存在(除了最近还整出了个IMAX胶片的诺兰),计算机时代,随手一部手机就可以解4K电影,软件播放器就可以保证音画同步,ExoPlayer当然也可以,这主要归功于今天要说的另一大组件Renderer,可以看出电影历史的一个切面就是音画同步史,而Renderer的核心内容就是同步。

如果你已经看完理解了前面MediaSource的内容,我相信你已经知道数据是如何获取并解析好放入到缓存了,我们先跳过中间那些控制管理环节,这些数据最终流入的方向就是Renderer。可以把Renderer想象成火箭的涡轮发动机,从MediaSource那源源不断的获取燃料,在发动机里点火燃烧,为火箭升空提供强大的动力。和火箭一样要想升空发动机必须平稳,在火箭运行的不同时间精确的执行预设好的动作。这就需要一个良好的时间同步设计。

Renderer

渲染从SampleStream读取的媒体。
在内部,渲染器的生命周期由所属的ExoPlayer管理。随着整体播放状态和启用的轨道的变化,渲染器会在各种状态之间转换。有效的状态转换如下所示,并用每次转换期间调用的方法进行注释。
在这里插入图片描述
看下主要方法

  • init 初始化Renderer,入参index为当前Renderer在所有Renderer中的索引,入参playerId为当前播放器的ID
  • enable 使渲染器能够使用传入的SampleStream,当Renderer 处于Disabled状态是才可能被调用
  • start 启动渲染器,这意味着对render的调用将导致媒体被渲染。当Renderer 处于Enable状态时才能调用此方法
  • render 增量渲染SampleStream 。当渲染器处于以下Enable、 Started状态时可以调用此方法 。
  • replaceStream 替换SampleStream 。当渲染器处于以下Enable、 Started状态时可以调用此方法 。

Renderer对象创建完成一般先调init方法初始化,然后调用enable传入SampleStream,enable内部会调用replaceStream初始化SampleStream,之后调用start将状态置为Started,最后调用render方法开始渲染

再看下Renderer模块的整体结构
在这里插入图片描述
Renderer直接由抽象类BaseRenderer实现,下面的MediaCodecRenderer(音视频)、TextRenderer(字幕)、MetadataRenderer(Meta信息)、CameraMotionRenderer(镜头信息,用于VR全景之类的数据)对应各种类型轨道的渲染器。本文篇幅有限,主要介绍音视频也就是MediaCodecRenderer,其他的Renderer感兴趣的可以自行研究。可以看到MediaCodecRenderer下又分视频Video和音频Audio两大块,视频最终交给Android系统的MediaCodec来处理,而音频最终交由Android系统的AudioTrack处理。

BaseRenderer

Renderer的直接实现类,主要用于一些状态的控制存储,和一些全局变量的管理

  @Override
  public final void init(int index, PlayerId playerId) {
   
    this.index = index;
    this.playerId = playerId;
  }
  
  @Override
  public final void enable(
      RendererConfiguration configuration,//renderer配置信息
      Format[] formats,//轨道信息
      SampleStream stream,//待渲染数据
      long positionUs,//当前播放位置
      boolean joining,//是否启用此渲染器来加入正在进行的播放
      boolean mayRenderStartOfStream,//即使状态尚未STATE_STARTED ,是否允许此渲染器渲染流的开头。
      long startPositionUs,//渲染的开始位置
      long offsetUs)//在渲染之前添加到从stream读取的缓冲区时间戳的偏移量。
      throws ExoPlaybackException {
   
    Assertions.checkState(state == STATE_DISABLED);
    this.configuration = configuration;
    state = STATE_ENABLED;
    onEnabled(joining, mayRenderStartOfStream);//调用子类
    replaceStream(formats, stream, startPositionUs, offsetUs);
    resetPosition(positionUs, joining);
  }
  
  @Override
  public final void replaceStream(
      Format[] formats, SampleStream stream, long startPositionUs, long offsetUs)
      throws ExoPlaybackException {
   
    Assertions.checkState(!streamIsFinal);
    this.stream = stream;//替换当前的全局SampleSteam
    if (readingPositionUs == C.TIME_END_OF_SOURCE) {
   
      readingPositionUs = startPositionUs;
    }
    streamFormats = formats;
    streamOffsetUs = offsetUs;
    onStreamChanged(formats, startPositionUs, offsetUs);//子类实现
  }
  
  @Override
  public final void start() throws ExoPlaybackException {
   
    Assertions.checkState(state == STATE_ENABLED);
    state = STATE_STARTED;//改变状态
    onStarted();//子类实现
  }
  //BaseRenderer还提供了readSource方法,用于读取Sample中的数据
  //readFlags知道当前需要获取的数据类型
  protected final @ReadDataResult int readSource(
      FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
   
    @ReadDataResult
    //这里的stream最终从上文讲的SampleQueue中获取数据
    int result = Assertions.checkNotNull(stream).readData(formatHolder, buffer, readFlags);
    if (result == C.RESULT_BUFFER_READ) {
   //当前获取的是BUFFER数据
      if (buffer.isEndOfStream()) {
   
        readingPositionUs = C.TIME_END_OF_SOURCE;
        return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
      }
      buffer.timeUs += streamOffsetUs;
      readingPositionUs = max(readingPositionUs, buffer.timeUs);
    } else if (result == C.RESULT_FORMAT_READ) {
   //当前获取的是Format数据
      Format format = Assertions.checkNotNull(formatHolder.format);
      if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
   
        format =
            format
                .buildUpon()
                .setSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs)
                .build();
        formatHolder.format = format;
      }
    }
    return result;
  }

BaseRenderer的实现比较简单,重点看下它的子类,这里主要学习下MediaCodecRenderer,在看MediaCodecRenderer前得先了解下Android系统的MediaCodec。

MediaCodec

MediaCodec 可用于访问低级媒体编解码器,即编码器/解码器组件。它是 Android 低级多媒体支持基础设施的一部分,这里主要了解下解码过程,不知道还有没有读者记得这张在讲SampleQueue里出现的图了
在这里插入图片描述
解码器的作用就是处理输入的编码数据输出解码后的数据。它使用一组输入和输出缓冲区异步处理数据。首先,调用者初始化MediaCodec.configure,向MediaCodec.dequeueInputBuffer请求一个空的输入缓冲区,将其他地方读取到的编码数据填充输入缓冲区,queueInputBuffer将其发送给MediaCodec进行处理。MediaCodec获取的输入缓冲数据进行解码,完成后将解码后的数据输出至输出缓冲区。最后,调用者向MediaCodec.dequeueOutputBuffer请求已填充的输出缓冲区,调用者将获取输出缓冲区的解码数据将其渲染到指定地方,使用完成后releaseOutputBuffer将其释放回MediaCodec。如果在MediaCodec.configure传入了Surface,releaseOutputBuffer后会将解码数据直接渲染到传入的Surface上。

在MediaCodec生命周期中,存在以下三种状态:Stopped、Executing 、 Released。 Stopped 状态实际上是三个状态的组合:Uninitialized、Configured 和 Error,而 Executing 状态会经历三个子状态:Flushed、Running 和 End-of-Stream。
在这里插入图片描述
当创建MediaCodec时,编解码器处于Uninitialized状态。首先,您需要通过configure对其进行配置,这会将其置于Configured 状态,然后调用start将其置于Executing 状态。在Executing 状态下,就可以通过上述缓冲区队列操作来处理数据了。
Executing 状态具有三个子状态:Flushed、Running 和 End-of-Stream。在 start之后,MediaCodec立即处于 Flushed 子状态,其中保存所有缓冲区。一旦第一个输入缓冲区出队,编解码器就会进入Running 子状态,大部分时间会执行在此状态下。当使用BUFFER_FLAG_END_OF_STREAM Flag标记进行MediaCodec.queueInputBuffer时,MediaCodec将转换到End-of-Stream子状态。在此状态下,编解码器不再接受更多输入缓冲区,但仍生成输出缓冲区,直到输出到达流末尾。对于解码器,可以在处于 Executing 状态时随时使用 flash返回到 Flushed 子状态。
调用 stop 将编解码器返回到Uninitialized状态,然后可以再次configure它。使用完编解码器后,必须通过调用release来释放它。
有了上面的知识,来看看看MediaCodecRenderer是如何使用这些方法,完成整个渲染的。

MediaCodecRenderer

MediaCodecRenderer主要是通过Android的MediaCodec来渲染解码渲染出音视频内容,主要有2个子类MediaCodecVideoRenderer和MediaCodecAudioRenderer。直接看下render的实现

  @Override
  //positionUs为当前的播放时间戳,如果有音轨会获取音轨的PTS
  //elapsedRealtimeUs循环调用render开始前的时间戳,组要用来计算程序的执行时长
  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
   
    ...
      // We have a format.
      maybeInitCodecOrBypass();
      //如果是直出的也就是不需要Codec解码的数据
      if (bypassEnabled) {
   
        TraceUtil.beginSection("bypassRender");
        //
        while (bypassRender(positionUs, elapsedRealtimeUs)) {
   }
        TraceUtil.endSection();
      } else if (codec != null) {
   //需要通过MeidaCodec解码的数据
      //记录循环开始时间,用于计算执行时间是否超过renderTimeLimitMs,决定是否继续循环
        long renderStartTimeMs = SystemClock.elapsedRealtime();
        TraceUtil.beginSection("drainAndFeed");
        //先从MediaCodec中获取已解码数据
        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
            && shouldContinueRendering(renderStartTimeMs)) {
   }
        //向MediaCodec输入待解码数据
        while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {
   }
        TraceUtil.endSection();
      }
     ...
  }
  
  protected final void maybeInitCodecOrBypass() throws ExoPlaybackException {
   
   ...

    if (isBypassPossible(inputFormat)) {
   
      initBypass(inputFormat);//对于不需要Codec解码的数据直接,初始化Bypass主要就是初始化Byapass的buffer:bypassBatchBuffer
      return;
    }
   ...
      //初始化MediaCodec
      maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
    ...
  }
  private void maybeInitCodecWithFallback(
      @Nullable MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)
      throws DecoderInitializationException {
   
    if (availableCodecInfos == null) {
   
      try {
   
        //通过输入的数据的Meta信息获取用于初始化Codec的相关数据
        List<MediaCodecInfo> allAvailableCodecInfos =
            getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);
       ...
    }

    ...
          //开始初始化Codec
          initCodec(codecInfo, crypto);
...
  }
  
  private void initCodec(MediaCodecInfo codecInfo, @Nullable MediaCrypto crypto) throws Exception {
   
    //通过子类获取MediaCodecAdapter.Configuration
    MediaCodecAdapter.Configuration configuration =
        getMediaCodecConfiguration(codecInfo, inputFormat, crypto, codecOperatingRate);
    ...
    //创建出MediaCodecAdapter,这里的Adapter主要有2个实现,一个是SynchronousMediaCodecAdapter 通过同步的方式调用MediaCodec,一个是针对API23的异步MeidaCodec调用的AsynchronousMediaCodecAdapter
    try {
   
      TraceUtil.beginSection("createCodec:" + codecName);
      codec = codecAdapterFactory.createAdapter(configuration);
    } finally {
   
      TraceUtil.endSection();
    }
    ...
  }
  //这里为了方便看下SynchronousMediaCodecAdapter 
  public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException {
   
      @Nullable MediaCodec codec = null;
      try {
   
        //主要通过MediaCodec.createByCodecName(codecName)创建出MediaCodec
        codec = createCodec(configuration);
        TraceUtil.beginSection("configureCodec");
        //配置MediaCodec
        codec.configure(
        //格式,其中KEY_MAX_INPUT_SIZE确定了缓冲区的大小,对应于format.maxInputSize,可以查看计算逻辑
            configuration.mediaFormat,
            configuration.surface,//渲染的surface
            configuration.crypto,
            configuration.flags);
        TraceUtil.endSection();
        TraceUtil.beginSection("startCodec")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值