From 5019da3e7b499f50dadfe691af807327e79b5d7c Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 13 Sep 2017 07:25:41 -0700 Subject: [PATCH] Update ABR logic in AdaptiveTrackSelection for live streaming case In live streaming, if the playback position is very close to live edge, the buffered duration will never reach minDurationForQualityIncreaseMs, which prevents switching from ever happening. So we will provide the durationToLiveEdgeUs to AdaptiveTrackSelection in live streaming case, so it can handle this edge case. GitHub: #3017 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168535969 --- .../AdaptiveTrackSelection.java | 70 +++++++++++++++++-- .../trackselection/FixedTrackSelection.java | 2 +- .../trackselection/RandomTrackSelection.java | 2 +- .../trackselection/TrackSelection.java | 13 ++-- .../source/dash/DefaultDashChunkSource.java | 35 ++++++++-- .../exoplayer2/source/hls/HlsChunkSource.java | 19 ++++- .../source/hls/HlsSampleStreamWrapper.java | 4 +- .../smoothstreaming/DefaultSsChunkSource.java | 19 ++++- .../exoplayer2/testutil/FakeChunkSource.java | 3 +- 9 files changed, 141 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 12f5952dd06..b999164f00f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -40,6 +40,7 @@ public static final class Factory implements TrackSelection.Factory { private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. @@ -48,7 +49,9 @@ public Factory(BandwidthMeter bandwidthMeter) { this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); } /** @@ -70,19 +73,53 @@ public Factory(BandwidthMeter bandwidthMeter) { public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { + this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, + bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + } + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed + * when a bandwidth estimate is unavailable. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for + * the selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for + * the selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can + * be discarded to speed up the switch. This is the minimum duration of media that must be + * retained at the lower quality. + * @param bandwidthFraction The fraction of the available bandwidth that the selection should + * consider available for use. Setting to a value less than 1 is recommended to account + * for inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of + * the duration from current playback position to the live edge that has to be buffered + * before the selected track can be switched to one of higher quality. This parameter is + * only applied when the playback position is closer to the live edge than + * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a + * higher quality from happening. + */ + public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, + int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease) { this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; } @Override public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, bandwidthFraction); + minDurationToRetainAfterDiscardMs, bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease); } } @@ -92,6 +129,7 @@ public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... trac public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; private final BandwidthMeter bandwidthMeter; private final int maxInitialBitrate; @@ -99,6 +137,7 @@ public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... trac private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; private int selectedIndex; private int reason; @@ -114,7 +153,9 @@ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); } /** @@ -135,11 +176,17 @@ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, * @param bandwidthFraction The fraction of the available bandwidth that the selection should * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of + * the duration from current playback position to the live edge that has to be buffered + * before the selected track can be switched to one of higher quality. This parameter is + * only applied when the playback position is closer to the live edge than + * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a + * higher quality from happening. */ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, - float bandwidthFraction) { + float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) { super(group, tracks); this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; @@ -147,12 +194,14 @@ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter ban this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; @@ -160,12 +209,13 @@ public void updateSelectedTrack(long bufferedDurationUs) { if (selectedIndex == currentSelectedIndex) { return; } + if (!isBlacklisted(currentSelectedIndex, nowMs)) { // Revert back to the current selection if conditions are not suitable for switching. Format currentFormat = getFormat(currentSelectedIndex); Format selectedFormat = getFormat(selectedIndex); if (selectedFormat.bitrate > currentFormat.bitrate - && bufferedDurationUs < minDurationForQualityIncreaseUs) { + && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) { // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. selectedIndex = currentSelectedIndex; @@ -251,4 +301,12 @@ private int determineIdealSelectedIndex(long nowMs) { return lowestBitrateNonBlacklistedIndex; } + private long minDurationForQualityIncreaseUs(long availableDurationUs) { + boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET + && availableDurationUs <= minDurationForQualityIncreaseUs; + return isAvailableDurationTooShort + ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) + : minDurationForQualityIncreaseUs; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index de1b500c617..ca43258e3fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -78,7 +78,7 @@ public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index 5c7625d6b4c..b70cc8e0d5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -88,7 +88,7 @@ public RandomTrackSelection(TrackGroup group, int[] tracks, Random random) { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { // Count the number of non-blacklisted formats. long nowMs = SystemClock.elapsedRealtime(); int nonBlacklistedFormatCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index fe66946a65d..aeb1d1d6e3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -26,7 +26,7 @@ * {@link TrackGroup}, and a possibly varying individual selected track from the subset. *

* Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected - * track may change as a result of calling {@link #updateSelectedTrack(long)}. + * track may change as a result of calling {@link #updateSelectedTrack(long, long)}. */ public interface TrackSelection { @@ -126,8 +126,11 @@ interface Factory { * Updates the selected track. * * @param bufferedDurationUs The duration of media currently buffered in microseconds. + * @param availableDurationUs The duration of media available for buffering from the current + * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered + * to the end of the current period. */ - void updateSelectedTrack(long bufferedDurationUs); + void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and @@ -148,10 +151,10 @@ interface Factory { /** * Attempts to blacklist the track at the specified index in the selection, making it ineligible - * for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time. - * Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the + * for selection by calls to {@link #updateSelectedTrack(long, long)} for the specified period of + * time. Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the * currently selected track, note that it will remain selected until the next call to - * {@link #updateSelectedTrack(long)}. + * {@link #updateSelectedTrack(long, long)}. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index c6c1461001c..cd7ef6a2bff 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -95,6 +95,7 @@ public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderEr private int periodIndex; private IOException fatalError; private boolean missingLastSegment; + private long liveEdgeTimeUs; /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. @@ -130,6 +131,7 @@ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + liveEdgeTimeUs = C.TIME_UNSET; List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { @@ -179,7 +181,8 @@ public final void getNextChunk(MediaChunk previous, long playbackPositionUs, Chu } long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = representationHolders[trackSelection.getSelectedIndex()]; @@ -203,7 +206,6 @@ public final void getNextChunk(MediaChunk previous, long playbackPositionUs, Chu } } - long nowUs = getNowUnixTimeUs(); int availableSegmentCount = representationHolder.getSegmentCount(); if (availableSegmentCount == 0) { // The index doesn't define any segments. @@ -216,21 +218,23 @@ public final void getNextChunk(MediaChunk previous, long playbackPositionUs, Chu if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; - long periodStartUs = manifest.getPeriod(periodIndex).startMs * 1000; + long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; + long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; } else { lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } + updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); + int segmentNum; if (previous == null) { segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), @@ -311,6 +315,19 @@ private ArrayList getRepresentations() { return representations; } + private void updateLiveEdgeTimeUs(RepresentationHolder representationHolder, + int lastAvailableSegmentNum) { + if (manifest.dynamic) { + DashSegmentIndex segmentIndex = representationHolder.representation.getIndex(); + long lastSegmentDurationUs = segmentIndex.getDurationUs(lastAvailableSegmentNum, + manifest.getPeriodDurationUs(periodIndex)); + liveEdgeTimeUs = segmentIndex.getTimeUs(lastAvailableSegmentNum) + + lastSegmentDurationUs; + } else { + liveEdgeTimeUs = C.TIME_UNSET; + } + } + private long getNowUnixTimeUs() { if (elapsedRealtimeOffsetMs != 0) { return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; @@ -375,6 +392,12 @@ private static Chunk newMediaChunk(RepresentationHolder representationHolder, } } + private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { + boolean resolveTimeToLiveEdgePossible = manifest.dynamic + && !isAfterPositionReset && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + // Protected classes. /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index d0161d839c8..0ad9dd1a6ec 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -103,6 +103,7 @@ public void clear() { // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; + private long liveEdgeTimeUs; /** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. @@ -122,6 +123,7 @@ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; + liveEdgeTimeUs = C.TIME_UNSET; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -214,7 +216,8 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = oldVariantIndex != selectedVariantIndex; @@ -228,6 +231,8 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); independentSegments = mediaPlaylist.hasIndependentSegmentsTag; + updateLiveEdgeTimeUs(mediaPlaylist); + // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { @@ -360,6 +365,16 @@ public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { // Private methods. + private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { + final boolean resolveTimeToLiveEdgePossible = !isAfterPositionReset + && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + + private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) { + liveEdgeTimeUs = mediaPlaylist.hasEndTag ? C.TIME_UNSET : mediaPlaylist.getEndTimeUs(); + } + private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); @@ -409,7 +424,7 @@ public InitializationTrackSelection(TrackGroup group, int[] tracks) { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); if (!isBlacklisted(selectedIndex, nowMs)) { return; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 00a3cd4a85c..b844988588b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -92,7 +92,6 @@ public interface Callback extends SequenceableLoader.Callback= dataSet.getChunkCount()) {