ExoPlayer 源码阅读小记--音视频数据流分析

基于ExoPlayer 2.17.1源码分析,分析下音视频数据是如何到播放器播放的:

首先话接上上篇ExoPlayer 源码阅读小记–HLS播放带缓存加载M38U文件过程最后一步

MediaChunk执行chunk加载,首先通过prepareExtraction创建出input数据流和HlsMediaChunkExtractor执行器,然后通过执行器执行相应的流,流的这块后续处理还是比较复杂的不是本文重点,主要目的就是ts文件的Demux,解析出Demux(对应代码里的Sample,相当于一帧)后的数据发送到一个DataQuene里供Renderer获取,Renderer最终调用系统的MediaCodec渲染到surface上

//HlsMediaChunk
private void feedDataToExtractor(
      DataSource dataSource,
      DataSpec dataSpec,
      boolean dataIsEncrypted,
      boolean initializeTimestampAdjuster)
      throws IOException {
....
    try {
      ExtractorInput input =
          prepareExtraction(dataSource, loadDataSpec, initializeTimestampAdjuster);
      if (skipLoadedBytes) {
        input.skipFully(nextLoadPosition);
      }
      try {
        while (!loadCanceled && extractor.read(input)) {}//通过执行器执行相应的流
      } catch (EOFException e) {
        ....
}

private DefaultExtractorInput prepareExtraction(
      DataSource dataSource, DataSpec dataSpec, boolean initializeTimestampAdjuster)
      throws IOException {
    long bytesToRead = dataSource.open(dataSpec);//这里又看到熟悉的代码类似ParsingLoadable加载m3u8文件一样,调用我们的三通TeeDataSource从网络获取ts文件,数据流会保存在datasource等待读取,读取时缓存文件
   ....
    DefaultExtractorInput extractorInput =
        new DefaultExtractorInput(dataSource, dataSpec.position, bytesToRead);//数据流

    if (extractor == null) {
....
      extractor =//执行器
          previousExtractor != null
              ? previousExtractor.recreate()
              : extractorFactory.createExtractor(
                  dataSpec.uri,
                  trackFormat,
                  muxedCaptionFormats,
                  timestampAdjuster,
                  dataSource.getResponseHeaders(),
                  extractorInput,
                  playerId);
   ....
    return extractorInput;
}

现在我们来详细分析下上面说的Renderer过程,继续往下看extractor read的过程,这里我们播放是ts文件,这个个read最终会使用TsExtractor的reader,这里主要就是对TS文件的解析,具体需要参照TS文件的结构看代码,这里就不在赘述,代码每次读取ts的一个块也就是188bytes,然后分析头和payload,继续看payload部分payloadReader.consume

//TsExtractor
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
      throws IOException {
    long inputLength = input.getLength();
....
    if (!fillBufferWithAtLeastOnePacket(input)) {
      return RESULT_END_OF_INPUT;
    }
....
 TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null;
 ....
    // Read the payload.
    boolean wereTracksEnded = tracksEnded;
    if (shouldConsumePacketPayload(pid)) {
      tsPacketBuffer.setLimit(endOfPacket);
      payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
      tsPacketBuffer.setLimit(limit);
    }
....
    return RESULT_CONTINUE;
  }

首次payloadReader get不到会使用SectionReader consume分析段信息,通过PatReader或者PmtReader 读取PAT和PMT表,最终目的就是查找出音视频数据具体参考TS文件个格式,这里我们直接进入到视频数据的读取,由于我们是采用的H.264标准的视频,最终会通过PesReader consume 调用H264Reader 的 consume 方法

//PesReader
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
    Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called.

    if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
      switch (state) {
       ....
    while (data.bytesLeft() > 0) {
      switch (state) {
        case STATE_FINDING_HEADER:
          data.skipBytes(data.bytesLeft());
          break;
        case STATE_READING_HEADER:
          if (continueRead(data, pesScratch.data, HEADER_SIZE)) {
            setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);
          }
          break;
        case STATE_READING_HEADER_EXTENSION:
          int readLength = min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);
          // Read as much of the extended header as we're interested in, and skip the rest.
          if (continueRead(data, pesScratch.data, readLength)
              && continueRead(data, /* target= */ null, extendedHeaderLength)) {
            parseHeaderExtension();
            flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
            reader.packetStarted(timeUs, flags);
            setState(STATE_READING_BODY);
          }
          break;
        case STATE_READING_BODY:
          readLength = data.bytesLeft();
          int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize;
          if (padding > 0) {
            readLength -= padding;
            data.setLimit(data.getPosition() + readLength);
          }
          reader.consume(data);//读取body
          if (payloadSize != C.LENGTH_UNSET) {
            payloadSize -= readLength;
            if (payloadSize == 0) {
              reader.packetFinished();
              setState(STATE_READING_HEADER);
            }
          }
          break;
        default:
          throw new IllegalStateException();
      }
    }
  }

H264Reader 会通过output将数据加到SampleDataQueue的buffer里,另外还会扫描附加数据,处理AL单元,获取当前视频帧的数据信息

//H264Reader
public void consume(ParsableByteArray data) {
    assertTracksCreated();

    int offset = data.getPosition();
    int limit = data.limit();
    byte[] dataArray = data.getData();

    // Append the data to the buffer.
    totalBytesWritten += data.bytesLeft();
    output.sampleData(data, data.bytesLeft());

    // Scan the appended data, processing NAL units as they are encountered
    while (true) {
      int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);

      if (nalUnitOffset == limit) {
        // We've scanned to the end of the data without finding the start of another NAL unit.
        nalUnitData(dataArray, offset, limit);
        return;
      }

      // We've seen the start of a NAL unit of the following type.
      int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);

      // This is the number of bytes from the current offset to the start of the next NAL unit.
      // It may be negative if the NAL unit started in the previously consumed data.
      int lengthToNalUnit = nalUnitOffset - offset;
      if (lengthToNalUnit > 0) {
        nalUnitData(dataArray, offset, nalUnitOffset);
      }
      int bytesWrittenPastPosition = limit - nalUnitOffset;
      long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
      // Indicate the end of the previous NAL unit. If the length to the start of the next unit
      // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
      // when notifying that the unit has ended.
      endNalUnit(
          absolutePosition,
          bytesWrittenPastPosition,
          lengthToNalUnit < 0 ? -lengthToNalUnit : 0,
          pesTimeUs);
      // Indicate the start of the next NAL unit.
      startNalUnit(absolutePosition, nalUnitType, pesTimeUs);
      // Continue scanning the data.
      offset = nalUnitOffset + 3;
    }
  }

最终通过output的sampleMetadata写入每个Sample的timeUs,size,offset,cryptoData等信息,SampleQueue内部管理着SampleDataQueue(也就是视频的帧数据),SharedSampleMetadata 、offsets、sizes、flags、timesUs(数据对应的Metadata信息),具体关系在下面读取的时候会讲到。

//SampleQueue
public void sampleMetadata(
      long timeUs,
      @C.BufferFlags int flags,
      int size,
      int offset,
      @Nullable CryptoData cryptoData) {
    if (upstreamFormatAdjustmentRequired) {
      format(Assertions.checkStateNotNull(unadjustedUpstreamFormat));
    }

    boolean isKeyframe = (flags & C.BUFFER_FLAG_KEY_FRAME) != 0;
    if (upstreamKeyframeRequired) {
      if (!isKeyframe) {
        return;
      }
      upstreamKeyframeRequired = false;
    }

    timeUs += sampleOffsetUs;
    if (upstreamAllSamplesAreSyncSamples) {
      if (timeUs < startTimeUs) {
        // If we know that all samples are sync samples, we can discard those that come before the
        // start time on the write side of the queue.
        return;
      }
      if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
        // The flag should always be set unless the source content has incorrect sample metadata.
        // Log a warning (once per format change, to avoid log spam) and override the flag.
        if (!loggedUnexpectedNonSyncSample) {
          Log.w(TAG, "Overriding unexpected non-sync sample for format: " + upstreamFormat);
          loggedUnexpectedNonSyncSample = true;
        }
        flags |= C.BUFFER_FLAG_KEY_FRAME;
      }
    }
    if (pendingSplice) {
      if (!isKeyframe || !attemptSplice(timeUs)) {
        return;
      }
      pendingSplice = false;
    }

    long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset;
    commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
  }

