基于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 有两个主要职责:
- 为播放器提供Timeline(定义了媒体结构),当在媒体结构发生变化时提供新的Timeline。MediaSource 通过调用MediaSource.MediaSourceCaller.onSourceInfoRefreshed来提供这些时间线。
- 为其时间线中的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文件读取的大致流程就完成了