Skip to content

Audio-only DASH manifest stuck in LOADING — _shouldAdvertisePeriod requires dispatchers for buffer types not present in the manifest #1847

Description

@et84121

Environment

  • rx-player version: 4.4.1 (also verified the same logic is present on dev branch as of today)
  • Browser: Chrome 134 / macOS
  • Build: ESM, rx-player/minimal entry point
  • Use case: music streaming — audio-only is the normal content shape here, not an edge case
  • Manifest: DASH on-demand (@profiles="urn:mpeg:dash:profile:isoff-on-demand:2011"), single Period, audio-only (one audio AdaptationSet, no video and no text AdaptationSet), Widevine-encrypted

Summary

When loading an audio-only DASH manifest through the minimal build, the player gets stuck in LOADING indefinitely, with no error event and no thrown exception, in either of these two situations:

  1. The application passes a HTMLAudioElement (new Audio()) as videoElement, or
  2. The application uses rx-player/minimal and does not add NATIVE_TEXT_BUFFER / HTML_TEXT_BUFFER to the feature set.

Playback works the moment both conditions are satisfied:

  • videoElement is a HTMLVideoElement (document.createElement('video')), AND
  • the feature set includes NATIVE_TEXT_BUFFER (or HTML_TEXT_BUFFER).

In a music streaming app, hitting both footguns at once is easy: an <audio> element is the natural choice for audio-only content, and pulling in a text-buffer feature is non-obvious when the manifest has no subtitles. The combination produces a permanent LOADING state with getError() returning null and play() returning a Promise that never settles, which makes diagnosing this from the application side very hard.

Minimal reproduction

import RxPlayer from 'rx-player/minimal';
import { DASH, EME } from 'rx-player/features';

RxPlayer.addFeatures([DASH, EME]);

// Bug condition A: HTMLAudioElement
const el = new Audio();

const player = new RxPlayer({ videoElement: el });

player.loadVideo({
  url: '<audio-only DASH manifest URL>',
  transport: 'dash',
  autoPlay: true,
  keySystems: [{ type: 'widevine', getLicense: /* ... */ }],
});

// 30 seconds later:
player.getPlayerState();        // 'LOADING'
player.getAvailablePeriods();   // []
player.getError();              // null

The same outcome occurs if we change new Audio() to document.createElement('video') but keep the feature set as [DASH, EME].

Internal state at the time of the hang

(Captured via _priv_contentInfos.tracksStore._storedPeriodInfo[0].)

Field Audio-only manifest
isPeriodAdvertised false
audio.dispatcher non-null
video.dispatcher null ← (when <audio> element)
text.dispatcher null ← (when no text feature added)

DEBUG-level log ends after:

Stream: Creating new PeriodStream bufferType="audio" periodStart=0
Track: Adding Track Reference bufferType="audio" periodId="0"
Track: The period does not have adaptation for video there is no track to choose

After this, only mediaTick events with rebuffering=true, paused=true, readyState=0 are emitted.

What I traced in the source

tracks_store.ts#_shouldAdvertisePeriod gates the newAvailablePeriods event on all three buffer-type dispatchers being non-null:

private _shouldAdvertisePeriod(periodObj: ITSPeriodObject): boolean {
  return (
    !periodObj.isPeriodAdvertised &&
    periodObj.text.dispatcher !== null &&
    periodObj.video.dispatcher !== null &&
    periodObj.audio.dispatcher !== null
  );
}

For an audio-only manifest, two upstream conditions can leave video.dispatcher and/or text.dispatcher permanently null:

  1. video.dispatcher depends on a "video" PeriodStream being created, which in turn depends on SegmentSinksStore.getNativeBufferTypes() (segment_sinks_store.ts) including "video". That method returns ["video", "audio"] only when _hasVideo === true, which MediaSourceContentInitializer (media_source_content_initializer.ts) sets from mediaElement.nodeName === "VIDEO". So a HTMLAudioElement will never produce a video PeriodStream, never call addTrackReference("video", ...), never set video.dispatcher.

  2. text.dispatcher depends on a "text" PeriodStream being created, which depends on SegmentSinksStore's _textInterface !== null, which depends on createTextDisplayer() returning a non-null displayer. That function returns null when neither features.htmlTextDisplayer nor features.nativeTextDisplayer has been registered (which is exactly the rx-player/minimal + addFeatures([DASH, EME]) case).

So _shouldAdvertisePeriod waits on dispatchers for buffer types whose PeriodStream is intentionally suppressed by upstream logic, and the result is a permanent LOADING state with no diagnostic signal.

Workaround for other users hitting this

For audio-only DASH content from rx-player/minimal, both of the following are required:

import { DASH, EME, NATIVE_TEXT_BUFFER } from 'rx-player/features';

RxPlayer.addFeatures([DASH, EME, NATIVE_TEXT_BUFFER]); // text dispatcher placeholder

const el = document.createElement('video');             // video dispatcher placeholder
const player = new RxPlayer({ videoElement: el });

Even though the manifest has no text/video adaptations, both placeholders are needed to satisfy _shouldAdvertisePeriod. The element does not need to be attached to the DOM.


Thanks for rx-player — the demo player with my real manifest was the key that let me pinpoint this.

Metadata

Metadata

Labels

Priority: 0 (Very high)This issue or PR has a very high priority. Efforts should be concentrated on it first.bugThis is an RxPlayer issue (unexpected result when comparing to the API)

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions