ExoPlayer 源碼分析 一 HLS 拉流及播放流程
ExoPlayer 源碼分析 二 類圖 & 名詞解釋
ExoPlayer 源碼分析 三 變速播放
ExoPlayer 源碼分析 四 緩存策略
ExoPlayer 源碼分析 五 碼率自適應(yīng)
本文基于 ExoPlayer 2.13.2 。
帶寬預(yù)估
DASH 和 Hls 都可以提供不同碼率的流望浩,ExoPlayer 使用 BandwidthMeter 來計(jì)算帶寬悴晰,它的默認(rèn)實(shí)現(xiàn)是 DefaultBandwidthMeter 培漏。
public interface BandwidthMeter extends TransferListener {
public interface EventListener {
/**
* Invoked periodically to indicate that bytes have been transferred.
*/
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
}
/**
* Indicates no bandwidth estimate is available.
*/
final long NO_ESTIMATE = -1;
/**
* Gets the estimated bandwidth, in bits/sec.
*
* @return Estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if no estimate is available.
*/
long getBitrateEstimate();
}
public final class DefaultBandwidthMeter implements BandwidthMeter {
public static final int DEFAULT_MAX_WEIGHT = 2000;
private final Handler eventHandler;
private final EventListener eventListener;
private final Clock clock;
// 計(jì)算碼率的工具,使用 moving average 來計(jì)算
private final SlidingPercentile slidingPercentile;
// 一個(gè)計(jì)算周期接受的字節(jié)數(shù)
private long bytesAccumulator;
// 一個(gè)計(jì)算周期的開始時(shí)間
private long startTimeMs;
// 帶寬估值
private long bitrateEstimate;
private int streamCount;
public DefaultBandwidthMeter() {
this(null, null);
}
public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) {
this(eventHandler, eventListener, new SystemClock());
}
public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, Clock clock) {
this(eventHandler, eventListener, clock, DEFAULT_MAX_WEIGHT);
}
public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) {
this(eventHandler, eventListener, new SystemClock(), maxWeight);
}
public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, Clock clock,
int maxWeight) {
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.clock = clock;
this.slidingPercentile = new SlidingPercentile(maxWeight);
bitrateEstimate = NO_ESTIMATE;
}
@Override
public synchronized long getBitrateEstimate() {
return bitrateEstimate;
}
@Override
public synchronized void onTransferStart() {
if (streamCount == 0) {
startTimeMs = clock.elapsedRealtime();
}
streamCount++;
}
@Override
public synchronized void onBytesTransferred(int bytes) {
bytesAccumulator += bytes;
}
@Override
public synchronized void onTransferEnd() {
Assertions.checkState(streamCount > 0);
long nowMs = clock.elapsedRealtime();
int elapsedMs = (int) (nowMs - startTimeMs);
if (elapsedMs > 0) {
// 單位用的是字節(jié)和毫秒所以要 *8000
float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs;
slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond);
// 使用 slidingPercentile.getPercentile 計(jì)算出帶寬估值
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
: (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate);
}
streamCount--;
if (streamCount > 0) {
startTimeMs = nowMs;
}
bytesAccumulator = 0;
}
private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onBandwidthSample(elapsedMs, bytes, bitrate);
}
});
}
}
}
slidingPercentile.addSample((int) Math./sqrt/(bytesAccumulator), bitsPerSecond) 會(huì)將當(dāng)前采樣周期傳輸?shù)淖止?jié)開方以及傳輸速率封裝成一個(gè) Sample 放入 SlidingPercentile 中的集合里悴务。
向集合中添加 Sample 的時(shí)候會(huì)保證當(dāng)前所有 Sample 的總 weight 保持在設(shè)定的 maxWeight 以下,如果超出則移除最早添加的數(shù)據(jù)。
帶寬的預(yù)估則采用如下方法:
/**
* Compute the percentile by integration.
*
* @param percentile The desired percentile, expressed as a fraction in the range (0,1].
* @return The requested percentile value or Float.NaN.
*/
public float getPercentile(float percentile) {
// Sample 按 value 從小到大排列
ensureSortedByValue();
float desiredWeight = percentile * totalWeight;
int accumulatedWeight = 0;
for (int i = 0; i < samples.size(); i++) {
Sample currentSample = samples.get(i);
// weight 相加
accumulatedWeight += currentSample.weight;
// 向加后的 weight 達(dá)到 desiredWeight 時(shí)返回當(dāng)前 sample 的value
if (accumulatedWeight >= desiredWeight) {
return currentSample.value;
}
}
// Clamp to maximum value or NaN if no values.
return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value;
}
Hls 碼率切換
切換時(shí)機(jī) & 條件
Hls 碼率切換的代碼在 HlsChunkSource#getChunkOperation 中箱玷,它首先會(huì)通過 getNextVariantIndex 獲得一個(gè)候選 Variant id,其主要步驟及條件如下:
- 根據(jù)預(yù)估帶寬獲取候選 Variant id 即 idealIndex陌宿。
- 切向碼率更低的流锡足,緩存數(shù)據(jù)的可用時(shí)間要小于 20 s。
- 切向碼率更高的流壳坪,緩存數(shù)據(jù)的可用時(shí)間要大于 5 s舶得。
得到 Variant index 之后會(huì)回到 getChunkOperation 代碼如下:
public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs,
ChunkOperationHolder out) {
int previousChunkVariantIndex =
previousTsChunk == null ? -1 : getVariantIndex(previousTsChunk.format);
// 下一個(gè) VariantIndex
int nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
boolean switchingVariant = previousTsChunk != null
&& previousChunkVariantIndex != nextVariantIndex;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex];
if (mediaPlaylist == null) {
// We don't have the media playlist for the next variant. Request it now.
// 當(dāng)前不存在特定的 media playlist 需要請求
out.chunk = newMediaPlaylistChunk(nextVariantIndex);
return;
}
selectedVariantIndex = nextVariantIndex;
int chunkMediaSequence;
// 根據(jù) live 與否,計(jì)算下一個(gè) Chunk 的 MediaSequence
if (live) {
if (previousTsChunk == null) {
chunkMediaSequence = getLiveStartChunkSequenceNumber(selectedVariantIndex);
} else {
chunkMediaSequence = getLiveNextChunkSequenceNumber(previousTsChunk.chunkIndex,
previousChunkVariantIndex, selectedVariantIndex);
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
}
}
} else {
// Not live.
if (previousTsChunk == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence;
} else if (switchingVariant) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
previousTsChunk.startTimeUs, true, true) + mediaPlaylist.mediaSequence;
} else {
chunkMediaSequence = previousTsChunk.getNextChunkIndex();
}
}
//
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
if (chunkIndex >= mediaPlaylist.segments.size()) {
if (!mediaPlaylist.live) {
out.endOfStream = true;
} else if (shouldRerequestLiveMediaPlaylist(selectedVariantIndex)) {
out.chunk = newMediaPlaylistChunk(selectedVariantIndex);
}
return;
}
...
}
緩存切換
Hls 的 RollingSampleBuffer 緩存在 DefaultTrackOutput 中爽蝴,碼率切換時(shí)會(huì)創(chuàng)建新的 DefaultTrackOutput沐批,解碼時(shí)只需要選一個(gè)合適的時(shí)機(jī)切換 DefaultTrackOutput就好了。
具體做法是:
- DefaultTrackOutput 中有個(gè)變量:spliceOutTimeUs 控制當(dāng)前緩存需要切出時(shí)間蝎亚。
- 當(dāng)有多個(gè) HlsExtractorWrapper 時(shí)會(huì)計(jì)算這個(gè)時(shí)間珠插。
- 時(shí)間到了切換到下一個(gè) HlsExtractorWrapper
DefaultTrackOutput. configureSpliceTo
public boolean configureSpliceTo(DefaultTrackOutput nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice.
return true;
}
long firstPossibleSpliceTime;
if (rollingBuffer.peekSample(sampleInfoHolder)) {
firstPossibleSpliceTime = sampleInfoHolder.timeUs;
} else {
firstPossibleSpliceTime = lastReadTimeUs + 1;
}
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime || !sampleInfoHolder.isSyncFrame())) {
// Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes.
nextRollingBuffer.skipSample();
}
if (nextRollingBuffer.peekSample(sampleInfoHolder)) {
// We've found a keyframe in the next queue that can serve as the splice point. Set the
// splice point now.
spliceOutTimeUs = sampleInfoHolder.timeUs;
return true;
}
return false;
}
private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
if (needKeyframe) {
while (haveNext && !sampleInfoHolder.isSyncFrame()) {
rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleInfoHolder);
}
}
if (!haveNext) {
return false;
}
// 如果sampleInfoHolder.timeUs 超過了spliceOutTimeUs 返回 false
if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) {
return false;
}
return true;
}
public boolean isEmpty() {
return !advanceToEligibleSample();
}
讀取數(shù)據(jù)的時(shí)候會(huì)判斷緩存是否為空,這時(shí)候就用到了上面計(jì)算的 spliceOutTimeUs 颖对。
DASH 碼率切換
切換時(shí)機(jī)
切換代碼在 FormatEvaluator 中捻撑,代碼如下:
@Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
Format[] formats, Evaluation evaluation) {
long bufferedDurationUs = queue.isEmpty() ? 0
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
Format current = evaluation.format;
// 根據(jù)帶寬選擇一個(gè)合適的 Format
Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate());
// 是否碼率提升
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
// 是否碼率降低
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) {
// 碼率提升但是當(dāng)前緩存小于 10s,放棄
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal format is a higher quality, but we have insufficient buffer to
// safely switch up. Defer switching up for now.
ideal = current;
} else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) {
// We're switching from an SD stream to a stream of higher resolution. Consider
// discarding already buffered media chunks. Specifically, discard media chunks starting
// from the first one that is of lower bandwidth, lower resolution and that is not HD.
for (int i = 1; i < queue.size(); i++) {
MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720
&& thisChunk.format.width < 1280) {
// Discard chunks from this one onwards.
evaluation.queueSize = i;
break;
}
}
}
} else if (isLower && current != null
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// 向低碼率轉(zhuǎn)換缤底,但是當(dāng)前緩存足夠顾患,先不轉(zhuǎn)
// The ideal format is a lower quality, but we have sufficient buffer to defer switching
// down for now.
ideal = current;
}
if (current != null && ideal != current) {
evaluation.trigger = Chunk.TRIGGER_ADAPTIVE;
}
evaluation.format = ideal;
}
小結(jié)
- 向高碼率切換,當(dāng)前緩存需要大于 10s个唧。
- 向低碼率切換江解,當(dāng)前緩存需要小于 25s。
緩存切換
與 Hls 會(huì)使用多個(gè) DefaultTrackOutput 來緩存數(shù)據(jù)不同徙歼,DASH 僅使用一個(gè)(音頻犁河、視頻、字幕流各有一個(gè)) DefaultTrackOutput 來保存數(shù)據(jù)魄梯。在碼率切換的時(shí)候如果是碼率提升切已緩存數(shù)據(jù)大于 25 s桨螺,那就要丟掉一部分?jǐn)?shù)據(jù)然后加載更高碼率的數(shù)據(jù)。
這個(gè)做法需要三步:
- 計(jì)算出從哪塊開始加載新的數(shù)據(jù)
- 這塊之后的緩存數(shù)據(jù)丟棄酿秸。
- 從這塊開始加載更高碼率的數(shù)據(jù)
代碼中記錄這「塊」的索引的變量是 queueSize灭翔。
1、計(jì)算在上面貼出 FormatEvaluator evaluate 方法中辣苏,這里再貼一遍肝箱。
// 碼率提升但是當(dāng)前緩存小于 10s哄褒,放棄
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal format is a higher quality, but we have insufficient buffer to
// safely switch up. Defer switching up for now.
ideal = current;
} else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) {
// 如果已經(jīng)緩存的數(shù)據(jù)很多,這時(shí)候我們只要保留一部分?jǐn)?shù)據(jù)來滿足當(dāng)前觀看煌张,然后緩存更高質(zhì)量的數(shù)據(jù)
// minDurationToRetainAfterDiscardUs(25s) 是一個(gè)時(shí)間呐赡,只需要保留這段時(shí)間的數(shù)據(jù),后邊的丟棄
// We're switching from an SD stream to a stream of higher resolution. Consider
// discarding already buffered media chunks. Specifically, discard media chunks starting
// from the first one that is of lower bandwidth, lower resolution and that is not HD.
for (int i = 1; i < queue.size(); i++) {
MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
// 根據(jù) minDurationToRetainAfterDiscardUs 計(jì)算從哪塊開始丟棄
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720
&& thisChunk.format.width < 1280) {
// Discard chunks from this one onwards.
evaluation.queueSize = i;
break;
}
}
}
}
2骏融、丟數(shù)據(jù)代碼:
private boolean discardUpstreamMediaChunks(int queueLength) {
if (mediaChunks.size() <= queueLength) {
return false;
}
long startTimeUs = 0;
long endTimeUs = mediaChunks.getLast().endTimeUs;
BaseMediaChunk removed = null;
while (mediaChunks.size() > queueLength) {
removed = mediaChunks.removeLast();
startTimeUs = removed.startTimeUs;
loadingFinished = false;
}
// getFirstSampleIndex 是當(dāng)前 chunk 緩存的第一個(gè) sample 的索引
// 從這個(gè)索引往后的數(shù)據(jù)都要丟掉
sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
notifyUpstreamDiscarded(startTimeUs, endTimeUs);
return true;
}
3链嘀、構(gòu)建新的 Chunk,重新加載數(shù)據(jù)绎谦。
out.queueSize = evaluation.queueSize;
MediaChunk previous = queue.get(out.queueSize - 1);
long nextSegmentStartTimeUs = previous.endTimeUs;
int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(playbackPositionUs)
: startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum()
: queue.get(out.queueSize - 1).getNextChunkIndex();
Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
mediaFormat, enabledTrack, segmentNum, evaluation.trigger, mediaFormat != null);
lastChunkWasInitialization = false;
out.chunk = nextMediaChunk;
如果切向低碼率的流管闷,第二步是不需要的粥脚。
小結(jié)
- DASH 一個(gè)視頻流的數(shù)據(jù)緩存在一個(gè)窃肠,RollingBuffer 里。
- 碼率切換就是選擇一個(gè)時(shí)間點(diǎn)刷允,在這個(gè)時(shí)間之后緩存改變碼率后的數(shù)據(jù)冤留。
- 如果是碼率提升且已緩存數(shù)據(jù)可播放時(shí)間大于 25s,需要把 25s 以后的緩存丟棄树灶,然后緩存更高碼率的數(shù)據(jù)纤怒。