Commit f832578e authored by François Beaufort's avatar François Beaufort Committed by Commit Bot

[Media Controls] Add Exit Picture-in-Picture button.

This makes sure clicking the native Picture-in-Picture button (after
entering Picture-in-Picture) exits Picture-in-Picture and that video
controls are reflected when entering and exiting Picture-in-Picture.

Screenshot: https://i.imgur.com/081Xbmb.png

Bug: 840516, 806249
Change-Id: I410a4a06cc4eea62dae8e1d368e4d05394f13c86
Reviewed-on: https://chromium-review.googlesource.com/1084833
Commit-Queue: François Beaufort <beaufort.francois@gmail.com>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Reviewed-by: default avatarJochen Eisinger <jochen@chromium.org>
Reviewed-by: default avatarapacible <apacible@chromium.org>
Cr-Commit-Position: refs/heads/master@{#564807}
parent ef1c7e05
......@@ -660,6 +660,14 @@ below:
exit full screen
</message>
<message name="IDS_AX_MEDIA_ENTER_PICTURE_IN_PICTURE_BUTTON" desc="Accessibility role description for enter Picture-in-Picture button">
enter picture-in-picture
</message>
<message name="IDS_AX_MEDIA_EXIT_PICTURE_IN_PICTURE_BUTTON" desc="Accessibility role description for exit Picture-in-Picture button">
exit picture-in-picture
</message>
<message name="IDS_AX_MEDIA_SHOW_CLOSED_CAPTIONS_BUTTON" desc="Accessibility role description for show closed captions button">
show closed captions
</message>
......@@ -732,6 +740,14 @@ below:
exit full screen
</message>
<message name="IDS_AX_MEDIA_ENTER_PICTURE_IN_PICTURE_BUTTON_HELP" desc="Accessibility help description for enter Picture-in-Picture button">
play video in picture-in-picture mode
</message>
<message name="IDS_AX_MEDIA_EXIT_PICTURE_IN_PICTURE_BUTTON_HELP" desc="Accessibility help description for exit Picture-in-Picture button">
exit picture-in-picture
</message>
<message name="IDS_AX_MEDIA_SHOW_CLOSED_CAPTIONS_BUTTON_HELP" desc="Accessibility help description for show closed captions button">
start displaying closed captions
</message>
......@@ -938,9 +954,12 @@ below:
<message name="IDS_MEDIA_OVERFLOW_MENU_DOWNLOAD" desc="Media controls overflow menu item label for a download button.">
Download
</message>
<message name="IDS_MEDIA_OVERFLOW_MENU_PICTURE_IN_PICTURE" desc="Media controls overflow menu item label for a picture-in-picture button.">
<message name="IDS_MEDIA_OVERFLOW_MENU_ENTER_PICTURE_IN_PICTURE" desc="Media controls overflow menu item label for a button to enter Picture-in-Picture.">
Picture-in-Picture
</message>
<message name="IDS_MEDIA_OVERFLOW_MENU_EXIT_PICTURE_IN_PICTURE" desc="Media controls overflow menu item label for a button to exit Picture-in-Picture.">
Exit Picture-in-Picture
</message>
<message name="IDS_MEDIA_PICTURE_IN_PICTURE_INTERSTITIAL_TEXT" desc="Text message shown to user when in picture in picture mode. When a video is in picture in picture mode, an interstitial with this text appears where the video player is positioned. The video continues to play back in another window that gives the experience that the video is 'popped out'.">
This video is playing in Picture-in-Picture
</message>
......
......@@ -105,6 +105,10 @@ static int ToMessageID(WebLocalizedString::Name name) {
return IDS_AX_MEDIA_ENTER_FULL_SCREEN_BUTTON;
case WebLocalizedString::kAXMediaExitFullscreenButton:
return IDS_AX_MEDIA_EXIT_FULL_SCREEN_BUTTON;
case WebLocalizedString::kAXMediaEnterPictureInPictureButton:
return IDS_AX_MEDIA_ENTER_PICTURE_IN_PICTURE_BUTTON;
case WebLocalizedString::kAXMediaExitPictureInPictureButton:
return IDS_AX_MEDIA_EXIT_PICTURE_IN_PICTURE_BUTTON;
case WebLocalizedString::kAXMediaShowClosedCaptionsButton:
return IDS_AX_MEDIA_SHOW_CLOSED_CAPTIONS_BUTTON;
case WebLocalizedString::kAXMediaHideClosedCaptionsButton:
......@@ -141,6 +145,10 @@ static int ToMessageID(WebLocalizedString::Name name) {
return IDS_AX_MEDIA_ENTER_FULL_SCREEN_BUTTON_HELP;
case WebLocalizedString::kAXMediaExitFullscreenButtonHelp:
return IDS_AX_MEDIA_EXIT_FULL_SCREEN_BUTTON_HELP;
case WebLocalizedString::kAXMediaEnterPictureInPictureButtonHelp:
return IDS_AX_MEDIA_ENTER_PICTURE_IN_PICTURE_BUTTON_HELP;
case WebLocalizedString::kAXMediaExitPictureInPictureButtonHelp:
return IDS_AX_MEDIA_EXIT_PICTURE_IN_PICTURE_BUTTON_HELP;
case WebLocalizedString::kAXMediaShowClosedCaptionsButtonHelp:
return IDS_AX_MEDIA_SHOW_CLOSED_CAPTIONS_BUTTON_HELP;
case WebLocalizedString::kAXMediaHideClosedCaptionsButtonHelp:
......@@ -223,8 +231,10 @@ static int ToMessageID(WebLocalizedString::Name name) {
return IDS_MEDIA_OVERFLOW_MENU_PAUSE;
case WebLocalizedString::kOverflowMenuDownload:
return IDS_MEDIA_OVERFLOW_MENU_DOWNLOAD;
case WebLocalizedString::kOverflowMenuPictureInPicture:
return IDS_MEDIA_OVERFLOW_MENU_PICTURE_IN_PICTURE;
case WebLocalizedString::kOverflowMenuEnterPictureInPicture:
return IDS_MEDIA_OVERFLOW_MENU_ENTER_PICTURE_IN_PICTURE;
case WebLocalizedString::kOverflowMenuExitPictureInPicture:
return IDS_MEDIA_OVERFLOW_MENU_EXIT_PICTURE_IN_PICTURE;
case WebLocalizedString::kPictureInPictureInterstitialText:
return IDS_MEDIA_PICTURE_IN_PICTURE_INTERSTITIAL_TEXT;
case WebLocalizedString::kPlaceholderForDayOfMonthField:
......
......@@ -18,6 +18,13 @@ async_test(t => {
assert_true(isPictureInPictureButtonEnabled(video), "button should exist");
video.addEventListener('enterpictureinpicture', t.step_func(() => {
setTimeout(t.step_func(() => {
assert_true(isPictureInPictureButtonEnabled(video), "button should exist");
clickPictureInPictureButton(video);
}));
}), { once: true });
video.addEventListener('leavepictureinpicture', t.step_func(() => {
setTimeout(t.step_func_done(() => {
assert_true(isPictureInPictureButtonEnabled(video), "button should exist");
}));
......
......@@ -19,9 +19,17 @@ async_test(t => {
checkPictureInPictureInterstitialDoesNotExist(video);
video.addEventListener('enterpictureinpicture', t.step_func_done(() => {
video.addEventListener('enterpictureinpicture', t.step_func(() => {
assert_true(isPictureInPictureInterstitialVisible(video),
"Interstitial should be visible when video enters Picture-in-Picture");
clickPictureInPictureButton(video);
}));
video.addEventListener('leavepictureinpicture', t.step_func(() => {
setTimeout(t.step_func_done(() => {
assert_false(isPictureInPictureInterstitialVisible(video),
"Interstitial should not be visible when video leaves Picture-in-Picture");
}), 300 /* transition */);
}));
clickPictureInPictureButton(video);
......
......@@ -76,6 +76,10 @@ struct WebLocalizedString {
kAXMediaVideoElement,
kAXMediaVideoElementHelp,
kAXMediaVideoSliderHelp,
kAXMediaEnterPictureInPictureButton,
kAXMediaEnterPictureInPictureButtonHelp,
kAXMediaExitPictureInPictureButton,
kAXMediaExitPictureInPictureButtonHelp,
kAXMillisecondFieldText,
kAXMinuteFieldText,
kAXMonthFieldText,
......@@ -113,7 +117,8 @@ struct WebLocalizedString {
kOverflowMenuPlay,
kOverflowMenuPause,
kOverflowMenuDownload,
kOverflowMenuPictureInPicture,
kOverflowMenuEnterPictureInPicture,
kOverflowMenuExitPictureInPicture,
kPictureInPictureInterstitialText,
// kPlaceholderForDayOfMonthField is for day placeholder text, e.g.
// "dd", for date field used in multiple fields "date", "datetime", and
......
......@@ -40,10 +40,14 @@ class CORE_EXPORT PictureInPictureController
kDisabledByAttribute,
};
// Enter Picture-in-Picture for a video element and resolve promise.
// Enter Picture-in-Picture for a video element and resolve promise if any.
virtual void EnterPictureInPicture(HTMLVideoElement*,
ScriptPromiseResolver*) = 0;
// Exit Picture-in-Picture for a video element and resolve promise if any.
virtual void ExitPictureInPicture(HTMLVideoElement*,
ScriptPromiseResolver*) = 0;
// Returns whether a given video element in a document associated with the
// controller is allowed to request Picture-in-Picture.
virtual Status IsElementAllowed(const HTMLVideoElement&) const = 0;
......
......@@ -90,6 +90,8 @@ AXObject* AccessibilityMediaControl::Create(
case kMediaOverflowList:
case kMediaDownloadButton:
case kMediaScrubbingMessage:
case kMediaEnterPictureInPictureButton:
case kMediaExitPictureInPictureButton:
return new AccessibilityMediaControl(layout_object, ax_object_cache);
}
......@@ -171,6 +173,12 @@ String AccessibilityMediaControl::TextAlternative(
case kMediaOverflowList:
case kMediaScrubbingMessage:
return QueryString(WebLocalizedString::kAXMediaDefault);
case kMediaEnterPictureInPictureButton:
return QueryString(
WebLocalizedString::kAXMediaEnterPictureInPictureButton);
case kMediaExitPictureInPictureButton:
return QueryString(
WebLocalizedString::kAXMediaExitPictureInPictureButton);
case kMediaSlider:
NOTREACHED();
return QueryString(WebLocalizedString::kAXMediaDefault);
......@@ -215,6 +223,12 @@ String AccessibilityMediaControl::Description(
return QueryString(WebLocalizedString::kAXMediaCastOnButtonHelp);
case kMediaOverflowButton:
return QueryString(WebLocalizedString::kAXMediaOverflowButtonHelp);
case kMediaEnterPictureInPictureButton:
return QueryString(
WebLocalizedString::kAXMediaEnterPictureInPictureButtonHelp);
case kMediaExitPictureInPictureButton:
return QueryString(
WebLocalizedString::kAXMediaExitPictureInPictureButtonHelp);
case kMediaSliderThumb:
case kMediaTextTrackList:
case kMediaTimelineContainer:
......@@ -262,6 +276,8 @@ AccessibilityRole AccessibilityMediaControl::RoleValue() const {
case kMediaDownloadButton:
case kMediaCastOnButton:
case kMediaCastOffButton:
case kMediaEnterPictureInPictureButton:
case kMediaExitPictureInPictureButton:
return kButtonRole;
case kMediaTimelineContainer:
......
......@@ -36,6 +36,8 @@ enum MediaControlElementType {
kMediaOverflowList,
kMediaDownloadButton,
kMediaScrubbingMessage,
kMediaEnterPictureInPictureButton,
kMediaExitPictureInPictureButton,
};
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_MEDIA_CONTROLS_ELEMENTS_MEDIA_CONTROL_ELEMENT_TYPE_H_
......@@ -29,9 +29,29 @@ bool MediaControlPictureInPictureButtonElement::
return true;
}
void MediaControlPictureInPictureButtonElement::UpdateDisplayType() {
DCHECK(MediaElement().IsHTMLVideoElement());
bool isInPictureInPicture =
PictureInPictureControllerImpl::From(MediaElement().GetDocument())
.IsPictureInPictureElement(&ToHTMLVideoElement(MediaElement()));
SetDisplayType(isInPictureInPicture ? kMediaExitPictureInPictureButton
: kMediaEnterPictureInPictureButton);
SetClass("on", isInPictureInPicture);
UpdateOverflowString();
MediaControlInputElement::UpdateDisplayType();
}
WebLocalizedString::Name
MediaControlPictureInPictureButtonElement::GetOverflowStringName() const {
return WebLocalizedString::kOverflowMenuPictureInPicture;
DCHECK(MediaElement().IsHTMLVideoElement());
bool isInPictureInPicture =
PictureInPictureControllerImpl::From(MediaElement().GetDocument())
.IsPictureInPictureElement(&ToHTMLVideoElement(MediaElement()));
return isInPictureInPicture
? WebLocalizedString::kOverflowMenuExitPictureInPicture
: WebLocalizedString::kOverflowMenuEnterPictureInPicture;
}
bool MediaControlPictureInPictureButtonElement::HasOverflowButton() const {
......@@ -51,9 +71,11 @@ void MediaControlPictureInPictureButtonElement::DefaultEventHandler(
PictureInPictureControllerImpl::From(MediaElement().GetDocument());
DCHECK(MediaElement().IsHTMLVideoElement());
// TODO(crbug.com/840516): Toggle PiP instead.
controller.EnterPictureInPicture(&ToHTMLVideoElement(MediaElement()),
nullptr);
HTMLVideoElement* video_element = &ToHTMLVideoElement(MediaElement());
if (controller.IsPictureInPictureElement(video_element))
controller.ExitPictureInPicture(video_element, nullptr);
else
controller.EnterPictureInPicture(video_element, nullptr);
}
MediaControlInputElement::DefaultEventHandler(event);
......
......@@ -19,6 +19,7 @@ class MediaControlPictureInPictureButtonElement final
// MediaControlInputElement:
bool WillRespondToMouseClickEvents() override;
void UpdateDisplayType() override;
WebLocalizedString::Name GetOverflowStringName() const override;
bool HasOverflowButton() const override;
......
......@@ -1678,6 +1678,10 @@ void MediaControlsImpl::OnExitedFullscreen() {
StartHideMediaControlsTimer();
}
void MediaControlsImpl::OnPictureInPictureChanged() {
picture_in_picture_button_->UpdateDisplayType();
}
void MediaControlsImpl::OnPanelKeypress() {
// If the user is interacting with the controls via the keyboard, don't hide
// the controls. This is called when the user mutes/unmutes, turns CC on/off,
......
......@@ -315,6 +315,7 @@ class MODULES_EXPORT MediaControlsImpl final : public HTMLDivElement,
void OnLoadedMetadata();
void OnEnteredFullscreen();
void OnExitedFullscreen();
void OnPictureInPictureChanged();
void OnPanelKeypress();
void OnMediaKeyboardEvent(Event* event) { DefaultEventHandler(event); }
void OnWaiting();
......
......@@ -49,6 +49,14 @@ void MediaControlsMediaEventListener::Attach() {
media_controls_->GetDocument().addEventListener(
EventTypeNames::fullscreenchange, this, false);
// Picture-in-Picture events.
if (RuntimeEnabledFeatures::PictureInPictureEnabled()) {
GetMediaElement().addEventListener(EventTypeNames::enterpictureinpicture,
this, false);
GetMediaElement().addEventListener(EventTypeNames::leavepictureinpicture,
this, false);
}
// TextTracks events.
TextTrackList* text_tracks = GetMediaElement().textTracks();
text_tracks->addEventListener(EventTypeNames::addtrack, this, false);
......@@ -188,6 +196,13 @@ void MediaControlsMediaEventListener::handleEvent(
return;
}
// Picture-in-Picture events.
if (event->type() == EventTypeNames::enterpictureinpicture ||
event->type() == EventTypeNames::leavepictureinpicture) {
media_controls_->OnPictureInPictureChanged();
return;
}
// TextTracks events.
if (event->type() == EventTypeNames::addtrack ||
event->type() == EventTypeNames::removetrack) {
......
<svg width="22" height="18" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" opacity=".87">
<path d="M18 4H4v10h14V4zm4 12V1.98C22 .88 21.1 0 20 0H2C.9 0 0 .88 0 1.98V16c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 .02H2V1.97h18v14.05z" fill="#000" fill-rule="nonzero"/>
<path d="M-1-3h24v24H-1z"/>
</g>
</svg>
\ No newline at end of file
......@@ -344,6 +344,11 @@ video::-internal-media-controls-picture-in-picture-button {
url(ic_picture_in_picture.svg) 1x);
}
video::-internal-media-controls-picture-in-picture-button.on {
background-image: -webkit-image-set(
url(ic_picture_in_picture_exit.svg) 1x);
}
video::-webkit-media-controls:not(.audio-only) [pseudo="-webkit-media-controls-panel"] [pseudo="-internal-media-controls-overflow-button"] {
background-image: -webkit-image-set(url(ic_menu_white.svg) 1x);
}
......
......@@ -49,15 +49,13 @@ class PictureInPictureControllerImpl : public PictureInPictureController {
ScriptPromiseResolver*,
const WebSize& picture_in_picture_window_size);
// Exit Picture-in-Picture for a video element and resolve promise if any.
void ExitPictureInPicture(HTMLVideoElement*, ScriptPromiseResolver*);
// Returns element currently in Picture-in-Picture if any. Null otherwise.
Element* PictureInPictureElement(TreeScope&) const;
// Implementation of PictureInPictureController.
void EnterPictureInPicture(HTMLVideoElement*,
ScriptPromiseResolver*) override;
void ExitPictureInPicture(HTMLVideoElement*, ScriptPromiseResolver*) override;
void OnExitedPictureInPicture(ScriptPromiseResolver*) override;
Status IsElementAllowed(const HTMLVideoElement&) const override;
bool IsPictureInPictureElement(const Element*) const override;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment