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:
- The application passes a
HTMLAudioElement (new Audio()) as videoElement, or
- 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:
-
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.
-
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.
Environment
rx-playerversion:4.4.1(also verified the same logic is present ondevbranch as of today)rx-player/minimalentry point@profiles="urn:mpeg:dash:profile:isoff-on-demand:2011"), single Period, audio-only (one audioAdaptationSet, no video and no textAdaptationSet), Widevine-encryptedSummary
When loading an audio-only DASH manifest through the minimal build, the player gets stuck in
LOADINGindefinitely, with no error event and no thrown exception, in either of these two situations:HTMLAudioElement(new Audio()) asvideoElement, orrx-player/minimaland does not addNATIVE_TEXT_BUFFER/HTML_TEXT_BUFFERto the feature set.Playback works the moment both conditions are satisfied:
videoElementis aHTMLVideoElement(document.createElement('video')), ANDNATIVE_TEXT_BUFFER(orHTML_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 permanentLOADINGstate withgetError()returningnullandplay()returning a Promise that never settles, which makes diagnosing this from the application side very hard.Minimal reproduction
The same outcome occurs if we change
new Audio()todocument.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].)isPeriodAdvertisedfalseaudio.dispatchervideo.dispatchernull← (when<audio>element)text.dispatchernull← (when no text feature added)DEBUG-level log ends after:
After this, only
mediaTickevents withrebuffering=true, paused=true, readyState=0are emitted.What I traced in the source
tracks_store.ts#_shouldAdvertisePeriodgates thenewAvailablePeriodsevent on all three buffer-type dispatchers being non-null:For an audio-only manifest, two upstream conditions can leave
video.dispatcherand/ortext.dispatcherpermanentlynull:video.dispatcherdepends on a"video"PeriodStreambeing created, which in turn depends onSegmentSinksStore.getNativeBufferTypes()(segment_sinks_store.ts) including"video". That method returns["video", "audio"]only when_hasVideo === true, whichMediaSourceContentInitializer(media_source_content_initializer.ts) sets frommediaElement.nodeName === "VIDEO". So aHTMLAudioElementwill never produce avideoPeriodStream, never calladdTrackReference("video", ...), never setvideo.dispatcher.text.dispatcherdepends on a"text"PeriodStreambeing created, which depends onSegmentSinksStore's_textInterface !== null, which depends oncreateTextDisplayer()returning a non-nulldisplayer. That function returnsnullwhen neitherfeatures.htmlTextDisplayernorfeatures.nativeTextDisplayerhas been registered (which is exactly therx-player/minimal+addFeatures([DASH, EME])case).So
_shouldAdvertisePeriodwaits on dispatchers for buffer types whosePeriodStreamis intentionally suppressed by upstream logic, and the result is a permanentLOADINGstate 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: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.