Migrate HLS over to new SampleQueue methods

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=161391296
This commit is contained in:
olly 2017-07-10 07:23:25 -07:00 committed by Oliver Woodman
parent 5ebbb6ef45
commit 06b3b3ca8d
5 changed files with 134 additions and 175 deletions

View File

@ -219,6 +219,10 @@ import java.util.Arrays;
if (!seekRequired) { if (!seekRequired) {
SampleQueue sampleQueue = sampleQueues[track]; SampleQueue sampleQueue = sampleQueues[track];
sampleQueue.rewind(); sampleQueue.rewind();
// A seek can be avoided if we're able to advance to the current playback position in the
// sample queue, or if we haven't read anything from the queue since the previous seek
// (this case is common for sparse tracks such as metadata tracks). In all other cases a
// seek is required.
seekRequired = !sampleQueue.advanceTo(positionUs, true, true) seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
&& sampleQueue.getReadIndex() != 0; && sampleQueue.getReadIndex() != 0;
} }

View File

@ -83,11 +83,6 @@ import com.google.android.exoplayer2.util.Util;
relativeStartIndex = 0; relativeStartIndex = 0;
readPosition = 0; readPosition = 0;
upstreamKeyframeRequired = true; upstreamKeyframeRequired = true;
}
// Called by the consuming thread, but only when there is no loading thread.
public void resetLargestParsedTimestamps() {
largestDiscardedTimestampUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE;
largestQueuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE;
} }

View File

@ -29,7 +29,6 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* A queue of media samples. * A queue of media samples.
@ -52,16 +51,11 @@ public final class SampleQueue implements TrackOutput {
private static final int INITIAL_SCRATCH_SIZE = 32; private static final int INITIAL_SCRATCH_SIZE = 32;
private static final int STATE_ENABLED = 0;
private static final int STATE_ENABLED_WRITING = 1;
private static final int STATE_DISABLED = 2;
private final Allocator allocator; private final Allocator allocator;
private final int allocationLength; private final int allocationLength;
private final SampleMetadataQueue metadataQueue; private final SampleMetadataQueue metadataQueue;
private final SampleExtrasHolder extrasHolder; private final SampleExtrasHolder extrasHolder;
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final AtomicInteger state;
// References into the linked list of allocations. // References into the linked list of allocations.
private AllocationNode firstAllocationNode; private AllocationNode firstAllocationNode;
@ -88,7 +82,6 @@ public final class SampleQueue implements TrackOutput {
metadataQueue = new SampleMetadataQueue(); metadataQueue = new SampleMetadataQueue();
extrasHolder = new SampleExtrasHolder(); extrasHolder = new SampleExtrasHolder();
scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
state = new AtomicInteger();
firstAllocationNode = new AllocationNode(0, allocationLength); firstAllocationNode = new AllocationNode(0, allocationLength);
readAllocationNode = firstAllocationNode; readAllocationNode = firstAllocationNode;
writeAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode;
@ -100,20 +93,13 @@ public final class SampleQueue implements TrackOutput {
* Resets the output. * Resets the output.
*/ */
public void reset() { public void reset() {
reset(true); metadataQueue.clearSampleData();
} clearAllocationNodes(firstAllocationNode);
firstAllocationNode = new AllocationNode(0, allocationLength);
/** readAllocationNode = firstAllocationNode;
* @deprecated Use {@link #reset()}. Don't disable sample queues. writeAllocationNode = firstAllocationNode;
*/ totalBytesWritten = 0;
@Deprecated allocator.trim();
public void reset(boolean enable) {
int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED);
clearSampleData();
metadataQueue.resetLargestParsedTimestamps();
if (previousState == STATE_DISABLED) {
downstreamFormat = null;
}
} }
/** /**
@ -174,16 +160,6 @@ public final class SampleQueue implements TrackOutput {
// Called by the consuming thread. // Called by the consuming thread.
/**
* @deprecated Don't disable sample queues.
*/
@Deprecated
public void disable() {
if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) {
clearSampleData();
}
}
/** /**
* Returns whether a sample is available to be read. * Returns whether a sample is available to be read.
*/ */
@ -265,15 +241,6 @@ public final class SampleQueue implements TrackOutput {
discardDownstreamTo(metadataQueue.discardToEnd()); discardDownstreamTo(metadataQueue.discardToEnd());
} }
/**
* @deprecated Use {@link #advanceToEnd()} followed by {@link #discardToRead()}.
*/
@Deprecated
public void skipAll() {
advanceToEnd();
discardToRead();
}
/** /**
* Advances the read position to the end of the queue. * Advances the read position to the end of the queue.
*/ */
@ -281,17 +248,6 @@ public final class SampleQueue implements TrackOutput {
metadataQueue.advanceToEnd(); metadataQueue.advanceToEnd();
} }
/**
* @deprecated Use {@link #advanceTo(long, boolean, boolean)} followed by
* {@link #discardToRead()}.
*/
@Deprecated
public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
boolean success = advanceTo(timeUs, true, allowTimeBeyondBuffer);
discardToRead();
return success;
}
/** /**
* Attempts to advance the read position to the sample before or at the specified time. * Attempts to advance the read position to the sample before or at the specified time.
* *
@ -307,19 +263,6 @@ public final class SampleQueue implements TrackOutput {
return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
} }
/**
* @deprecated Use {@link #read(FormatHolder, DecoderInputBuffer, boolean, boolean, long)}
* followed by {@link #discardToRead()}.
*/
@Deprecated
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
boolean loadingFinished, long decodeOnlyUntilUs) {
int result = read(formatHolder, buffer, formatRequired, loadingFinished,
decodeOnlyUntilUs);
discardToRead();
return result;
}
/** /**
* Attempts to read from the queue. * Attempts to read from the queue.
* *
@ -558,39 +501,21 @@ public final class SampleQueue implements TrackOutput {
@Override @Override
public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (!startWriteOperation()) { length = preAppend(length);
int bytesSkipped = input.skip(length); int bytesAppended = input.read(writeAllocationNode.allocation.data,
if (bytesSkipped == C.RESULT_END_OF_INPUT) { writeAllocationNode.translateOffset(totalBytesWritten), length);
if (allowEndOfInput) { if (bytesAppended == C.RESULT_END_OF_INPUT) {
return C.RESULT_END_OF_INPUT; if (allowEndOfInput) {
} return C.RESULT_END_OF_INPUT;
throw new EOFException();
} }
return bytesSkipped; throw new EOFException();
}
try {
length = preAppend(length);
int bytesAppended = input.read(writeAllocationNode.allocation.data,
writeAllocationNode.translateOffset(totalBytesWritten), length);
if (bytesAppended == C.RESULT_END_OF_INPUT) {
if (allowEndOfInput) {
return C.RESULT_END_OF_INPUT;
}
throw new EOFException();
}
postAppend(bytesAppended);
return bytesAppended;
} finally {
endWriteOperation();
} }
postAppend(bytesAppended);
return bytesAppended;
} }
@Override @Override
public void sampleData(ParsableByteArray buffer, int length) { public void sampleData(ParsableByteArray buffer, int length) {
if (!startWriteOperation()) {
buffer.skipBytes(length);
return;
}
while (length > 0) { while (length > 0) {
int bytesAppended = preAppend(length); int bytesAppended = preAppend(length);
buffer.readBytes(writeAllocationNode.allocation.data, buffer.readBytes(writeAllocationNode.allocation.data,
@ -598,7 +523,6 @@ public final class SampleQueue implements TrackOutput {
length -= bytesAppended; length -= bytesAppended;
postAppend(bytesAppended); postAppend(bytesAppended);
} }
endWriteOperation();
} }
@Override @Override
@ -607,47 +531,19 @@ public final class SampleQueue implements TrackOutput {
if (pendingFormatAdjustment) { if (pendingFormatAdjustment) {
format(lastUnadjustedFormat); format(lastUnadjustedFormat);
} }
if (!startWriteOperation()) { if (pendingSplice) {
metadataQueue.commitSampleTimestamp(timeUs); if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {
return; return;
}
try {
if (pendingSplice) {
if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {
return;
}
pendingSplice = false;
} }
timeUs += sampleOffsetUs; pendingSplice = false;
long absoluteOffset = totalBytesWritten - size - offset;
metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
} finally {
endWriteOperation();
} }
timeUs += sampleOffsetUs;
long absoluteOffset = totalBytesWritten - size - offset;
metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
} }
// Private methods. // Private methods.
private boolean startWriteOperation() {
return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING);
}
private void endWriteOperation() {
if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) {
clearSampleData();
}
}
private void clearSampleData() {
metadataQueue.clearSampleData();
clearAllocationNodes(firstAllocationNode);
firstAllocationNode = new AllocationNode(0, allocationLength);
readAllocationNode = firstAllocationNode;
writeAllocationNode = firstAllocationNode;
totalBytesWritten = 0;
allocator.trim();
}
/** /**
* Clears allocation nodes starting from {@code fromNode}. * Clears allocation nodes starting from {@code fromNode}.
* *

View File

@ -53,6 +53,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final Handler continueLoadingHandler; private final Handler continueLoadingHandler;
private Callback callback; private Callback callback;
private long preparePositionUs;
private int pendingPrepareCount; private int pendingPrepareCount;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
@ -84,8 +85,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override @Override
public void prepare(Callback callback, long positionUs) { public void prepare(Callback callback, long positionUs) {
playlistTracker.addListener(this);
this.callback = callback; this.callback = callback;
playlistTracker.addListener(this);
preparePositionUs = positionUs;
buildAndPrepareSampleStreamWrappers(positionUs); buildAndPrepareSampleStreamWrappers(positionUs);
} }
@ -123,7 +125,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
} }
} }
} }
boolean selectedNewTracks = false; // We'll always need to seek if this is a first selection to a position other than the prepare
// position.
boolean seekRequired = !seenFirstTrackSelection && positionUs != preparePositionUs;
streamWrapperIndices.clear(); streamWrapperIndices.clear();
// Select tracks for each child, copying the resulting streams back into a new streams array. // Select tracks for each child, copying the resulting streams back into a new streams array.
SampleStream[] newStreams = new SampleStream[selections.length]; SampleStream[] newStreams = new SampleStream[selections.length];
@ -136,8 +140,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
} }
selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections, seekRequired |= sampleStreamWrappers[i].selectTracks(childSelections, mayRetainStreamFlags,
mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection); childStreams, streamResetFlags, positionUs, seenFirstTrackSelection, seekRequired);
boolean wrapperEnabled = false; boolean wrapperEnabled = false;
for (int j = 0; j < selections.length; j++) { for (int j = 0; j < selections.length; j++) {
if (selectionChildIndices[j] == i) { if (selectionChildIndices[j] == i) {
@ -173,7 +177,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
} }
sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers);
if (seenFirstTrackSelection && selectedNewTracks) { if (seekRequired) {
seekToUs(positionUs); seekToUs(positionUs);
// We'll need to reset renderers consuming from all streams due to the seek. // We'll need to reset renderers consuming from all streams due to the seek.
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
@ -188,7 +192,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override @Override
public void discardBuffer(long positionUs) { public void discardBuffer(long positionUs) {
// Do nothing. for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
sampleStreamWrapper.discardBuffer(positionUs);
}
} }
@Override @Override

View File

@ -47,7 +47,7 @@ import java.util.LinkedList;
* {@link SampleStream}s from which the loaded media can be consumed. * {@link SampleStream}s from which the loaded media can be consumed.
*/ */
/* package */ final class HlsSampleStreamWrapper implements Loader.Callback<Chunk>, /* package */ final class HlsSampleStreamWrapper implements Loader.Callback<Chunk>,
SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {
/** /**
* A callback to be notified of events. * A callback to be notified of events.
@ -165,21 +165,42 @@ import java.util.LinkedList;
return trackGroups; return trackGroups;
} }
/**
* Called by the parent {@link HlsMediaPeriod} when a track selection occurs.
*
* @param selections The renderer track selections.
* @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
* for each selection. A {@code true} value indicates that the selection is unchanged, and
* that the caller does not require that the sample stream be recreated.
* @param streams The existing sample streams, which will be updated to reflect the provided
* selections.
* @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
* have been retained but with the requirement that the consuming renderer be reset.
* @param positionUs The current playback position in microseconds.
* @param seenFirstTrackSelection Whether we've already had the first track selection, meaning
* this is a subsequent selection.
* @param seekRequired Whether the parent {@link HlsMediaPeriod} is already guaranteed to perform
* a seek as part of the track selection
* @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as
* part of the track selection.
*/
public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs,
boolean seenFirstTrackSelection, boolean seekRequired) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
// Disable old tracks. int oldEnabledTrackCount = enabledTrackCount;
// Deselect old tracks.
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
int group = ((HlsSampleStream) streams[i]).group; int group = ((HlsSampleStream) streams[i]).group;
setTrackGroupEnabledState(group, false); setTrackGroupEnabledState(group, false);
sampleQueues.valueAt(group).disable();
streams[i] = null; streams[i] = null;
} }
} }
// Enable new tracks. // We'll always need to seek if we're making a selection having previously disabled all tracks.
seekRequired |= seenFirstTrackSelection && oldEnabledTrackCount == 0;
// Select new tracks.
TrackSelection primaryTrackSelection = null; TrackSelection primaryTrackSelection = null;
boolean selectedNewTracks = false;
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) { if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i]; TrackSelection selection = selections[i];
@ -191,37 +212,60 @@ import java.util.LinkedList;
} }
streams[i] = new HlsSampleStream(this, group); streams[i] = new HlsSampleStream(this, group);
streamResetFlags[i] = true; streamResetFlags[i] = true;
selectedNewTracks = true; // If there's still a chance of avoiding a seek, try and seek within the sample queue.
} if (!seekRequired) {
} SampleQueue sampleQueue = sampleQueues.valueAt(group);
if (isFirstTrackSelection) { sampleQueue.rewind();
// At the time of the first track selection all queues will be enabled, so we need to disable // A seek can be avoided if we're able to advance to the current playback position in the
// any that are no longer required. // sample queue, or if we haven't read anything from the queue since the previous seek
int sampleQueueCount = sampleQueues.size(); // (this case is common for sparse tracks such as metadata tracks). In all other cases a
for (int i = 0; i < sampleQueueCount; i++) { // seek is required.
if (!groupEnabledStates[i]) { seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
sampleQueues.valueAt(i).disable(); && sampleQueue.getReadIndex() != 0;
}
}
if (primaryTrackSelection != null && !mediaChunks.isEmpty()) {
primaryTrackSelection.updateSelectedTrack(0);
int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat);
if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {
// The loaded preparation chunk does match the selection. We discard it.
seekTo(lastSeekPositionUs);
} }
} }
} }
// Cancel requests if necessary.
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
chunkSource.reset(); chunkSource.reset();
downstreamTrackFormat = null; downstreamTrackFormat = null;
mediaChunks.clear(); mediaChunks.clear();
int sampleQueueCount = sampleQueues.size();
if (loader.isLoading()) { if (loader.isLoading()) {
// Discard as much as we can synchronously.
for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).discardToEnd();
}
loader.cancelLoading(); loader.cancelLoading();
} else {
for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).reset();
}
}
return false;
}
// If this is the first selection and the chunk loaded during preparation does not match the
// selection, we call seekTo to discard it. Note that if seekRequired is true then the wrapping
// HlsMediaPeriod will call seekTo regardless, and so we do not need to perform the selection
// check here.
if (!seekRequired && !seenFirstTrackSelection && primaryTrackSelection != null
&& !mediaChunks.isEmpty()) {
primaryTrackSelection.updateSelectedTrack(0);
int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat);
if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {
// The loaded preparation chunk does not match the selection, so discard it.
seekTo(positionUs);
} }
} }
return selectedNewTracks; return seekRequired;
}
public void discardBuffer(long positionUs) {
int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).discardTo(positionUs, false, groupEnabledStates[i]);
}
} }
public void seekTo(long positionUs) { public void seekTo(long positionUs) {
@ -234,7 +278,7 @@ import java.util.LinkedList;
} else { } else {
int sampleQueueCount = sampleQueues.size(); int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) { for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).reset(groupEnabledStates[i]); sampleQueues.valueAt(i).reset();
} }
} }
} }
@ -262,15 +306,27 @@ import java.util.LinkedList;
} }
public void release() { public void release() {
int sampleQueueCount = sampleQueues.size(); boolean releasedSynchronously = loader.release(this);
for (int i = 0; i < sampleQueueCount; i++) { if (prepared && !releasedSynchronously) {
sampleQueues.valueAt(i).disable(); // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
// sampleQueues may still be being modified by the loading thread.
int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).discardToEnd();
}
} }
loader.release();
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
released = true; released = true;
} }
@Override
public void onLoaderReleased() {
int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).reset();
}
}
public void setIsTimestampMaster(boolean isTimestampMaster) { public void setIsTimestampMaster(boolean isTimestampMaster) {
chunkSource.setIsTimestampMaster(isTimestampMaster); chunkSource.setIsTimestampMaster(isTimestampMaster);
} }
@ -310,16 +366,16 @@ import java.util.LinkedList;
downstreamTrackFormat = trackFormat; downstreamTrackFormat = trackFormat;
} }
return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, return sampleQueues.valueAt(group).read(formatHolder, buffer, requireFormat, loadingFinished,
loadingFinished, lastSeekPositionUs); lastSeekPositionUs);
} }
/* package */ void skipData(int group, long positionUs) { /* package */ void skipData(int group, long positionUs) {
SampleQueue sampleQueue = sampleQueues.valueAt(group); SampleQueue sampleQueue = sampleQueues.valueAt(group);
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
sampleQueue.skipAll(); sampleQueue.advanceToEnd();
} else { } else {
sampleQueue.skipToKeyframeBefore(positionUs, true); sampleQueue.advanceTo(positionUs, true, true);
} }
} }
@ -408,9 +464,11 @@ import java.util.LinkedList;
if (!released) { if (!released) {
int sampleQueueCount = sampleQueues.size(); int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) { for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).reset(groupEnabledStates[i]); sampleQueues.valueAt(i).reset();
}
if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this);
} }
callback.onContinueLoadingRequested(this);
} }
} }