ExoPlayer 源码阅读小记--HLS播放带缓存加载M3U8文件过程

基于ExoPlayer 2.17.1源码分析,基本是一边看一边写的流水账,记录下防止以后忘了:

第一步createMediaSource创建HlsMediaSource对象时同时会实例化出HlsPlaylistTracker.Factory

mediaSource = new HlsMediaSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir, uerAgent))
                    .setAllowChunklessPreparation(true)
                    .setPlaylistParserFactory(new HlsPlaylistParserFactory())
                    .createMediaSource(mediaItem);
    public HlsMediaSource createMediaSource(MediaItem mediaItem) {
      checkNotNull(mediaItem.localConfiguration);
      HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory;
      List<StreamKey> streamKeys = mediaItem.localConfiguration.streamKeys;
      if (!streamKeys.isEmpty()) {
        playlistParserFactory =
            new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
      }

      return new HlsMediaSource(
          mediaItem,
          hlsDataSourceFactory,
          extractorFactory,
          compositeSequenceableLoaderFactory,
          drmSessionManagerProvider.get(mediaItem),
          loadErrorHandlingPolicy,
          playlistTrackerFactory.createTracker(
              hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),//实例化TrackerFactory
          elapsedRealTimeOffsetMs,
          allowChunklessPreparation,
          metadataType,
          useSessionKeys);
    }

第二步调用prepare(),在准备阶段将m3u8文件下载并解析

     player.prepare();
//ExoPlayerImpl
public void prepare() {
   ....
    internalPlayer.prepare();
   ......
  }
//ExoPlayerImplInternal
  private void prepareInternal() {
   ....
    mediaSourceList.prepare(bandwidthMeter.getTransferListener());
  ....
  }
//MediaSourceList
  public void prepare(@Nullable TransferListener mediaTransferListener) {
    ....
    for (int i = 0; i < mediaSourceHolders.size(); i++) {
      MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
      prepareChildSource(mediaSourceHolder);
      enabledMediaSourceHolders.add(mediaSourceHolder);
    }
    ....
  }
  
  private void prepareChildSource(MediaSourceHolder holder) {
   .....
    mediaSource.prepareSource(caller, mediaTransferListener, playerId);
  }
//BaseMediaSource
public final void prepareSource(
      MediaSourceCaller caller,
      @Nullable TransferListener mediaTransferListener,
      PlayerId playerId) {
   ....
      prepareSourceInternal(mediaTransferListener);
  ....
  }
//HlsMediaSource
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
....
    playlistTracker.start(
        localConfiguration.uri, eventDispatcher, /* primaryPlaylistListener= */ this);
  }

到这里使用HlsPlaylistTracker,这个类主要是管理播放列表(控制加载解析流程),主列表叫primary playlist,另外该类还能获取当前播放的列表快照,该类入口就在这个start方法

//DefaultHlsPlaylistTracker
public void start(
      Uri initialPlaylistUri,
      EventDispatcher eventDispatcher,
      PrimaryPlaylistListener primaryPlaylistListener) {
   ....
    ParsingLoadable<HlsPlaylist> multivariantPlaylistLoadable =
        new ParsingLoadable<>(
            dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
            initialPlaylistUri,
            C.DATA_TYPE_MANIFEST,
            playlistParserFactory.createPlaylistParser());
    Assertions.checkState(initialPlaylistLoader == null);
    initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MultivariantPlaylist");//创建m3u8文件的网络加载解析线程
    long elapsedRealtime =
        initialPlaylistLoader.startLoading(
            multivariantPlaylistLoadable,
            this,
            loadErrorHandlingPolicy.getMinimumLoadableRetryCount(
                multivariantPlaylistLoadable.type));
   .....
  }

ParsingLoadable主要代码就这个方法,先通过DataSource获取需要加载的文件流,将文件流传入解析器解析,这里说下DataSourceInputStream本质是InputStream子类,但是里面包含一个datasSource和dataSpec,datasSource可以理解为数据源访问工具(如网络数据源访问Okhttp)获取到数据流后会将流保存在datasSource以供读取,dataSpec用来标记当前数据范围,里面还封装了读取数据的方式参数(如http请求url和header)

//ParsingLoadable
  public final void load() throws IOException {
    // We always load from the beginning, so reset bytesRead to 0.
    dataSource.resetBytesRead();
    DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
    try {
      inputStream.open();
      Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri());
      result = parser.parse(dataSourceUri, inputStream);
    } finally {
      Util.closeQuietly(inputStream);
    }
  }

当DataSourceInputStream调用open时,就会通过 dataSource.open(dataSpec)
当DataSourceInputStream,reader时就读取dataSource的数据流

//DataSourceInputStream
 private void checkOpened() throws IOException {
    if (!opened) {
      dataSource.open(dataSpec);
      opened = true;
    }
  }
  public int read(byte[] buffer, int offset, int length) throws IOException {
    Assertions.checkState(!closed);
    checkOpened();
    int bytesRead = dataSource.read(buffer, offset, length);
    if (bytesRead == C.RESULT_END_OF_INPUT) {
      return -1;
    } else {
      totalBytesRead += bytesRead;
      return bytesRead;
    }
  }

好了我们继续open流程,StatsDataSource是一个封装DataSource的DataSource,里面包含一个DataSource 重定向的 uri 和响应标头等数据

//StatsDataSource
  public long open(DataSpec dataSpec) throws IOException {
    // Reassign defaults in case dataSource.open throws an exception.
    lastOpenedUri = dataSpec.uri;
    lastResponseHeaders = Collections.emptyMap();
    long availableBytes = dataSource.open(dataSpec);
    lastOpenedUri = Assertions.checkNotNull(getUri());
    lastResponseHeaders = getResponseHeaders();
    return availableBytes;
  }

继续调用StatsDataSource封装的DataSource,继续套娃,套娃一般是为了分层,因为设置的是CacheDataSource这里会调用TeeDataSource 的open,Tees翻译就是三通的意思,这是个一进二出的三通,一个水龙头冷水进,一路进小厨宝缓存加热后出,一路直接水龙出,

//TeeDataSource
public long open(DataSpec dataSpec) throws IOException {
    bytesRemaining = upstream.open(dataSpec);//调用OkHttpDataSource open打开冷水进水阀门,准备一路上出水到水龙头,一路到小厨宝缓存
    if (bytesRemaining == 0) {
      return 0;
    }
    if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) {
      // Reconstruct dataSpec in order to provide the resolved length to the sink.
      dataSpec = dataSpec.subrange(0, bytesRemaining);
    }
    dataSinkNeedsClosing = true;
    dataSink.open(dataSpec);//dataSink由CacheDataSource设置,调用CacheDataSink open打开厨宝的进水阀,准备下出水到小厨宝缓存
    return bytesRemaining;
  }

OkHttpDataSource 就忽略了,就是访问网络获取m3u8的文件流,这里看下CacheDataSink ,openNextOutputStream主要就是创建缓存的目录,创建缓存文件并获取输出流,这里并没有写入数据

//CacheDataSink 
public void open(DataSpec dataSpec) throws CacheDataSinkException {
   ....
    try {
      openNextOutputStream(dataSpec);
    } catch (IOException e) {
      throw new CacheDataSinkException(e);
    }
  }
private void openNextOutputStream(DataSpec dataSpec) throws IOException {
    ....
    file =
        cache.startFile(
            castNonNull(dataSpec.key), dataSpec.position + dataSpecBytesWritten, length);
    FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);
    if (bufferSize > 0) {
      if (bufferedOutputStream == null) {
        bufferedOutputStream =
            new ReusableBufferedOutputStream(underlyingFileOutputStream, bufferSize);
      } else {
        bufferedOutputStream.reset(underlyingFileOutputStream);
      }
      outputStream = bufferedOutputStream;
    } else {
      outputStream = underlyingFileOutputStream;
    }
  ....
  }

那么在哪里什么时机写入缓存文件了呢,通过上面open的流程知道,open的流也没有立即读取而是缓存在DataSource里,当调用DataSource的read方法才会读取,所以理所应当写入缓存的时机就在读取的时候
,回到TeeDataSource read方法,具体的read时间点在下面会讲到,我们继续往下看

//TeeDataSource
  public int read(byte[] buffer, int offset, int length) throws IOException {
    if (bytesRemaining == 0) {
      return C.RESULT_END_OF_INPUT;
    }
    int bytesRead = upstream.read(buffer, offset, length);
    if (bytesRead > 0) {
      // TODO: Consider continuing even if writes to the sink fail.这里还有个TODO作者想要缓存写入失败的时候也不要影响读取
      dataSink.write(buffer, offset, bytesRead);
      if (bytesRemaining != C.LENGTH_UNSET) {
        bytesRemaining -= bytesRead;
      }
    }
    return bytesRead;
  }

缓存相关先不展开说了,读取看完继续看解析,这里没啥好说的需要熟悉m3u8文件的格式,返回一个解析后的数据结构

//HlsPlaylistParser
public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    Queue<String> extraLines = new ArrayDeque<>();
    String line;
    try {
      if (!checkPlaylistHeader(reader)) {//这里校验M3U8文件头,有些不规范的M3U8文件会包含BOM头校验会不通过,可以在创建MediaSource时通过setPlaylistParserFactory设置自定义HlsPlaylistParser修改支持BOM头
        throw ParserException.createForMalformedManifest(
            /* message= */ "Input does not start with the #EXTM3U header.", /* cause= */ null);
      }
      while ((line = reader.readLine()) != null) {
        line = line.trim();
        if (line.isEmpty()) {
          // Do nothing.
        } else if (line.startsWith(TAG_STREAM_INF)) {
          extraLines.add(line);
          return parseMultivariantPlaylist(new LineIterator(extraLines, reader), uri.toString());
        } else if (line.startsWith(TAG_TARGET_DURATION)
            || line.startsWith(TAG_MEDIA_SEQUENCE)
            || line.startsWith(TAG_MEDIA_DURATION)
            || line.startsWith(TAG_KEY)
            || line.startsWith(TAG_BYTERANGE)
            || line.equals(TAG_DISCONTINUITY)
            || line.equals(TAG_DISCONTINUITY_SEQUENCE)
            || line.equals(TAG_ENDLIST)) {
          extraLines.add(line);
          return parseMediaPlaylist(
              multivariantPlaylist,
              previousMediaPlaylist,
              new LineIterator(extraLines, reader),
              uri.toString());
        } else {
          extraLines.add(line);
        }
      }
    } finally {
      Util.closeQuietly(reader);
    }
    throw ParserException.createForMalformedManifest(
        "Failed to parse the playlist, could not identify any tags.", /* cause= */ null);
  }

解析完成又会回到DefaultHlsPlaylistTracker的onLoadCompleted,会获取刚才的额解析结果

//DefaultHlsPlaylistTracker
public void onLoadCompleted(
      ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
    HlsPlaylist result = loadable.getResult();//获取解析结果
  .......
    MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
    if (isMediaPlaylist) {
      // We don't need to load the playlist again. We can use the same result.
      primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo);
    } else {
      primaryBundle.loadPlaylist();
    }
  ....
  }

private void processLoadedPlaylist(
        HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) {
      @Nullable HlsMediaPlaylist oldPlaylist = playlistSnapshot;
    ....
      if (playlistSnapshot != oldPlaylist) {
        playlistError = null;
        lastSnapshotChangeMs = currentTimeMs;
        onPlaylistUpdated(playlistUrl, playlistSnapshot);
      } ....
      }

  private void onPlaylistUpdated(Uri url, HlsMediaPlaylist newSnapshot) {
    if (url.equals(primaryMediaPlaylistUrl)) {
 ....
      primaryMediaPlaylistSnapshot = newSnapshot;
      primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);//首次加载primary
    }
....
  }

playlist获取后就会交友MediaSource类处理,MediaSource 有两个主要职责:

  1. 为播放器提供Timeline(定义了媒体结构),当在媒体结构发生变化时提供新的Timeline。MediaSource 通过调用MediaSource.MediaSourceCaller.onSourceInfoRefreshed来提供这些时间线。
  2. 为其时间线中的Period提供MediaPeriod实例。MediaPeriods 是通过调用createPeriod获得的,并为播放器提供了一种加载和读取媒体的方式。关于Timeline
//HlsMediaSource
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist) {
    ....
    SinglePeriodTimeline timeline =
        playlistTracker.isLive()
            ? createTimelineForLive(
                mediaPlaylist, presentationStartTimeMs, windowStartTimeMs, manifest)
            : createTimelineForOnDemand(
                mediaPlaylist, presentationStartTimeMs, windowStartTimeMs, manifest);//Timeline在这里创建
    refreshSourceInfo(timeline);
  }
//BaseMediaSource
  protected final void refreshSourceInfo(Timeline timeline) {
    this.timeline = timeline;
    for (MediaSourceCaller caller : mediaSourceCallers) {
      caller.onSourceInfoRefreshed(/* source= */ this, timeline);
    }
  }
//CompositeMediaSource
protected final void prepareChildSource(@UnknownNull T id, MediaSource mediaSource) {
   ,....
    MediaSourceCaller caller =
        (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline);//调用此处prepare时注册的caller
  ....
  }
//MaskingMediaSource
protected void onChildSourceInfoRefreshed(
      Void id, MediaSource mediaSource, Timeline newTimeline) {
    @Nullable MediaPeriodId idForMaskingPeriodPreparation = null;
    ....
    if (idForMaskingPeriodPreparation != null) {
      Assertions.checkNotNull(unpreparedMaskingMediaPeriod)
          .createPeriod(idForMaskingPeriodPreparation);//创建HlsMediaPeriod
    }
  }
public void createPeriod(MediaPeriodId id) {
....
    if (callback != null) {
      mediaPeriod.prepare(/* callback= */ this, preparePositionUs);
    }
  }

HlsMediaPeriod用来加载Timeline里的Period

//HlsMediaPeriod
  public void prepare(Callback callback, long positionUs) {
    this.callback = callback;
    playlistTracker.addListener(this);
    buildAndPrepareSampleStreamWrappers(positionUs);
  }


private void buildAndPrepareSampleStreamWrappers(long positionUs) {
....
  
    for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) {
      sampleStreamWrapper.continuePreparing();
    }
   ....
  }

HlsSampleStreamWrapper包含一个HlsChunkSource用来获取HlsMediaChunk进行加载提供出SampleStream,
Chunk可以理解为数据块,这里对应一个ts文件,通过ChunkSource获取Chunk执行加载

//HlsSampleStreamWrapper
  public void continuePreparing() {
    if (!prepared) {
      continueLoading(lastSeekPositionUs);
    }
  }
public boolean continueLoading(long positionUs) {
....
    chunkSource.getNextChunk(
        positionUs,
        loadPositionUs,
        chunkQueue,
        /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(),
        nextChunkHolder);
....
    @Nullable Chunk loadable = nextChunkHolder.chunk;
   ....
    long elapsedRealtimeMs =
        loader.startLoading(//加载获取到的chunk,这里可以看到将回调函数设置在了HlsSampleStreamWrapper,后面加载完成后会回调它的onLoadCompleted方法
            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
  ....
    return true;
  }

这里看下获取下一个chunk 的代码,其实就是从解析的playerlist里通过获取Segment创建出对应的chunk

//HlsChunkSource
public void getNextChunk(
      long playbackPositionUs,
      long loadPositionUs,
      List<HlsMediaChunk> queue,
      boolean allowEndOfStream,
      HlsChunkHolder out) {
  ....
    // Select the track.
    MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);
    trackSelection.updateSelectedTrack(
        playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);
  ....
    @Nullable
    HlsMediaPlaylist playlist =
        playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);//通过Tracker获取当前播放的列表
....
    Pair<Long, Integer> nextMediaSequenceAndPartIndex =
        getNextMediaSequenceAndPartIndex(
            previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);//获取下一个Sequence也就是m3u8里的下一个片段索引
    long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
    int partIndex = nextMediaSequenceAndPartIndex.second;
   ....
    @Nullable
    SegmentBaseHolder segmentBaseHolder =
        getNextSegmentHolder(playlist, chunkMediaSequence, partIndex);//获取信息
   ....
   //创建通过获取的Segment创建出对应的chunk
    out.chunk =
        HlsMediaChunk.createInstance(
            extractorFactory,
            mediaDataSource,
            playlistFormats[selectedTrackIndex],
            startOfPlaylistInPeriodUs,
            playlist,
            segmentBaseHolder,
            selectedPlaylistUrl,
            muxedCaptionFormats,
            trackSelection.getSelectionReason(),
            trackSelection.getSelectionData(),
            isTimestampMaster,
            timestampAdjusterProvider,
            previous,
            /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
            /* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
            shouldSpliceIn,
            playerId);
  }

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

//HlsMediaChunk
public void load() throws IOException {
   ....
    if (!loadCanceled) {
      if (!hasGapTag) {
        loadMedia();
      }
      loadCompleted = !loadCanceled;
    }
}

private void loadMedia() throws IOException {
    feedDataToExtractor(
        dataSource, dataSpec, mediaSegmentEncrypted, /* initializeTimestampAdjuster= */ true);
}

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;
}

到这里第一个ts文件已经加载,读取流,完成后会回到HlsSampleStreamWrapper这个类的onLoadCompleted方法,后续的ts文件加载会在播放器主loop里循环判断是否要继续加载,如果判断需要继续加载,最终调用continueLoading实现加载下一个ts文件。最具调用判断过程后续会讲到。

//HlsSampleStreamWrapper
public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
    loadingChunk = null;
    chunkSource.onChunkLoadCompleted(loadable);
....
    if (!prepared) {
      continueLoading(lastSeekPositionUs);
    } else {
      callback.onContinueLoadingRequested(this);
    }
  }

至此HLS的m3u8文件加载解析以及ts文件读取的大致流程就完成了

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值