Skip to content

[DASH] getTotalBufferedDurationUs() reports inflated value during cross-period seeks, causing infinite buffering #3081

@akhilesh-dubey

Description

@akhilesh-dubey

Version

Media3 main branch

More version details

Media3 1.6.0

Devices that reproduce the issue

Multiple Android devices (tested on Android TV, Fire TV Stick and Android mobile)

Devices that do not reproduce the issue

NA

Reproducible in the demo app?

Yes

Reproduction steps

Issue description
When seeking across DASH periods in a multi-period live stream (e.g., from the start-over position in period 0 to the live edge in period 3), ExoPlayerImplInternal.getTotalBufferedDurationUs() can transiently report an enormously inflated value — as high as 31 minutes — despite virtually zero actual media data being loaded. This causes LoadControl.shouldContinueLoading() to receive a bufferedDurationUs parameter that far exceeds maxBufferUs, returning false and permanently stopping all loading. The player then stays stuck in STATE_BUFFERING indefinitely.

Reproduction scenario

  • Stream type: Multi-period DASH live stream with DVR (type="dynamic", timeShiftBufferDepth="PT30M", 4 periods)
  • Trigger: Seek from the start-over position (near the beginning, ~30s into the stream) to the live edge (~1946s) -- this crosses from an early period to the last period (period index 3)
  • Result: Player enters STATE_BUFFERING and never transitions to STATE_READY

Expected result

After a cross-period seek, getTotalBufferedDurationUs() should reflect the actual amount of buffered media data. Since the buffer is flushed during the seek, it should report a value near 0 until real segments are loaded at the new position.

Actual result

getTotalBufferedDurationUs() reports 1,886,516,000µs (~31.4 minutes) with 0 bytes of actual data in the allocator, because loadingPeriodHolder.toPeriodTime(rendererPositionUs) converts the stale rendererPositionUs (still from the old period) using the new loading period's rendererOffset, producing a meaningless large negative period-time that inflates the result.

Media

Root cause analysis

The value passed as parameters.bufferedDurationUs to LoadControl.shouldContinueLoading() is computed by:

// ExoPlayerImplInternal.java
   private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
    if (loadingPeriodHolder == null) {
      return 0;
    }
    long totalBufferedDurationUs =
        bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
    return max(0, totalBufferedDurationUs);
  }
 public long toPeriodTime(long rendererTimeUs) {
    return rendererTimeUs - getRendererOffset();
  }

The toPeriodTime(rendererPositionUs) method converts renderer-coordinate time to the loading period's local time:

periodTime = rendererPositionUs - loadingPeriodHolder.rendererOffset

So the full formula is:

totalBufferedDurationUs = bufferedPositionInLoadingPeriodUs - (rendererPositionUs - loadingPeriodHolder.rendererOffset)

During a cross-period seek there is a window where:

  • The loading period has already transitioned to the target period (period 3), so loadingPeriodHolder.getRendererOffset() is a very large value corresponding to the accumulated duration of prior periods.
  • rendererPositionUs -- a field on ExoPlayerImplInternal -- is stale, still reflecting the old playback position from the source period.
  • toPeriodTime(rendererPositionUs) subtracts the new period's large rendererOffset from the stale small rendererPositionUs, producing a large negative period-time.
  • bufferedPositionInLoadingPeriodUs (near 0, since loading just started in the new period) minus this large negative value produces a massive positive phantom duration.
  • max(0, ...) does not catch this because the result is already positive.

Log evidence
The following logs were captured from a custom LoadControl that logs the parameters.bufferedDurationUs and allocator.totalBytesAllocated values received in shouldContinueLoading().

Before the seek — normal buffering:

19:04:54.372  shouldContinueLoading: result=true,
    bufferedDurationUs=41,366,689,   // ~41s — normal
    totalBytesAllocated=12,255,232,  // ~12MB — real data
    totalTargetBufferBytes=144,310,272

Seek issued — seekTo(1946579):

19:04:54.365  seekTo: targetPositionMs=1946579, currentPosition=30101
19:04:54.425  seekTo completed: positionAfterSeek=1946579,
    currentPeriodIndex=3, totalBufferedDurationAfterSeek=0

Immediately after seek — buffer correctly reset:

19:04:54.483  shouldContinueLoading: result=true,
    bufferedDurationUs=0,            // correct — buffer flushed
    totalBytesAllocated=131,072      // ~128KB

19:04:54.486  shouldContinueLoading: result=true,
    bufferedDurationUs=0,            // still correct
    totalBytesAllocated=131,072

16ms later — bufferedDurationUs explodes with no actual data loaded:

19:04:54.499  shouldContinueLoading: result=false,
    bufferedDurationUs=1,886,516,000,  // ~31.4 MINUTES!
    totalBytesAllocated=0,             // ZERO bytes!
    totalTargetBufferBytes=144,310,272

Loading stops permanently — shouldContinueLoading keeps returning false:

19:04:54.500  shouldContinueLoading: result=false, bufferedDurationUs=1,886,516,000, totalBytesAllocated=0
19:04:54.509  shouldContinueLoading: result=false, bufferedDurationUs=1,886,516,000, totalBytesAllocated=0
19:04:54.519  shouldContinueLoading: result=false, bufferedDurationUs=1,886,516,000, totalBytesAllocated=0
  ... continues indefinitely ...

The currentPosition reported by ExoPlayer confirms the stale renderer position:

19:04:54.426  onEvents: currentPosition=1,946,579  // immediately after seek — correct
19:04:54.542  onEvents: currentPosition=60,063      // 116ms later — reverted to old value!
              bufferedPosition=1,946,579             // bufferedPosition is at seek target

This directly shows the position inconsistency that drives the inflated calculation:

bufferedDurationUs ≈ bufferedPosition - currentPosition
                   = 1,946,579,000 - 60,063,000
                   = 1,886,516,000 µs

Suggested fix

In ExoPlayerImplInternal, ensure rendererPositionUs is updated before maybeContinueLoading() is called during seek processing, or add a consistency check in getTotalBufferedDurationUs() that validates the computed result is plausible (e.g., by cross-checking against allocator.totalBytesAllocated).

Bug Report

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions