最近在研究哪种播放器能够支持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个地方
经过对这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
上面比较明显就是一些音频和视频解码相关的,我们随机点击一个进入,基本都是createTracks这个函数调用,我们再次跟踪createTracks这个函数调用
大部分都是提取器,提取器看名称就知道是专门对应,而有一个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函数,也就添加全部的轨道信息了
if (MAPPABLE_TYPES.contains(id)) {