When a page with an active <video > or <audio > element enters the Back/Forward Cache on navigation, HTMLMediaElement::suspend(ReasonForSuspension::BackForwardCache) only calls stopWithoutDestroyingMediaPlayer(). This pauses playback but keeps the full GStreamer pipeline (and its hardware decoder allocation) alive.
On embedded devices with limited hardware decoder slots (e.g., STBs with a single secure decoder), this prevents the newly navigated page from acquiring a decoder, resulting in a black screen or media playback failure.
But neither path ReasonForSuspension::PageWillBeSuspended releases the hardware decoder:
setBufferingPolicy(MakeResourcesPurgeable) — unimplemented in MediaPlayerPrivateGStreamer (empty virtual in MediaPlayerPrivate.h)
setPageIsSuspended(true) — only clears the hole-punch video rectangle, no pipeline state change
For comparison, Chromium destroys its media renderer on suspend (PipelineImpl::Suspend → DestroyRenderer).
The code contrast:
In Source/WebCore/html/HTMLMediaElement.cpp:
void HTMLMediaElement::stop()
{
// ...
stopWithoutDestroyingMediaPlayer();
closeTaskQueues();
clearMediaPlayer(); // <-- releases decoder, destroys pipeline
// ...
}
void HTMLMediaElement::suspend(ReasonForSuspension reason)
{
// ...
case ReasonForSuspension::BackForwardCache:
stopWithoutDestroyingMediaPlayer(); // <-- pauses but keeps decoder allocated
setBufferingPolicy(BufferingPolicy::MakeResourcesPurgeable); // no-op on GStreamer
break;
case ReasonForSuspension::PageWillBeSuspended:
stopWithoutDestroyingMediaPlayer(); // <-- pauses but keeps decoder allocated
setBufferingPolicy(BufferingPolicy::MakeResourcesPurgeable); // no-op on GStreamer
m_player->setPageIsSuspended(true); // only hides video rect
break;
// ...
}
HTMLMediaElement::stop() properly releases all resources by calling clearMediaPlayer() after stopWithoutDestroyingMediaPlayer().
Reproduction scenario:
- Page A plays a video (hardware decoder acquired)
- JavaScript executes window.location.href = "pageB.html" (standard navigation)
- Page A enters BFCache — suspend(BackForwardCache) called — decoder NOT released
- Page B attempts to play video — no hardware decoder available — black screen or media playback failure
Proposed fix options:
- Call clearMediaPlayer() on BFCache entry — simplest fix. On restore, the media element would need to re-create the player and seek back. This mirrors what stop() already does, and is acceptable because BFCache restore is not guaranteed.
- Transition GStreamer pipeline to NULL state on BFCache entry — releases the decoder without destroying the player object. Lighter-weight, but requires re-negotiation of pipeline on restore.
- Override setBufferingPolicy() in MediaPlayerPrivateGStreamer — change pipeline to GST_STATE_NULL when MakeResourcesPurgeable is received. No changes to HTMLMediaElement.cpp needed since the call already exists in suspend(BackForwardCache). On resume, WebKit's existing reload path handles reconstruction
When a page with an active <
video> or <audio> element enters the Back/Forward Cache on navigation, HTMLMediaElement::suspend(ReasonForSuspension::BackForwardCache) only calls stopWithoutDestroyingMediaPlayer(). This pauses playback but keeps the full GStreamer pipeline (and its hardware decoder allocation) alive.On embedded devices with limited hardware decoder slots (e.g., STBs with a single secure decoder), this prevents the newly navigated page from acquiring a decoder, resulting in a black screen or media playback failure.
But neither path ReasonForSuspension::PageWillBeSuspended releases the hardware decoder:
For comparison, Chromium destroys its media renderer on suspend (PipelineImpl::Suspend → DestroyRenderer).
The code contrast:
In Source/WebCore/html/HTMLMediaElement.cpp:
HTMLMediaElement::stop() properly releases all resources by calling clearMediaPlayer() after stopWithoutDestroyingMediaPlayer().
Reproduction scenario:
Proposed fix options: