-
Notifications
You must be signed in to change notification settings - Fork 893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Android] youtube fullscreen in landscape and pip #23933
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* Copyright (c) 2024 The Brave Authors. All rights reserved. | ||
* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this file, | ||
* You can obtain one at https://mozilla.org/MPL/2.0/. */ | ||
|
||
package org.chromium.chrome.browser; | ||
|
||
import org.jni_zero.JNINamespace; | ||
import org.jni_zero.NativeMethods; | ||
|
||
import org.chromium.content_public.browser.WebContents; | ||
|
||
@JNINamespace("chrome::android") | ||
public class BackgroundVideoPlaybackTabHelper { | ||
|
||
public static void toggleFullscreen(WebContents webContents, boolean isFullScreen) { | ||
BackgroundVideoPlaybackTabHelperJni.get().toggleFullscreen(webContents, isFullScreen); | ||
} | ||
|
||
public static boolean isPlayingMedia(WebContents webContents) { | ||
return BackgroundVideoPlaybackTabHelperJni.get().isPlayingMedia(webContents); | ||
} | ||
|
||
@NativeMethods | ||
interface Natives { | ||
void toggleFullscreen(WebContents webContents, boolean isFullScreen); | ||
|
||
boolean isPlayingMedia(WebContents webContents); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
import android.app.Activity; | ||
import android.app.NotificationChannel; | ||
import android.app.NotificationManager; | ||
import android.app.PictureInPictureParams; | ||
import android.content.Context; | ||
import android.content.Intent; | ||
import android.content.IntentSender; | ||
|
@@ -83,6 +84,7 @@ | |
import org.chromium.brave_wallet.mojom.TxService; | ||
import org.chromium.chrome.R; | ||
import org.chromium.chrome.browser.ApplicationLifetime; | ||
import org.chromium.chrome.browser.BackgroundVideoPlaybackTabHelper; | ||
import org.chromium.chrome.browser.BraveAdFreeCalloutDialogFragment; | ||
import org.chromium.chrome.browser.BraveFeatureUtil; | ||
import org.chromium.chrome.browser.BraveHelper; | ||
|
@@ -210,6 +212,7 @@ | |
import org.chromium.mojo.bindings.ConnectionErrorHandler; | ||
import org.chromium.mojo.system.MojoException; | ||
import org.chromium.ui.widget.Toast; | ||
import org.chromium.url.GURL; | ||
|
||
import java.util.Arrays; | ||
import java.util.Calendar; | ||
|
@@ -218,17 +221,18 @@ | |
import java.util.Locale; | ||
import java.util.concurrent.CopyOnWriteArrayList; | ||
|
||
/** | ||
* Brave's extension for ChromeActivity | ||
*/ | ||
/** Brave's extension for ChromeActivity */ | ||
@JNINamespace("chrome::android") | ||
public abstract class BraveActivity extends ChromeActivity | ||
implements BrowsingDataBridge.OnClearBrowsingDataListener, BraveVpnObserver, | ||
OnBraveSetDefaultBrowserListener, ConnectionErrorHandler, PrefObserver, | ||
BraveSafeBrowsingApiHandler.BraveSafeBrowsingApiHandlerDelegate, | ||
BraveNewsConnectionErrorHandler.BraveNewsConnectionErrorHandlerDelegate, | ||
MiscAndroidMetricsConnectionErrorHandler | ||
.MiscAndroidMetricsConnectionErrorHandlerDelegate { | ||
implements BrowsingDataBridge.OnClearBrowsingDataListener, | ||
BraveVpnObserver, | ||
OnBraveSetDefaultBrowserListener, | ||
ConnectionErrorHandler, | ||
PrefObserver, | ||
BraveSafeBrowsingApiHandler.BraveSafeBrowsingApiHandlerDelegate, | ||
BraveNewsConnectionErrorHandler.BraveNewsConnectionErrorHandlerDelegate, | ||
MiscAndroidMetricsConnectionErrorHandler | ||
.MiscAndroidMetricsConnectionErrorHandlerDelegate { | ||
public static final String BRAVE_WALLET_HOST = "wallet"; | ||
public static final String BRAVE_WALLET_URL = "brave://wallet/crypto/portfolio/assets"; | ||
public static final String BRAVE_BUY_URL = "brave://wallet/crypto/fund-wallet"; | ||
|
@@ -261,6 +265,8 @@ public abstract class BraveActivity extends ChromeActivity | |
|
||
public static final int APP_OPEN_COUNT_FOR_WIDGET_PROMO = 25; | ||
|
||
private static final String TAG = "BraveActivity"; | ||
|
||
private static final boolean ENABLE_IN_APP_UPDATE = | ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE; | ||
|
||
|
@@ -366,9 +372,30 @@ public void onPauseWithNative() { | |
if (BraveVpnUtils.isVpnFeatureSupported(BraveActivity.this)) { | ||
BraveVpnNativeWorker.getInstance().removeObserver(this); | ||
} | ||
|
||
super.onPauseWithNative(); | ||
} | ||
|
||
@Override | ||
public void onUserLeaveHint() { | ||
super.onUserLeaveHint(); | ||
if (isActivityFinishingOrDestroyed()) return; | ||
Tab currentTab = getActivityTab(); | ||
if (currentTab != null | ||
&& currentTab.getUrl() != null | ||
&& isYTVideoUrl(currentTab.getUrl()) | ||
&& !isInPictureInPictureMode() | ||
&& BackgroundVideoPlaybackTabHelper.isPlayingMedia(currentTab.getWebContents())) { | ||
BackgroundVideoPlaybackTabHelper.toggleFullscreen(currentTab.getWebContents(), true); | ||
try { | ||
enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); | ||
} catch (IllegalStateException | IllegalArgumentException e) { | ||
Log.e(TAG, "Error entering PiP: " + e); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public boolean onMenuOrKeyboardAction(int id, boolean fromMenu) { | ||
final Tab currentTab = getActivityTab(); | ||
|
@@ -1378,6 +1405,34 @@ public void initBraveNewsController() { | |
} | ||
} | ||
|
||
@Override | ||
public void performOnConfigurationChanged(Configuration newConfig) { | ||
super.performOnConfigurationChanged(newConfig); | ||
|
||
Tab currentTab = getActivityTab(); | ||
if (currentTab != null | ||
&& currentTab.getUrl() != null | ||
&& isYTVideoUrl(currentTab.getUrl()) | ||
&& !isInPictureInPictureMode()) { | ||
BackgroundVideoPlaybackTabHelper.toggleFullscreen( | ||
currentTab.getWebContents(), | ||
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); | ||
} | ||
} | ||
|
||
private boolean isYTVideoUrl(GURL url) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tapanmodh can we place this function into some util class ? i think we are already using similar function at other places There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we are using this function in two places in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need some kind of test for this so would it be easier to unit test if we put it someplace else? Or actually maybe it's better to pass the url and |
||
|
||
if (!GURL.isEmptyOrInvalid(url) | ||
&& url.domainIs(BraveConstants.YOUTUBE_DOMAIN) | ||
&& url.getPath() != null | ||
&& url.getPath().equalsIgnoreCase("/watch") | ||
&& url.getQuery() != null) { | ||
String videoId = UrlUtilities.getValueForKeyInQuery(url, "v"); | ||
return videoId != null && videoId.trim().length() > 0; | ||
} | ||
return false; | ||
} | ||
|
||
private void migrateBgPlaybackToFeature() { | ||
if (ChromeSharedPreferences.getInstance() | ||
.readBoolean( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
specific_include_rules = { | ||
"background_video_playback_tab_helper.cc": [ | ||
"+content/browser/media/session/media_session_impl.h", | ||
"+content/browser/web_contents/web_contents_impl.h", | ||
], | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,31 +7,35 @@ | |
|
||
#include <string> | ||
|
||
#include "base/strings/utf_string_conversions.h" | ||
#include "brave/browser/android/preferences/features.h" | ||
#include "brave/build/android/jni_headers/BackgroundVideoPlaybackTabHelper_jni.h" | ||
#include "brave/components/brave_shields/content/browser/brave_shields_util.h" | ||
#include "brave/components/constants/pref_names.h" | ||
#include "chrome/browser/content_settings/host_content_settings_map_factory.h" | ||
#include "brave/components/script_injector/common/mojom/script_injector.mojom.h" | ||
#include "chrome/browser/profiles/profile.h" | ||
#include "chrome/common/chrome_isolated_world_ids.h" | ||
#include "components/prefs/pref_service.h" | ||
#include "content/browser/web_contents/web_contents_impl.h" | ||
#include "content/public/browser/navigation_controller.h" | ||
#include "content/public/browser/navigation_entry.h" | ||
#include "content/public/browser/navigation_handle.h" | ||
#include "content/public/browser/web_contents.h" | ||
#include "mojo/public/cpp/bindings/associated_remote.h" | ||
#include "net/base/registry_controlled_domains/registry_controlled_domain.h" | ||
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" | ||
#include "url/gurl.h" | ||
|
||
namespace { | ||
bool is_media_playing_ = false; | ||
|
||
const char16_t k_youtube_background_playback_script[] = | ||
u"(function() {" | ||
" if (document._addEventListener === undefined) {" | ||
" document._addEventListener = document.addEventListener;" | ||
" document.addEventListener = function(a,b,c) {" | ||
" if(a != 'visibilitychange') {" | ||
" document._addEventListener(a,b,c);" | ||
" }" | ||
" };" | ||
" if(a != 'visibilitychange') {" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is this script even doing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is handling background audio playback if we put app in background while video is playing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why you need to keep a reference to the original function in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also there should be comments explaining what this is doing |
||
" document._addEventListener(a,b,c);" | ||
" }" | ||
" };" | ||
" }" | ||
"}());"; | ||
|
||
|
@@ -58,6 +62,14 @@ bool IsBackgroundVideoPlaybackEnabled(content::WebContents* contents) { | |
|
||
return true; | ||
} | ||
|
||
mojo::AssociatedRemote<script_injector::mojom::ScriptInjector> GetRemote( | ||
content::RenderFrameHost* rfh) { | ||
mojo::AssociatedRemote<script_injector::mojom::ScriptInjector> | ||
script_injector_remote; | ||
rfh->GetRemoteAssociatedInterfaces()->GetInterface(&script_injector_remote); | ||
return script_injector_remote; | ||
} | ||
} // namespace | ||
|
||
BackgroundVideoPlaybackTabHelper::BackgroundVideoPlaybackTabHelper( | ||
|
@@ -80,4 +92,68 @@ void BackgroundVideoPlaybackTabHelper::DidFinishNavigation( | |
} | ||
} | ||
|
||
void BackgroundVideoPlaybackTabHelper::MediaStartedPlaying( | ||
const MediaPlayerInfo& /*video_type*/, | ||
const content::MediaPlayerId& id) { | ||
is_media_playing_ = true; | ||
} | ||
|
||
void BackgroundVideoPlaybackTabHelper::MediaStoppedPlaying( | ||
const MediaPlayerInfo& /*video_type*/, | ||
const content::MediaPlayerId& id, | ||
WebContentsObserver::MediaStoppedReason /*reason*/) { | ||
is_media_playing_ = false; | ||
} | ||
|
||
namespace chrome { | ||
namespace android { | ||
|
||
void JNI_BackgroundVideoPlaybackTabHelper_ToggleFullscreen( | ||
JNIEnv* env, | ||
const base::android::JavaParamRef<jobject>& jweb_contents, | ||
jboolean is_full_screen) { | ||
content::WebContents* web_contents = | ||
content::WebContents::FromJavaWebContents(jweb_contents); | ||
// Injecting this script to make youtube video fullscreen on landscape mode | ||
// and exit fullscreen on portrait mode. | ||
if (is_full_screen) { | ||
constexpr const char16_t script[] = | ||
uR"js(if(!document.fullscreenElement) { | ||
var fullscreenBtn = | ||
document.getElementsByClassName('fullscreen-icon'); | ||
if(fullscreenBtn && fullscreenBtn.length > 0) { | ||
fullscreenBtn[0].click(); | ||
} else { | ||
var moviePlayer = document.getElementById('movie_player'); | ||
if (moviePlayer) { | ||
moviePlayer.click(); | ||
} | ||
setTimeout(() => { | ||
var fullscreenBtn = | ||
document.getElementsByClassName('fullscreen-icon'); | ||
if(fullscreenBtn && fullscreenBtn.length > 0) { | ||
fullscreenBtn[0].click(); | ||
} | ||
}, 50); | ||
} | ||
} )js"; | ||
GetRemote(web_contents->GetPrimaryMainFrame()) | ||
->RequestAsyncExecuteScript( | ||
ISOLATED_WORLD_ID_BRAVE_INTERNAL, script, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reported by reviewdog 🐶 |
||
blink::mojom::UserActivationOption::kActivate, | ||
blink::mojom::PromiseResultOption::kAwait, base::NullCallback()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reported by reviewdog 🐶 |
||
} else { | ||
if (web_contents->HasActiveEffectivelyFullscreenVideo()) { | ||
web_contents->ExitFullscreen(true); | ||
} | ||
} | ||
} | ||
|
||
jboolean JNI_BackgroundVideoPlaybackTabHelper_IsPlayingMedia( | ||
JNIEnv* env, | ||
const base::android::JavaParamRef<jobject>& jweb_contents) { | ||
return is_media_playing_; | ||
} | ||
} // namespace android | ||
} // namespace chrome | ||
WEB_CONTENTS_USER_DATA_KEY_IMPL(BackgroundVideoPlaybackTabHelper); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to expose this function ? can we check idf the media is being played before we inject the script ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are not injecting the script in this case. we are checking only when user press the home button. we have to display PIP on portrait mode if video is playing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't
FullscreenVideoPictureInPictureController
already handle this? AlsoisPictureInPictureAllowedForFullscreenVideo
in webcontents?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to display PIP from portrait mode and in that case
FullscreenVideoPictureInPictureController
is not handling this.