ExoPlayer 漫谈之Renderer

2,653 阅读3分钟

一个视频由声音轨道和视频轨道组成,一般声音轨道的数据比较小,我们一般不需要担心声音解析的问题.但是视频轨道的数据很大,视频轨道的解码就是整个视频解码的瓶颈。

Render执行流程: 音频和视频解码默认都是使用的MediaCodec,视频解码放在Render:MediaCodecVideoRenderer中完成,音频解码放在MediaCodecAudioRenderer中完成。

  • 1.MediaCodec有同步和异步两种方式,ExoPlayer中使用的是同步还是异步的方式? ExoPlayer中使用的是同步的方式,同步的方式不用设置MediaCodec.setCallback(....)回调,依赖codec.dequeueInputBuffer和codec.dequeueOutputBuffer获取可用的input buffer和output buffer。 这里的input buffer就是不断从Codec中取出解码好的数据,然后放到output buffer中,送出去显示出来。
  • 2.核心的处理方法是MediaCodecRenderer中的render函数:
  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
    if (pendingOutputEndOfStream) {
      pendingOutputEndOfStream = false;
      processEndOfStream();
    }
    try {
      if (outputStreamEnded) {
        renderToEndOfStream();
        return;
      }
      if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) {
        // We still don't have a format and can't make progress without one.
        return;
      }
      // We have a format.
      maybeInitCodec();
      if (codec != null) {
        long drainStartTimeMs = SystemClock.elapsedRealtime();
        TraceUtil.beginSection("drainAndFeed");
        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
        while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {}
        TraceUtil.endSection();
      } else {
        decoderCounters.skippedInputBufferCount += skipSource(positionUs);
        // We need to read any format changes despite not having a codec so that drmSession can be
        // updated, and so that we have the most recent format should the codec be initialized. We
        // may also reach the end of the stream. Note that readSource will not read a sample into a
        // flags-only buffer.
        readToFlagsOnlyBuffer(/* requireFormat= */ false);
      }
      decoderCounters.ensureUpdated();
    } catch (IllegalStateException e) {
      if (isMediaCodecException(e)) {
        throw createRendererException(e, inputFormat);
      }
      throw e;
    }
  }

maybeInitCodec根据mimetype创建codec,然后将surface配置到codec内部,之后的output buffers数据会不断送显到这个surface上. drainOutputBuffer消耗codec中的output buffers队列中的数据; feedInputBuffer填充codec中的input buffers队列.

shouldContinueFeeding(drainStartTimeMs)函数一看是作音视频同步用的,下一章我们分析音视频同步,本章我们只将音视频解码的流程以及其中的关键点. 关于丢帧的讨论都在音视频同步中分析

视频的送显直接使用在MediaCodec.configure中设置surface就可以的,音频的播放还是要借助AudioTrack或者OpenSL ES,当然ExoPlayer中使用的是AudioTrack

音频播放

初始化定义Audio Render的时候已经设置了Audio的渲染入口: DefaultRenderersFactory中buildAudioRenderers 函数

    out.add(
        new MediaCodecAudioRenderer(
            context,
            mediaCodecSelector,
            drmSessionManager,
            playClearSamplesWithoutKeys,
            enableDecoderFallback,
            eventHandler,
            eventListener,
            new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));

关注最后一个参数DefaultAudioSink对象. AudioProcessor是处理音频的基类,拓展他实现众多音频处理的方式. DefaultAudioSink.initialize函数中初始化一个AudioTrack对象:

    audioTrack =
        Assertions.checkNotNull(configuration)
            .buildAudioTrack(tunneling, audioAttributes, audioSessionId);

在这个函数的下面有一个AudioTrackPositionTracker对象,这个对象是音频同步的辅助类,非常重要.

    audioTrackPositionTracker.setAudioTrack(
        audioTrack,
        configuration.outputEncoding,
        configuration.outputPcmFrameSize,
        configuration.bufferSize);

MediaCodecAudioRenderer.processOutputBuffer中将解码出来的原始音频数据送到AudioSink中渲染:这是一个循环的过程,不断地从output buffers中去除解码出的原始数据.

      if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) {
        codec.releaseOutputBuffer(bufferIndex, false);
        decoderCounters.renderedOutputBufferCount++;
        return true;
      }

DefaultAudioSink.handleBuffer中处理的核心方法块是:

    if (configuration.processingEnabled) {
      processBuffers(presentationTimeUs);
    } else {
      writeBuffer(inputBuffer, presentationTimeUs);
    }

将解码出来的原始数据写道AudioTrack中:

    if (Util.SDK_INT < 21) { // isInputPcm == true
      // Work out how many bytes we can write without the risk of blocking.
      int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes);
      if (bytesToWrite > 0) {
        bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
        bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
        if (bytesWritten > 0) {
          preV21OutputBufferOffset += bytesWritten;
          buffer.position(buffer.position() + bytesWritten);
        }
      }
    } else if (tunneling) {
      Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
      bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining,
          avSyncPresentationTimeUs);
    } else {
      bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
    }
audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);

AudioTrack的阻塞类型都是WRITE_NON_BLOCKING,不能影响播放线程的进行

AudioTrack写入buffer的时候会明确告知系统当前的buffer的timestamp,在播放时候会根据这个timestamp来控制播放的进度.

  • ExoPlayer中关于Render这一块很多工业代码,但是不妨碍我们对整体流程的把握
  • 音视频这一块的核心功能还是音视频同步

问题: 如何引入其他的Audio或者Video Render模块? 请思考一下,以后揭晓