Repository: youtube/cobalt
Pull request #5428: Deactivate MediaSession when no video playing
Author: login: jasonzhangxx
Labels: name: cp-26.android
description: Cherry Pick to the 26.android branch
Created: 2025-04-16T00:02:45Z | Merged: 2025-04-18T18:34:41Z

1. It now creates MediaSession only when there's an controllable media
   session.
2. It no longer uses web content title as media session title when media
   session title is empty.

b/409562881


Diff:
diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
index dd29b8d8a7f9..639f7d60fc35 100644
--- a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
+++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
@@ -55,11 +55,13 @@ public class CobaltMediaSession implements ArtworkLoader.Callback {
   private WebContents mWebContents;
   private MediaSessionCompat mMediaSession;
   private MediaSessionObserver mMediaSessionObserver;
+  private boolean mIsControllable;
   private boolean mIsPaused;
   private MediaMetadata mMetadata;
   private Set<Integer> mActions;
   private MediaPosition mPosition;
   private Bitmap mArtworkImage;
+  private MediaSessionCompat.Callback mMediaSessionCallback;
 
   // TODO: decouple LifecycleCallback and CobaltMediaSession implementation.
   /** LifecycleCallback to notify listeners when |mediaSession| becomes active or inactive. */
@@ -77,27 +79,7 @@ public CobaltMediaSession(
       Context context, Holder<Activity> activityHolder, ArtworkDownloader artworkDownloader) {
     mContext = context;
     mArtworkLoader = new ArtworkLoader(this, artworkDownloader);
-    createMediaSession();
-  }
-
-  public void setWebContents(WebContents webContents) {
-    if (mWebContents == webContents) {
-      return;
-    }
-    if (webContents == null) {
-      cleanupMediaSessionObserver();
-      mWebContents = null;
-      return;
-    }
-
-    mWebContents = webContents;
-    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
-  }
-
-  private void createMediaSession() {
-    mMediaSession = new MediaSessionCompat(mContext, TAG);
-    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
-    mMediaSession.setCallback(
+    mMediaSessionCallback =
         new MediaSessionCompat.Callback() {
           private void onReceiveAction(int action) {
             // To be cautious, explicitly run the code on main loop .
@@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
+    mMediaSession = new MediaSessionCompat(mContext, TAG);
+    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+    mMediaSession.setCallback(mMediaSessionCallback);
     mMediaSession.setActive(true);
+
+    Log.i(TAG, "MediaSession is activated.");
+  }
+
+  private void deactivateMediaSession() {
+    if (mMediaSession == null) {
+      return;
+    }
+
+    mMediaSession.setCallback(null);
+    mMediaSession.setActive(false);
+    mMediaSession = null;
+
+    Log.i(TAG, "MediaSession has been deactivated.");
   }
 
   private void cleanupMediaSessionObserver() {
@@ -202,6 +218,7 @@ public void mediaSessionDestroyed() {
 
           @Override
           public void mediaSessionStateChanged(boolean isControllable, boolean isPaused) {
+            mIsControllable = isControllable;
             mIsPaused = isPaused;
             updatePlaybackState();
           }
@@ -238,6 +255,15 @@ public void mediaSessionPositionChanged(@Nullable MediaPosition position) {
   }
 
   private void updatePlaybackState() {
+    if (!mIsControllable) {
+      deactivateMediaSession();
+      return;
+    }
+
+    if (mIsControllable && mMediaSession == null) {
+      activateMediaSession();
+    }
+
     PlaybackStateCompat.Builder playbackStateBuilder =
         new PlaybackStateCompat.Builder().setActions(computeMediaSessionActions());
 
@@ -274,11 +300,19 @@ private void updatePlaybackState() {
   }
 
   private void resetMetaData() {
+    if (mMediaSession == null) {
+      return;
+    }
+
     MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
     mMediaSession.setMetadata(metadataBuilder.build());
   }
 
   private void updateMetadata() {
+    if (mMediaSession == null) {
+      return;
+    }
+
     if (mMetadata == null) {
       resetMetaData();
       return;
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index 3440f79cb5da..084bbce68941 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -1722,8 +1722,11 @@ void MediaSessionImpl::RebuildAndNotifyMetadataChanged() {
     artwork = routed_service_->metadata()->artwork;
   }
 
+#if !BUILDFLAG(IS_COBALT)
+  // We don't want to use web content title as media session title.
   if (metadata.title.empty())
     metadata.title = SanitizeMediaTitle(web_contents()->GetTitle());
+#endif // !BUILDFLAG(IS_COBALT)
 
   ContentClient* content_client = content::GetContentClient();
   const GURL& url = web_contents()->GetLastCommittedURL();


Reviews:
user: login: zhongqiliang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T23:34:14Z
user: login: jasonzhangxx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T23:37:46Z
user: login: jasonzhangxx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T23:44:44Z
user: login: zhongqiliang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:00:38Z
user: login: zhongqiliang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:01:44Z
user: login: zhongqiliang
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:57:51Z
user: login: jasonzhangxx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T20:30:39Z
user: login: jasonzhangxx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T20:30:44Z
user: login: yell0wd0g
body: RS LGTM albeit I don't have enough viz into what this CL is supposed to do.

What about unittests?

At the very least it seems like media_session_impl.cc already has some tests, and in particular one that deals with the metadata update: https://source.corp.google.com/h/lbshell-internal/cobalt_src/+/main:content/browser/media/session/media_session_impl_unittest.cc;l=756-773 -- can we update it in this CL?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T18:34:39Z

Review comments:
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
+    mMediaSession = new MediaSessionCompat(mContext, TAG);
+    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+    mMediaSession.setCallback(mMediaSessionCallabck);
     mMediaSession.setActive(true);
+
+    Log.i(TAG, "MediaSession is activated.");
+  }
+
+  private void deactivateMediaSession() {
+    if (mMediaSession == null) {
+      return;
+    }
+
+    mMediaSession.setCallback(null);
+    mMediaSession.setActive(false);
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: zhongqiliang
body: can you move this above mMediaSession.setCallback(null); to avoid race condition just in case
created_at: 2025-04-16T23:26:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,27 +79,7 @@ public CobaltMediaSession(
       Context context, Holder<Activity> activityHolder, ArtworkDownloader artworkDownloader) {
     mContext = context;
     mArtworkLoader = new ArtworkLoader(this, artworkDownloader);
-    createMediaSession();
-  }
-
-  public void setWebContents(WebContents webContents) {
-    if (mWebContents == webContents) {
-      return;
-    }
-    if (webContents == null) {
-      cleanupMediaSessionObserver();
-      mWebContents = null;
-      return;
-    }
-
-    mWebContents = webContents;
-    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
-  }
-
-  private void createMediaSession() {
-    mMediaSession = new MediaSessionCompat(mContext, TAG);
-    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
-    mMediaSession.setCallback(
+    mMediaSessionCallabck =
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: zhongqiliang
body: fix typo
created_at: 2025-04-16T23:31:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: zhongqiliang
body: will this method be called when there is already a mMediaSession? 
created_at: 2025-04-16T23:34:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: jasonzhangxx
body: No, but it's ok to call it while there's a `mMediaSession`, the media session will be re-created.
created_at: 2025-04-16T23:37:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
+    mMediaSession = new MediaSessionCompat(mContext, TAG);
+    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+    mMediaSession.setCallback(mMediaSessionCallabck);
     mMediaSession.setActive(true);
+
+    Log.i(TAG, "MediaSession is activated.");
+  }
+
+  private void deactivateMediaSession() {
+    if (mMediaSession == null) {
+      return;
+    }
+
+    mMediaSession.setCallback(null);
+    mMediaSession.setActive(false);
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: jasonzhangxx
body: Why would these two calls have race condition?
created_at: 2025-04-16T23:44:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: zhongqiliang
body: do we need to release the callback on the old mediasession? 
created_at: 2025-04-17T17:00:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
+    mMediaSession = new MediaSessionCompat(mContext, TAG);
+    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+    mMediaSession.setCallback(mMediaSessionCallabck);
     mMediaSession.setActive(true);
+
+    Log.i(TAG, "MediaSession is activated.");
+  }
+
+  private void deactivateMediaSession() {
+    if (mMediaSession == null) {
+      return;
+    }
+
+    mMediaSession.setCallback(null);
+    mMediaSession.setActive(false);
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: zhongqiliang
body: In case other application still think this media session is active, and trigger the callback first?
created_at: 2025-04-17T17:01:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
+    mMediaSession = new MediaSessionCompat(mContext, TAG);
+    mMediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+    mMediaSession.setCallback(mMediaSessionCallabck);
     mMediaSession.setActive(true);
+
+    Log.i(TAG, "MediaSession is activated.");
+  }
+
+  private void deactivateMediaSession() {
+    if (mMediaSession == null) {
+      return;
+    }
+
+    mMediaSession.setCallback(null);
+    mMediaSession.setActive(false);
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: jasonzhangxx
body: Discussed offline, it should be ok to keep it as is.
created_at: 2025-04-17T20:30:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +153,42 @@ public void onStop() {
             Log.i(TAG, "MediaSession action: STOP");
             onReceiveAction(MediaSessionAction.STOP);
           }
-        });
+        };
+  }
+
+  public void setWebContents(WebContents webContents) {
+    if (mWebContents == webContents) {
+      return;
+    }
+    if (webContents == null) {
+      cleanupMediaSessionObserver();
+      mWebContents = null;
+      return;
+    }
+
+    mWebContents = webContents;
+    setupMediaSessionObserver(MediaSession.fromWebContents(mWebContents));
+  }
+
+  private void activateMediaSession() {
path: cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java
user: login: jasonzhangxx
body: Discussed offline, it should be ok to keep it as is.
created_at: 2025-04-17T20:30:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: johnxwork
created_at: 2025-04-16T21:12:37Z
body: Does Android OS not show splash screen video on home page after this change?
author_association: CONTRIBUTOR
user: login: jasonzhangxx
created_at: 2025-04-16T22:57:18Z
body: > Does Android OS not show splash screen video on home page after this change?

Updated the change. Now the media session will be removed after Kabuki suspended. And it won't show web content title in any cases.
author_association: CONTRIBUTOR
user: login: jasonzhangxx
created_at: 2025-04-18T18:37:32Z
body: These

> RS LGTM albeit I don't have enough viz into what this CL is supposed to do.
> 
> What about unittests?
> 
> At the very least it seems like media_session_impl.cc already has some tests, and in particular one that deals with the metadata update: https://source.corp.google.com/h/lbshell-internal/cobalt_src/+/main:content/browser/media/session/media_session_impl_unittest.cc;l=756-773 -- can we update it in this CL?

These tests are for web api media session. The change is for android media session.
author_association: CONTRIBUTOR
user: login: yell0wd0g
created_at: 2025-04-18T18:47:08Z
body: > These
> 
> > RS LGTM albeit I don't have enough viz into what this CL is supposed to do.
> > What about unittests?
> > At the very least it seems like media_session_impl.cc already has some tests, and in particular one that deals with the metadata update: [source.corp.google.com/h/lbshell-internal/cobalt_src/+/main:content/browser/media/session/media_session_impl_unittest.cc;l=756-773](https://source.corp.google.com/h/lbshell-internal/cobalt_src/+/main:content/browser/media/session/media_session_impl_unittest.cc;l=756-773) -- can we update it in this CL?
> 
> These tests are for web api media session. The change is for android media session.

Even the tiny changes in content/browser/media/session/media_session_impl.cc ? The not-is-cobalt part.
author_association: CONTRIBUTOR