最终调用SampleDataQueue的sampleData,首先介绍下SampleDataQueue,首先它是一个队列,确切的说应该是它内部维护了一个队列,队列每一项都是一个AllocationNode,AllocationNode有个next属性用来指向下一项,这个队列是滚动的,队尾不停的放入新的数据也就是下面的sampleData

//SampleDataQueue
public void sampleData(ParsableByteArray buffer, int length) {
    while (length > 0) {
      int bytesAppended = preAppend(length);
      buffer.readBytes(
          writeAllocationNode.allocation.data,
          writeAllocationNode.translateOffset(totalBytesWritten),
          bytesAppended);
      length -= bytesAppended;
      postAppend(bytesAppended);
    }
  }
 private int preAppend(int length) {
    if (writeAllocationNode.allocation == null) {//分配空间当前Node空间,并实列化下一个Node
      writeAllocationNode.initialize(
          allocator.allocate(),//DefaultAllocator 分配空间
          new AllocationNode(writeAllocationNode.endPosition, allocationLength));
    }
    return min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
  }
  
    private void postAppend(int length) {
    totalBytesWritten += length;//更新已写大小
    if (totalBytesWritten == writeAllocationNode.endPosition) {//如果当前Node写满了,将当前写入Node指向下一个
      writeAllocationNode = writeAllocationNode.next;
    }
  }

这里可以了解下DefaultAllocator这个类,这是一个内存buffer分配器,负责分配和管理内存,当已分配的内存不在使用时(如已经被Renderer读取)播放器在dosomework 内部loop时会释放掉这块内存,最终会调用release“释放掉”,但是并不是正在意义上的释放只是将当前内存块放入到availableAllocations 这个List里保存,供allocate时重复循环利用,这么看SampleDataQueue也可以理解成一个环形队列。另外我们看到这个DefaultAllocator在内存分配的时候并没有做限制,当分配内存已经超过APP最大可用内存时这里就会OOM。

//DefaultAllocator
public synchronized Allocation allocate() {
    allocatedCount++;
    Allocation allocation;
    if (availableCount > 0) {//如果当前还有可用内存,使用当前
      allocation = Assertions.checkNotNull(availableAllocations[--availableCount]);
      availableAllocations[availableCount] = null;
    } else {//没有足够的剩余内存则创建并扩容List大小为原先2倍
      allocation = new Allocation(new byte[individualAllocationSize], 0);
      if (allocatedCount > availableAllocations.length) {
        // Make availableAllocations be large enough to contain all allocations made by this
        // allocator so that release() does not need to grow the availableAllocations array. See
        // [Internal ref: b/209801945].
        availableAllocations = Arrays.copyOf(availableAllocations, availableAllocations.length * 2);
      }
    }
    return allocation;
  }

看完写入我们再来看读取,读取要从播放器内部loop dosomework看起,这是一个很重要的方法,贯穿了播放器的整个播放过程,这里我们做个分支任务详细分析下这个方法,主要关注updatePeriods方法

//ExoPlayerImplInternal
private void doSomeWork() throws ExoPlaybackException, IOException {
    long operationStartTimeMs = clock.uptimeMillis();
    updatePeriods();//更新Periods

    if (playbackInfo.playbackState == Player.STATE_IDLE
        || playbackInfo.playbackState == Player.STATE_ENDED) {
      // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
      handler.removeMessages(MSG_DO_SOME_WORK);
      return;
    }

    @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
    if (playingPeriodHolder == null) {
      // We're still waiting until the playing period is available.
      scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
      return;
    }

    TraceUtil.beginSection("doSomeWork");

    updatePlaybackPositions();

    boolean renderersEnded = true;
    boolean renderersAllowPlayback = true;
    if (playingPeriodHolder.prepared) {
      long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
      playingPeriodHolder.mediaPeriod.discardBuffer(
          playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
      for (int i = 0; i < renderers.length; i++) {
        Renderer renderer = renderers[i];
        if (!isRendererEnabled(renderer)) {
          continue;
        }
        // TODO: Each renderer should return the maximum delay before which it wishes to be called
        // again. The minimum of these values should then be used as the delay before the next
        // invocation of this method.
        renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
        renderersEnded = renderersEnded && renderer.isEnded();
        // Determine whether the renderer allows playback to continue. Playback can continue if the
        // renderer is ready or ended. Also continue playback if the renderer is reading ahead into
        // the next stream or is waiting for the next stream. This is to avoid getting stuck if
        // tracks in the current period have uneven durations and are still being read by another
        // renderer. See: https://github.com/google/ExoPlayer/issues/1874.
        boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
        boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
        boolean allowsPlayback =
            isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
        renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
        if (!allowsPlayback) {
          renderer.maybeThrowStreamError();
        }
      }
    } else {
      playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
    }
....
  }

private void updatePeriods() throws ExoPlaybackException, IOException {
    if (playbackInfo.timeline.isEmpty() || !mediaSourceList.isPrepared()) {
      // No periods available.
      return;
    }
    maybeUpdateLoadingPeriod();
    maybeUpdateReadingPeriod();
    maybeUpdateReadingRenderers();
    maybeUpdatePlayingPeriod();
  }

看下maybeUpdateLoadingPeriod,首先判断是否需要下一曲,然后判断是否需要继续加载下一个ts文件

//ExoPlayerImplInternal
private void maybeUpdateLoadingPeriod() throws ExoPlaybackException {
    queue.reevaluateBuffer(rendererPositionUs);//上面说的释放掉一些已读的Allocation内存块
    if (queue.shouldLoadNextMediaPeriod()) {//是否需要加载下一个MediaPeriod,根据Timeline定义,这种属于Single media file,包含一个Period,如果是一个播放列表,就对应下一个曲目
      @Nullable
      MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
      if (info != null) {
        MediaPeriodHolder mediaPeriodHolder =
            queue.enqueueNextMediaPeriodHolder(
                rendererCapabilities,
                trackSelector,
                loadControl.getAllocator(),
                mediaSourceList,
                info,
                emptyTrackSelectorResult);
        mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
        if (queue.getPlayingPeriod() == mediaPeriodHolder) {
          resetRendererPosition(info.startPositionUs);
        }
        handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
      }
    }
    if (shouldContinueLoading) {
      // We should still be loading, except when there is nothing to load or we have fully loaded
      // the current period.
      shouldContinueLoading = isLoadingPossible();
      updateIsLoading();
    } else {
      maybeContinueLoading();//决定是否需要继续加载下一个分段到内存,这里对应HLS的一个ts文件
    }
  }
  private void maybeContinueLoading() {
    shouldContinueLoading = shouldContinueLoading();
    if (shouldContinueLoading) {
      queue.getLoadingPeriod().continueLoading(rendererPositionUs);//对应前篇末尾所说的continueLoading
    }
    updateIsLoading();
  }
private boolean shouldContinueLoading() {
    if (!isLoadingPossible()) {
      return false;
    }
    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
    long bufferedDurationUs =
        getTotalBufferedDurationUs(loadingPeriodHolder.getNextLoadPositionUs());
    long playbackPositionUs =
        loadingPeriodHolder == queue.getPlayingPeriod()
            ? loadingPeriodHolder.toPeriodTime(rendererPositionUs)
            : loadingPeriodHolder.toPeriodTime(rendererPositionUs)
                - loadingPeriodHolder.info.startPositionUs;
    return loadControl.shouldContinueLoading(//调用loadcontrol来判断
        playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
  }

其中判断是否加载下一个的逻辑在DefaultLoadControl里,这个可以在创建ExoPlayer时通过setLoadControl方法设置,
这个类主要关注4个属性:

  • targetBufferBytes 最大可用缓存,如果设置为-1,会动态通过当前缓存的轨道类型设置合适的大小,如果是视频轨则大小为2000 * 64*1024 bytes
  • minBufferUs 当前缓存的最小时长,单位微秒
  • maxBufferUs 当前缓存的最大时长,单位微秒
  • prioritizeTimeOverSizeThresholds 是否采用时间优先,设置为true会忽略targetBufferBytes 最大可用缓存

用户可以通过设置以上几个值来制定自己的缓存策略,注意这里说的缓存指的是存储内存中视频的数据,如果设置不当很有可能OOM

//DefaultLoadControl
public boolean shouldContinueLoading(
      long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
    boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes;//判断是否超出最大允许内存,这里获取所有已分配内存使用的就是上面说的DefaultAllocator
    long minBufferUs = this.minBufferUs;
    if (playbackSpeed > 1) {//倍数播放的场景需要重新计算最小缓存时间
      // The playback speed is faster than real time, so scale up the minimum required media
      // duration to keep enough media buffered for a playout duration of minBufferUs.
      long mediaDurationMinBufferUs =
          Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed);
      minBufferUs = min(mediaDurationMinBufferUs, maxBufferUs);
    }
    // Prevent playback from getting stuck if minBufferUs is too small.
    minBufferUs = max(minBufferUs, 500_000);//最小不能低于500000
    if (bufferedDurationUs < minBufferUs) {//比较时间
      isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;//决定是否需要缓存,优先判断时间优先级,时间优先级高的化就直接忽略targetBufferBytes了
      if (!isLoading && bufferedDurationUs < 500_000) {
        Log.w(
            "DefaultLoadControl",
            "Target buffer size reached with less than 500ms of buffered media data.");
      }
    } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
      isLoading = false;
    } // Else don't change the loading state.
    return isLoading;
  }

继续看doSomeWork renderer.render 这里又回到我们主线任务来了,这里是使用renderer 来渲染数据到屏幕,应该就是读取数据的入口的了,数据的解码方式有很多,EXO默认是采用系统来解码的,也就是是MediaCodec,这里直接看MediaCodecRenderer的render

//MediaCodecRenderer
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
   ....
        TraceUtil.beginSection("drainAndFeed");
        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
            && shouldContinueRendering(renderStartTimeMs)) {}
        while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {}//循环读取
        TraceUtil.endSection();
    ....
  }
  
private boolean feedInputBuffer() throws ExoPlaybackException {
....
    @SampleStream.ReadDataResult int result;
    try {
      result = readSource(formatHolder, buffer, /* readFlags= */ 0);
    } catch (InsufficientCapacityException e) {
      onCodecError(e);
      // Skip the sample that's too large by reading it without its data. Then flush the codec so
      // that rendering will resume from the next key frame.
      readSourceOmittingSampleData(/* readFlags= */ 0);
      flushCodec();
      return true;
    }
    ....
 }

protected final @ReadDataResult int readSource(
      FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
    @ReadDataResult
    int result = Assertions.checkNotNull(stream).readData(formatHolder, buffer, readFlags);
....
    return result;
  }
//HlsSampleStream
public int readData(
      FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
   ....
    return hasValidSampleQueueIndex()
        ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, readFlags)
        : C.RESULT_NOTHING_READ;
  }
//HlsSampleStreamWrapper
 public int readData(
      int sampleQueueIndex,
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      @ReadFlags int readFlags) {
   ....
    int result =
        sampleQueues[sampleQueueIndex].read(formatHolder, buffer, readFlags, loadingFinished);
   ....
    return result;
  }

SampleQueue读取数据时并不是直接从sampleDataQueue中读取,而是先调用peekSampleMetadata给buffer设置Metadata相关数据,然后再去从sampleDataQueue里获取帧数据,看下peekSampleMetadata方法,我们发现它是通过synchronized加锁的,而sampleDataQueue的peekToBuffer并没有加锁,到这里应该会理解为什么会先读取Metadata了

//SampleQueue
public int read(
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      @ReadFlags int readFlags,
      boolean loadingFinished) {
    int result =
        peekSampleMetadata(
            formatHolder,
            buffer,
            /* formatRequired= */ (readFlags & FLAG_REQUIRE_FORMAT) != 0,
            loadingFinished,
            extrasHolder);
    if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
      boolean peek = (readFlags & FLAG_PEEK) != 0;
      if ((readFlags & FLAG_OMIT_SAMPLE_DATA) == 0) {
        if (peek) {
          sampleDataQueue.peekToBuffer(buffer, extrasHolder);
        } else {
          sampleDataQueue.readToBuffer(buffer, extrasHolder);
        }
      }
      if (!peek) {
        readPosition++;
      }
    }
    return result;
  }

可以看到SampleData和SampleMetadata之间通过relativeReadIndex来关联,读取数据时先读取SampleMetadata,计算出relativeReadIndex获取到SampleData offset,用于后面的SampleData获取

//SampleQueue
private synchronized int peekSampleMetadata(
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      boolean formatRequired,
      boolean loadingFinished,
      SampleExtrasHolder extrasHolder) {
    buffer.waitingForKeys = false;
    if (!hasNextSample()) {
      if (loadingFinished || isLastSampleQueued) {
        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
        return C.RESULT_BUFFER_READ;
      } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) {
        onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder);
        return C.RESULT_FORMAT_READ;
      } else {
        return C.RESULT_NOTHING_READ;
      }
    }

    Format format = sharedSampleMetadata.get(getReadIndex()).format;
    if (formatRequired || format != downstreamFormat) {
      onFormatResult(format, formatHolder);
      return C.RESULT_FORMAT_READ;
    }

    int relativeReadIndex = getRelativeIndex(readPosition);//通过计算relativeReadIndex获取对应的Metadata信息
    if (!mayReadSample(relativeReadIndex)) {
      buffer.waitingForKeys = true;
      return C.RESULT_NOTHING_READ;
    }

    buffer.setFlags(flags[relativeReadIndex]);
    buffer.timeUs = timesUs[relativeReadIndex];
    if (buffer.timeUs < startTimeUs) {
      buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
    }
    extrasHolder.size = sizes[relativeReadIndex];
    extrasHolder.offset = offsets[relativeReadIndex];//将offset保存到holder中供后面读取SampData用
    extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];

    return C.RESULT_BUFFER_READ;
  }
//SampleDataQueue
public void peekToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
    readSampleData(readAllocationNode, buffer, extrasHolder, scratch);
  }
  private static AllocationNode readSampleData(
      AllocationNode allocationNode,
      DecoderInputBuffer buffer,
      SampleExtrasHolder extrasHolder,
      ParsableByteArray scratch) {
  ....

      // Write the sample data.
      buffer.ensureSpaceForWrite(sampleSize);
      allocationNode = readData(allocationNode, extrasHolder.offset, buffer.data, sampleSize);
      extrasHolder.offset += sampleSize;
      extrasHolder.size -= sampleSize;
....
    return allocationNode;
  }
  private static AllocationNode readData(
      AllocationNode allocationNode, long absolutePosition, ByteBuffer target, int length) {
    allocationNode = getNodeContainingPosition(allocationNode, absolutePosition);
    int remaining = length;
    while (remaining > 0) {
      int toCopy = min(remaining, (int) (allocationNode.endPosition - absolutePosition));
      Allocation allocation = allocationNode.allocation;
      target.put(allocation.data, allocationNode.translateOffset(absolutePosition), toCopy);//获取allocation里的数据
      remaining -= toCopy;
      absolutePosition += toCopy;
      if (absolutePosition == allocationNode.endPosition) {
        allocationNode = allocationNode.next;
      }
    }
    return allocationNode;
  }

到这里数据的读取过程就结束了,最终renderer会将获取到的数据渲染到屏幕上

看完这些感觉Exoplayer这块设计得很巧妙,最大限度的保证了视频的读写效率:

  • SampleQueue内部管理着SampleData,SampleMetadata,通过SampleMetadata来管理SampleData
    SampleData读写是不加锁的,SampleData数据量大,这样可以保证视频数据读写的效率,
    SampleMetadata读写是加锁的,但是metadata数据量太小了,这个锁基本不会影响视频数据读写的效率
  • SampleData通过SampleDataQueue来管理,这是一个动态的Queue,使用多少分配多少空间,不用就会回收再利用,超出大小会自动扩容,比单纯的一块固定大小的buffer要灵活实用很多
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值