Skip to content

YouTube: keep the media session in sync with playback - #844

Merged
AndreyPavlenko merged 10 commits into
AndreyPavlenko:masterfrom
giunatale:fix/media-session
Nov 10, 2026
Merged

AndreyPavlenko merged 10 commits into
AndreyPavlenko:masterfrom
giunatale:fix/media-session

Conversation

@giunatale

@giunatale giunatale commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

With YouTube playing in Fermata Auto, the media session often didn't reflect what was on screen: cards for videos nobody was watching, cards left behind after leaving a video, and Android Auto's autoplay waiting on Fermata for nothing. Seven commits, each fixing one cause, plus one addressing the review (the inactive session during playback was fixed separately by #849, so it's no longer part of this PR):

  • Ignore YouTube feed previews in playback state. The home feed autoplays muted previews in the same player, and they were reported as playback. A muted video outside /watch and /shorts is now treated as a preview; it's reported once it's unmuted on a video page.
  • Stop playback when the YouTube page leaves the video. YouTube's own back button navigates within the page, and the video keeps playing muted and hidden as the home preview, so the session kept reporting playback. Leaving a video page now stops playback, the same as Fermata's goBack() already did.
  • Take the YouTube video title from the player. The title came from document.title, which YouTube updates some time after opening a video, so the card could say "YouTube". It now comes from the player's getVideoData(), falling back to the page title, and is decoded from the JSON that evaluateJavascript() returns, which had quoted the title and shown its quotation marks escaped.
  • Stop YouTube playback when its page is destroyed. Exiting Fermata Auto from Android Auto's launcher destroys the YouTube page, but its engine stayed current: the session kept reporting playback and play requests went nowhere. Playback now stops when the page's activity is destroyed. Switching to another section only detaches the page, which is shown again when coming back, so that doesn't stop anything. Stopping also no longer asks an engine without a current item for its position, which a destroyed page may never answer.
  • Report at once when there is nothing to resume. A play request with no current playback and no last played item did nothing, so callers such as Android Auto's autoplay after a YouTube session waited until they timed out. Fermata now reports an error immediately. A stopped external item (e.g. a web video), which the service can't resume, also no longer keeps its title on the card.
  • Pause when the car disconnects. Switching the car off doesn't always close Fermata's car screen or send it a pause (Android Auto pauses the app it considers its media source, which may be another one), so the video carried on from the phone. Fermata Auto now listens to the car connection updates that Android Auto broadcasts (the same ones the Car App Library's CarConnection uses) and pauses when the car goes from connected to not connected. This pauses whatever is playing, music included, as other media apps do.
  • Reopen the YouTube video on a new car screen. A car screen has no saved state to restore its page from, as a phone screen does. When Android Auto started again after such a disconnect, the new screen opened on YouTube's home while the previous page kept playing out of sight, until it was destroyed and stopped, and play then had nothing to resume. The new car screen now takes over the previous one's video at its position: playing or paused as it was if its page is still there, paused if the page is already gone.

Tested on a OnePlus 12 (Android 16) with Android Auto 17.7, on the phone and in the car. Checked: media card and AA media bar during playback, no card from feed previews, the card going away on every exit path (YouTube's back button, Fermata's back, exiting Fermata Auto from AA's launcher), the right title from the start, and AA's autoplay no longer hanging on Fermata. Session state was checked with logcat -s MediaSessionService. The car disconnect pause and the video takeover are also tested in the car, though not extensively yet. The last commit addresses the review.

@AndreyPavlenko

Copy link
Copy Markdown
Owner

Review of the last two commits (car disconnect pause, new car screen takeover), plus notes on the rest.

  1. closedVideo never expires. It is static for the life of the process. It is written in onActivityEvent(ACTIVITY_DESTROY) for phone activities too, not only car ones, so a later car screen can reopen a stale phone video. Suggest writing it only when a.isCarActivity() and/or adding a time limit.

  2. urlChanged stops any YoutubeMediaEngine, not only this page's. doUpdateVisitedHistory calls cb.onStop() on whichever YouTube engine is current. Add the same eng.getWebView() == this guard that getVideo already uses. Robustness issue rather than a confirmed bug, since takeOverVideo has already stopped the engine in the takeover case.

  3. /live/ and /embed/ are not handled. isVideoUrl and the JS regex only cover /watch and /shorts. Leaving a live video for a non-video page would stop playback, and a muted live stream would be treated as a feed preview. The Java and JS rules are also duplicated; a comment linking them would help keep them in sync.

  4. Car disconnect pauses every engine, including music. Probably intended, but say so in the PR description.

  5. Minor: the CarConnectionState query runs on the main thread in the constructor and in onReceive. It is small, but it is still cross-process IPC.

Verified and not an issue: configChanges in both main activities covers rotation, so ACTIVITY_DESTROY does not fire on rotation. The auto manifest already declares the androidx.car.app.connection provider under <queries>. FermataWebView registers its activity listener correctly.

Not checked: the 3 s delayed seek/pause race in the takeover flow, and the last two commits have not been tested in the car.

Reviewed by Claude

The home feed autoplays muted previews in the main player, which were
reported as playback and now show up as an active media session. Ignore
muted videos outside /watch and /shorts, report a pause when playback
turns into a preview, and report playing when an opened preview is
unmuted.
Going back with Fermata stops the YouTube playback (goBack()), but
YouTube's own back button navigates within the page, so the video kept
playing muted and hidden as the home feed preview and the session kept
reporting playback. Do the same as goBack() when the page navigates
away from a video page.
The title was read from the page title when playback started, which
YouTube updates only some time after a video is opened, so the media
session could show "YouTube" until the next update. Use the player's
own video data, falling back to the page title. The result is also
decoded from the JSON that evaluateJavascript() returns, which quoted
the title and showed its quotation marks escaped.
Exiting Fermata Auto from Android Auto's launcher destroys the YouTube
page, but that page's playback engine stayed current, so the session
kept reporting playback and play requests went to a page that no longer
exists. Stop the playback when the page's activity is destroyed.
Switching to another section only detaches the page, which is shown
again when coming back, so that doesn't stop anything. Stopping no
longer asks an engine without a source for its position, which a
destroyed page may never answer.
A play request with no current playback and no last played item (for
example Android Auto's autoplay after watching YouTube, whose items are
never recorded as last played) did nothing, so the caller waited until
it timed out. Report an error right away instead. A stopped external
item, which can't be resumed from the service, no longer keeps its
metadata, so the media card doesn't offer it.
Switching the car off doesn't always end the car screen or send a pause, so the playback carried on from the phone. Android Auto broadcasts the car connection changes, so pause when it goes from connected to not connected.
A car screen has no saved state to restore its page from, as a phone screen does. A car that's switched off doesn't always close Fermata's screen: when Android Auto started again, the new screen opened on YouTube's home while the previous page kept playing out of sight, until it was closed and stopped, and play then had nothing to resume. Take over the previous screen's video instead, at its position: playing or paused as it was if its page is still there, paused if the page is gone.
- Remember a closed page's video only for a car screen: a phone screen restores its own.
- After reopening a video, seek and pause only if it's still this page's video.
- Stop on leaving a video page only if the current engine is this page's.
- Treat /live and /embed as video pages too, in both the Java and the JS check.
- Query the car connection state off the main thread, as the Car App Library's CarConnection does.
@giunatale

Copy link
Copy Markdown
Contributor Author

Thanks! Rebased on master and addressed in 7325379:

  1. closedVideo: now written only when a car screen is destroyed. A phone screen restores its own video from its saved state, so it never needed it. No time limit: it lives only as long as the process, and it always reopens paused, so at worst a car screen opens on the last video, ready to play.
  2. urlChanged: now stops only this page's engine (eng.getWebView() == this), as getVideo does.
  3. /live and /embed: both now count as video pages, in isVideoUrl and in the JS isPreview, and each one has a comment pointing to the other.
  4. Car disconnect pauses everything: intended, as other media apps do. It's in the PR description now.
  5. Main-thread query: the car connection state is now queried with an AsyncQueryHandler, off the main thread, like the Car App Library's CarConnection does. The broadcast only triggers a query.

3 s seek/pause after a takeover: it could seek and pause another video if one was opened within those 3 s. It now acts only if the current engine is the new page's (eng.getWebView() == webView). This also covers the existing restore from saved state on the phone.

Car testing: the disconnect pause and the takeover work in my car (switching off mid-video paused it, and reopening Fermata brought the video back). They haven't had extensive use yet.

Reopening a video sought and paused it after a fixed 3 s. A page that took longer to load, as in a car that's just been started, started the video after that and kept playing it, and YouTube could start it muted: without the audio focus, the previously playing app carried on and kept the media card.

The video now opens at its position with YouTube's start time parameter, and it's paused and unmuted as soon as it starts playing. The same applies to restoring the page from its saved state.
A new car screen reopened the last video however long ago it was left, e.g. the next time the car was started. It now does so only within a set number of minutes, 10 by default and up to 60, and opens YouTube's home otherwise; 0 never reopens it. The time counts from when the video was paused, or from when its screen was closed if it was still playing.
@AndreyPavlenko
AndreyPavlenko merged commit 8a1a5f4 into AndreyPavlenko:master Nov 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants