天天看點

修改ExoPlayer源碼播放hls顯示多音軌

最近在研究哪種播放器能夠支援drm視訊播放,查找了一些資料,比較多的都是推薦谷歌播放器ExoPlayer,測試drm是可以正常播放,但是無法顯示出多音軌,預設都是隻有一條音軌和一條字幕軌,使用了官方提供的測試連結,發現官方的可以正常顯示出來,于是我對比了公司和官方的m3u8檔案,發現官方的會多一個這樣的資訊

#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2218327,BANDWIDTH=2227464,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc1",AUDIO="aud1",SUBTITLES="sub1"
           

也就是#EXT-X-STREAM-INF,這個表示該m3u8流的一些音頻軌、字幕軌等資訊,既然找到了這個差别,那麼在ExoPlayer例子中全局查找關于這個标志,在HlsPlaylistParser這個類中有關于這個标志的定義

private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
           

接下來我們可以找找關于這個标志位的使用

在HlsPlaylistParser類中的parse(Uri uri, InputStream inputStream)函數,有對這個标志位的判斷

if (line.isEmpty()) {
  // Do nothing.
} else if (line.startsWith(TAG_STREAM_INF)) {//有TAG_STREAM_INF的走這裡
  extraLines.add(line);
  return parseMasterPlaylist(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)) {
    //無TAG_STREAM_INF的走這裡(公司的流就是走的這裡)
  extraLines.add(line);
  return parseMediaPlaylist(
      masterPlaylist, new LineIterator(extraLines, reader), uri.toString());
} else {
  extraLines.add(line);
}
           

很明顯,有無TAG_STREAM_INF處理是不一樣的,讓我們看看parseMasterPlaylist這個函數

從上面可以看出該函數定義了關于audios、videos、subtitles等的ArrayList數組,中間省略的就是對m3u8檔案中關于音頻、視訊、字幕軌道資訊的讀取,然後再指派給audios、videos、subtitles,而parseMediaPlaylist函數中則沒有相關音頻、視訊、字幕軌道資訊的讀取

上面好像看不出什麼有用的資訊,那麼我們從音軌隻有一條入手,從選擇軌道的代碼中看起,在TrackSelectionDialog類的createForTrackSelector函數中,有設定軌道的代碼

if (!overrides.isEmpty()) {
  builder.setSelectionOverride(
      /* rendererIndex= */ i,
      mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),
      overrides.get(0));
}
           

我們可以從 mappedTrackInfo.getTrackGroups(i)入手,getTrackGroups函數擷取到的是TrackGroupArray[],這裡的TrackGroupArray[]表示有多少個軌道組,而TrackGroupArray類中有TrackGroup[],表示了該類型的軌道下有多少個軌道資訊,那麼我們可以看看哪裡對這個TrackGroup[]指派

public TrackGroupArray(TrackGroup... trackGroups) {
  this.trackGroups = trackGroups;
  this.length = trackGroups.length;
}
           

接下來查找哪裡調用了TrackGroupArray的構造函數,因為我們使用的是hls,是以對查找結果做了一些過濾,剩下了這2個地方

修改ExoPlayer源碼播放hls顯示多音軌

經過對這2處地方的研究,HlsMediaPeriod類中有一個HlsSampleStreamWrapper[] sampleStreamWrappers

@Override
public void onPrepared() {
  if (--pendingPrepareCount > 0) {
    return;
  }
  int totalTrackGroupCount = 0;
  for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
    totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length;
  }
  TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
  int trackGroupIndex = 0;
  for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
    int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length;
    for (int j = 0; j < wrapperTrackGroupCount; j++) {
      trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j);
    }
  }
  trackGroups = new TrackGroupArray(trackGroupArray);
  callback.onPrepared(this);
}
           

在HlsMediaPeriod的onPrepared函數中,最後有對trackGroups的指派操作,而且trackGroups的長度則是sampleStreamWrappers數組中每個軌道組長度之和,那麼接下來應該重點研究sampleStreamWrappers數組中每個軌道是如何指派的,通過TrackGroupArray構造函數在HlsSampleStreamWrapper類中的使用,我們可以在HlsSampleStreamWrapper類中找到createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups)這個函數,在定位該函數的調用,一共有2處,buildTracksFromSampleStreams()和prepareWithMasterPlaylistInfo(),根據名稱我們可以很容易排除prepareWithMasterPlaylistInfo

@EnsuresNonNull({"trackGroups", "optionalTrackGroups", "trackGroupToSampleQueueIndex"})
private void buildTracksFromSampleStreams() {
  android.util.Log.e("zbj", "HlsSampleStreamWrapper" + "_" + "buildTracksFromSampleStreams: ");
  // Iterate through the extractor tracks to discover the "primary" track type, and the index
  // of the single track of this type.
  int primaryExtractorTrackType = C.TRACK_TYPE_NONE;
  int primaryExtractorTrackIndex = C.INDEX_UNSET;
  int extractorTrackCount = sampleQueues.length;
  ...
  TrackGroup chunkSourceTrackGroup = chunkSource.getTrackGroup();
  int chunkSourceTrackCount = chunkSourceTrackGroup.length;

  // Instantiate the necessary internal data-structures.
  primaryTrackGroupIndex = C.INDEX_UNSET;
  trackGroupToSampleQueueIndex = new int[extractorTrackCount];
  for (int i = 0; i < extractorTrackCount; i++) {
    trackGroupToSampleQueueIndex[i] = i;
  }

  // Construct the set of exposed track groups.
  TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
  ...
  this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);
  android.util.Log.e("HlsSampleStreamWrapper",
      "_buildTracksFromSampleStreams , " + "trackGroups.size = " + trackGroups.length);
  Assertions.checkState(optionalTrackGroups == null);
  optionalTrackGroups = Collections.emptySet();
}
           

在buildTracksFromSampleStreams函數中,我們可以看到trackGroups的長度與sampleQueues的長度有關

private FormatAdjustingSampleQueue[] sampleQueues;
           

接下來,繼續查找sampleQueues的指派,在createSampleQueue(int id, int type)函數中

private SampleQueue createSampleQueue(int id, int type) {
  int trackCount = sampleQueues.length;

  boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
  FormatAdjustingSampleQueue trackOutput =
      new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData,
          type);
  if (isAudioVideo) {
    trackOutput.setDrmInitData(drmInitData);
  }
  trackOutput.setSampleOffsetUs(sampleOffsetUs);
  trackOutput.sourceId(chunkUid);
  trackOutput.setUpstreamFormatChangeListener(this);
  sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
  sampleQueueTrackIds[trackCount] = id;
  sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);
  ...
}
           

最後一行就是對sampleQueues做擴充,而該函數隻有被一個函數引用,那就是track(int id, int type),當我們點選track引用到的類時,大部分類都是...Reader

修改ExoPlayer源碼播放hls顯示多音軌

上面比較明顯就是一些音頻和視訊解碼相關的,我們随機點選一個進入,基本都是createTracks這個函數調用,我們再次跟蹤createTracks這個函數調用

修改ExoPlayer源碼播放hls顯示多音軌

大部分都是提取器,提取器看名稱就知道是專門對應,而有一個PesReader,這個所有視訊都會用到,我們進入PesReader調用處

@Override
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
    TrackIdGenerator idGenerator) {
  this.timestampAdjuster = timestampAdjuster;
  reader.createTracks(extractorOutput, idGenerator);//run 3 times -zbj
}
           

繼續跟蹤init函數,發現都是在TsExtractor類中consume(ParsableByteArray sectionData)函數中調用,這個函數是對視訊pid、pmt等的讀取,其中trackIds判斷了目前是否包含了trackId ,如果有的話就不走下面init函數了,通過列印日志,證明了音軌的init都隻走了1次

int trackId = mode == MODE_HLS ? streamType : elementaryPid;
if (trackIds.get(trackId)) {
  continue;
}
           

我們注意到trackId在HLS的情況是隻取streamType的,我們可以修改為

int trackId =  elementaryPid;
           

這樣trackId就不會有重複了,保證都走到下面的init建立軌道的流程

雖然上面修改後保證了都有走init函數,但是測試結果還是不理想,軌道資訊還是隻有一條,通過對HlsSampleStreamWrapper類的track(int id, int type)函數加列印,發現createSampleQueue函數隻走了1遍,在track函數中

@Override
public TrackOutput track(int id, int type) {
  @Nullable TrackOutput trackOutput = null;
  if (MAPPABLE_TYPES.contains(type)) {
    // Track types in MAPPABLE_TYPES are handled manually to ignore IDs.
    trackOutput = getMappedTrackOutput(id, type);
  } else /* non-mappable type track */ {
    for (int i = 0; i < sampleQueues.length; i++) {
      if (sampleQueueTrackIds[i] == id) {
        trackOutput = sampleQueues[i];
        break;
      }
    }
  }

  if (trackOutput == null) {
    if (tracksEnded) {
      return createDummyTrackOutput(id, type);
    } else {
      // The relevant SampleQueue hasn't been constructed yet - so construct it.
      trackOutput = createSampleQueue(id, type);
    }
  }
  ...
}
           

MAPPABLE_TYPES是一個存放Integer的Set集合,要想走createSampleQueue函數,那得保證trackOutput 為空,可以看到上面對trackOutput 的指派,首先從MAPPABLE_TYPES中查找,沒有的話再從sampleQueues中查找,到這裡我們可以對MAPPABLE_TYPES的判斷做一下修改,讓MAPPABLE_TYPES去存放id,而非type,這樣根據id不同就會走createSampleQueue函數,也就添加全部的軌道資訊了