| /* |
| * Copyright (C) 2013-2022 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| |
| #if ENABLE(VIDEO) |
| |
| #include "MediaControlsHost.h" |
| |
| #include "AddEventListenerOptions.h" |
| #include "AudioTrackList.h" |
| #include "CaptionUserPreferences.h" |
| #include "Chrome.h" |
| #include "ChromeClient.h" |
| #include "ContextMenu.h" |
| #include "ContextMenuController.h" |
| #include "ContextMenuItem.h" |
| #include "ContextMenuProvider.h" |
| #include "Event.h" |
| #include "EventListener.h" |
| #include "EventNames.h" |
| #include "EventTarget.h" |
| #include "FloatRect.h" |
| #include "HTMLElement.h" |
| #include "HTMLMediaElement.h" |
| #include "HTMLVideoElement.h" |
| #include "LocalizedStrings.h" |
| #include "Logging.h" |
| #include "MediaControlTextTrackContainerElement.h" |
| #include "MediaControlsContextMenuItem.h" |
| #include "Node.h" |
| #include "Page.h" |
| #include "PageGroup.h" |
| #include "RenderTheme.h" |
| #include "TextTrack.h" |
| #include "TextTrackCueList.h" |
| #include "TextTrackList.h" |
| #include "UserGestureIndicator.h" |
| #include "VTTCue.h" |
| #include "VoidCallback.h" |
| #include <JavaScriptCore/JSCJSValueInlines.h> |
| #include <variant> |
| #include <wtf/Function.h> |
| #include <wtf/JSONValues.h> |
| #include <wtf/Scope.h> |
| #include <wtf/UUID.h> |
| |
| #if USE(APPLE_INTERNAL_SDK) |
| #include <WebKitAdditions/MediaControlsHostAdditions.h> |
| #endif |
| |
| namespace WebCore { |
| |
| const AtomString& MediaControlsHost::automaticKeyword() |
| { |
| static MainThreadNeverDestroyed<const AtomString> automatic("automatic"_s); |
| return automatic; |
| } |
| |
| const AtomString& MediaControlsHost::forcedOnlyKeyword() |
| { |
| static MainThreadNeverDestroyed<const AtomString> forcedOnly("forced-only"_s); |
| return forcedOnly; |
| } |
| |
| static const AtomString& alwaysOnKeyword() |
| { |
| static MainThreadNeverDestroyed<const AtomString> alwaysOn("always-on"_s); |
| return alwaysOn; |
| } |
| |
| static const AtomString& manualKeyword() |
| { |
| static MainThreadNeverDestroyed<const AtomString> alwaysOn("manual"_s); |
| return alwaysOn; |
| } |
| |
| Ref<MediaControlsHost> MediaControlsHost::create(HTMLMediaElement& mediaElement) |
| { |
| return adoptRef(*new MediaControlsHost(mediaElement)); |
| } |
| |
| MediaControlsHost::MediaControlsHost(HTMLMediaElement& mediaElement) |
| : m_mediaElement(mediaElement) |
| { |
| } |
| |
| MediaControlsHost::~MediaControlsHost() |
| { |
| #if ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) |
| if (auto showMediaControlsContextMenuCallback = std::exchange(m_showMediaControlsContextMenuCallback, nullptr)) |
| showMediaControlsContextMenuCallback->handleEvent(); |
| #endif // ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) |
| } |
| |
| String MediaControlsHost::layoutTraitsClassName() const |
| { |
| #if defined(MEDIA_CONTROLS_HOST_LAYOUT_TRAITS_CLASS_NAME_OVERRIDE) |
| return MEDIA_CONTROLS_HOST_LAYOUT_TRAITS_CLASS_NAME_OVERRIDE""_s; |
| #else |
| #if PLATFORM(MAC) || PLATFORM(MACCATALYST) |
| return "MacOSLayoutTraits"_s; |
| #elif PLATFORM(IOS) |
| return "IOSLayoutTraits"_s; |
| #elif PLATFORM(WATCHOS) |
| return "WatchOSLayoutTraits"_s; |
| #elif PLATFORM(GTK) || PLATFORM(WPE) |
| return "AdwaitaLayoutTraits"_s; |
| #else |
| ASSERT_NOT_REACHED(); |
| return nullString(); |
| #endif |
| #endif |
| } |
| |
| const AtomString& MediaControlsHost::mediaControlsContainerClassName() const |
| { |
| static MainThreadNeverDestroyed<const AtomString> className("media-controls-container"_s); |
| return className; |
| } |
| |
| Vector<RefPtr<TextTrack>> MediaControlsHost::sortedTrackListForMenu(TextTrackList& trackList) |
| { |
| if (!m_mediaElement) |
| return { }; |
| |
| Page* page = m_mediaElement->document().page(); |
| if (!page) |
| return { }; |
| |
| return page->group().ensureCaptionPreferences().sortedTrackListForMenu(&trackList, { TextTrack::Kind::Subtitles, TextTrack::Kind::Captions, TextTrack::Kind::Descriptions }); |
| } |
| |
| Vector<RefPtr<AudioTrack>> MediaControlsHost::sortedTrackListForMenu(AudioTrackList& trackList) |
| { |
| if (!m_mediaElement) |
| return { }; |
| |
| Page* page = m_mediaElement->document().page(); |
| if (!page) |
| return { }; |
| |
| return page->group().ensureCaptionPreferences().sortedTrackListForMenu(&trackList); |
| } |
| |
| String MediaControlsHost::displayNameForTrack(const std::optional<TextOrAudioTrack>& track) |
| { |
| if (!m_mediaElement || !track) |
| return emptyString(); |
| |
| Page* page = m_mediaElement->document().page(); |
| if (!page) |
| return emptyString(); |
| |
| return std::visit([page] (auto& track) { |
| return page->group().ensureCaptionPreferences().displayNameForTrack(track.get()); |
| }, track.value()); |
| } |
| |
| TextTrack& MediaControlsHost::captionMenuOffItem() |
| { |
| return TextTrack::captionMenuOffItem(); |
| } |
| |
| TextTrack& MediaControlsHost::captionMenuAutomaticItem() |
| { |
| return TextTrack::captionMenuAutomaticItem(); |
| } |
| |
| AtomString MediaControlsHost::captionDisplayMode() const |
| { |
| if (!m_mediaElement) |
| return emptyAtom(); |
| |
| Page* page = m_mediaElement->document().page(); |
| if (!page) |
| return emptyAtom(); |
| |
| switch (page->group().ensureCaptionPreferences().captionDisplayMode()) { |
| case CaptionUserPreferences::Automatic: |
| return automaticKeyword(); |
| case CaptionUserPreferences::ForcedOnly: |
| return forcedOnlyKeyword(); |
| case CaptionUserPreferences::AlwaysOn: |
| return alwaysOnKeyword(); |
| case CaptionUserPreferences::Manual: |
| return manualKeyword(); |
| default: |
| ASSERT_NOT_REACHED(); |
| return emptyAtom(); |
| } |
| } |
| |
| void MediaControlsHost::setSelectedTextTrack(TextTrack* track) |
| { |
| if (m_mediaElement) |
| m_mediaElement->setSelectedTextTrack(track); |
| } |
| |
| Element* MediaControlsHost::textTrackContainer() |
| { |
| if (!m_textTrackContainer && m_mediaElement) |
| m_textTrackContainer = MediaControlTextTrackContainerElement::create(m_mediaElement->document(), *m_mediaElement); |
| |
| return m_textTrackContainer.get(); |
| } |
| |
| void MediaControlsHost::updateTextTrackContainer() |
| { |
| if (m_textTrackContainer) |
| m_textTrackContainer->updateDisplay(); |
| } |
| |
| void MediaControlsHost::updateTextTrackRepresentationImageIfNeeded() |
| { |
| if (m_textTrackContainer) |
| m_textTrackContainer->updateTextTrackRepresentationImageIfNeeded(); |
| } |
| |
| void MediaControlsHost::enteredFullscreen() |
| { |
| if (m_textTrackContainer) |
| m_textTrackContainer->enteredFullscreen(); |
| } |
| |
| void MediaControlsHost::exitedFullscreen() |
| { |
| if (m_textTrackContainer) |
| m_textTrackContainer->exitedFullscreen(); |
| } |
| |
| void MediaControlsHost::updateCaptionDisplaySizes(ForceUpdate force) |
| { |
| if (m_textTrackContainer) |
| m_textTrackContainer->updateSizes(force == ForceUpdate::Yes ? MediaControlTextTrackContainerElement::ForceUpdate::Yes : MediaControlTextTrackContainerElement::ForceUpdate::No); |
| } |
| |
| bool MediaControlsHost::allowsInlineMediaPlayback() const |
| { |
| return m_mediaElement && !m_mediaElement->mediaSession().requiresFullscreenForVideoPlayback(); |
| } |
| |
| bool MediaControlsHost::supportsFullscreen() const |
| { |
| return m_mediaElement && m_mediaElement->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard); |
| } |
| |
| bool MediaControlsHost::isVideoLayerInline() const |
| { |
| return m_mediaElement && m_mediaElement->isVideoLayerInline(); |
| } |
| |
| bool MediaControlsHost::isInMediaDocument() const |
| { |
| return m_mediaElement && m_mediaElement->document().isMediaDocument(); |
| } |
| |
| bool MediaControlsHost::userGestureRequired() const |
| { |
| return m_mediaElement && !m_mediaElement->mediaSession().playbackStateChangePermitted(MediaPlaybackState::Playing); |
| } |
| |
| bool MediaControlsHost::shouldForceControlsDisplay() const |
| { |
| return m_mediaElement && m_mediaElement->shouldForceControlsDisplay(); |
| } |
| |
| String MediaControlsHost::externalDeviceDisplayName() const |
| { |
| #if ENABLE(WIRELESS_PLAYBACK_TARGET) |
| if (!m_mediaElement) |
| return emptyString(); |
| |
| auto player = m_mediaElement->player(); |
| if (!player) { |
| LOG(Media, "MediaControlsHost::externalDeviceDisplayName - returning \"\" because player is NULL"); |
| return emptyString(); |
| } |
| |
| String name = player->wirelessPlaybackTargetName(); |
| LOG(Media, "MediaControlsHost::externalDeviceDisplayName - returning \"%s\"", name.utf8().data()); |
| return name; |
| #else |
| return emptyString(); |
| #endif |
| } |
| |
| auto MediaControlsHost::externalDeviceType() const -> DeviceType |
| { |
| #if !ENABLE(WIRELESS_PLAYBACK_TARGET) |
| return DeviceType::None; |
| #else |
| if (!m_mediaElement) |
| return DeviceType::None; |
| |
| auto player = m_mediaElement->player(); |
| if (!player) { |
| LOG(Media, "MediaControlsHost::externalDeviceType - returning \"none\" because player is NULL"); |
| return DeviceType::None; |
| } |
| |
| switch (player->wirelessPlaybackTargetType()) { |
| case MediaPlayer::WirelessPlaybackTargetType::TargetTypeNone: |
| return DeviceType::None; |
| case MediaPlayer::WirelessPlaybackTargetType::TargetTypeAirPlay: |
| return DeviceType::Airplay; |
| case MediaPlayer::WirelessPlaybackTargetType::TargetTypeTVOut: |
| return DeviceType::Tvout; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return DeviceType::None; |
| #endif |
| } |
| |
| bool MediaControlsHost::controlsDependOnPageScaleFactor() const |
| { |
| return m_mediaElement && m_mediaElement->mediaControlsDependOnPageScaleFactor(); |
| } |
| |
| void MediaControlsHost::setControlsDependOnPageScaleFactor(bool value) |
| { |
| if (m_mediaElement) |
| m_mediaElement->setMediaControlsDependOnPageScaleFactor(value); |
| } |
| |
| String MediaControlsHost::generateUUID() |
| { |
| return createVersion4UUIDString(); |
| } |
| |
| #if ENABLE(MODERN_MEDIA_CONTROLS) |
| |
| String MediaControlsHost::shadowRootCSSText() |
| { |
| return RenderTheme::singleton().mediaControlsStyleSheet(); |
| } |
| |
| String MediaControlsHost::base64StringForIconNameAndType(const String& iconName, const String& iconType) |
| { |
| return RenderTheme::singleton().mediaControlsBase64StringForIconNameAndType(iconName, iconType); |
| } |
| |
| String MediaControlsHost::formattedStringForDuration(double durationInSeconds) |
| { |
| return RenderTheme::singleton().mediaControlsFormattedStringForDuration(durationInSeconds); |
| } |
| |
| #if ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) |
| |
| #if ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| class MediaControlsContextMenuProvider final : public ContextMenuProvider { |
| public: |
| static Ref<MediaControlsContextMenuProvider> create(Vector<ContextMenuItem>&& items, Function<void(uint64_t)>&& callback) |
| { |
| return adoptRef(*new MediaControlsContextMenuProvider(WTFMove(items), WTFMove(callback))); |
| } |
| |
| private: |
| MediaControlsContextMenuProvider(Vector<ContextMenuItem>&& items, Function<void(uint64_t)>&& callback) |
| : m_items(WTFMove(items)) |
| , m_callback(WTFMove(callback)) |
| { |
| } |
| |
| ~MediaControlsContextMenuProvider() override |
| { |
| contextMenuCleared(); |
| } |
| |
| void populateContextMenu(ContextMenu* menu) override |
| { |
| for (auto& item : m_items) |
| menu->appendItem(item); |
| } |
| |
| void didDismissContextMenu() override |
| { |
| if (!m_didDismiss) { |
| m_didDismiss = true; |
| m_callback(ContextMenuItemTagNoAction); |
| } |
| } |
| |
| void contextMenuItemSelected(ContextMenuAction action, const String&) override |
| { |
| m_callback(action - ContextMenuItemBaseCustomTag); |
| } |
| |
| void contextMenuCleared() override |
| { |
| didDismissContextMenu(); |
| m_items.clear(); |
| } |
| |
| ContextMenuContext::Type contextMenuContextType() override |
| { |
| return ContextMenuContext::Type::MediaControls; |
| } |
| |
| Vector<ContextMenuItem> m_items; |
| Function<void(uint64_t)> m_callback; |
| bool m_didDismiss { false }; |
| }; |
| |
| class MediaControlsContextMenuEventListener final : public EventListener { |
| public: |
| static Ref<MediaControlsContextMenuEventListener> create(Ref<MediaControlsContextMenuProvider>&& contextMenuProvider) |
| { |
| return adoptRef(*new MediaControlsContextMenuEventListener(WTFMove(contextMenuProvider))); |
| } |
| |
| bool operator==(const EventListener& other) const override |
| { |
| return this == &other; |
| } |
| |
| void handleEvent(ScriptExecutionContext&, Event& event) override |
| { |
| ASSERT(event.type() == eventNames().contextmenuEvent); |
| |
| auto* target = event.target(); |
| if (!is<Node>(target)) |
| return; |
| auto& node = downcast<Node>(*target); |
| |
| auto* page = node.document().page(); |
| if (!page) |
| return; |
| |
| page->contextMenuController().showContextMenu(event, m_contextMenuProvider); |
| event.preventDefault(); |
| event.stopPropagation(); |
| event.stopImmediatePropagation(); |
| } |
| |
| private: |
| MediaControlsContextMenuEventListener(Ref<MediaControlsContextMenuProvider>&& contextMenuProvider) |
| : EventListener(EventListener::CPPEventListenerType) |
| , m_contextMenuProvider(WTFMove(contextMenuProvider)) |
| { |
| } |
| |
| Ref<MediaControlsContextMenuProvider> m_contextMenuProvider; |
| }; |
| |
| #endif // ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| |
| bool MediaControlsHost::showMediaControlsContextMenu(HTMLElement& target, String&& optionsJSONString, Ref<VoidCallback>&& callback) |
| { |
| #if USE(UICONTEXTMENU) || (ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS)) |
| if (m_showMediaControlsContextMenuCallback) |
| return false; |
| |
| if (!m_mediaElement) |
| return false; |
| |
| auto& mediaElement = *m_mediaElement; |
| |
| auto* page = mediaElement.document().page(); |
| if (!page) |
| return false; |
| |
| auto optionsJSON = JSON::Value::parseJSON(optionsJSONString); |
| if (!optionsJSON) |
| return false; |
| |
| auto optionsJSONObject = optionsJSON->asObject(); |
| if (!optionsJSONObject) |
| return false; |
| |
| #if USE(UICONTEXTMENU) |
| using MenuItem = MediaControlsContextMenuItem; |
| using MenuItemIdentifier = MediaControlsContextMenuItem::ID; |
| constexpr auto invalidMenuItemIdentifier = MediaControlsContextMenuItem::invalidID; |
| #elif ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| using MenuItem = ContextMenuItem; |
| using MenuItemIdentifier = uint64_t; |
| constexpr auto invalidMenuItemIdentifier = ContextMenuItemTagNoAction; |
| #endif |
| |
| #if ENABLE(VIDEO_PRESENTATION_MODE) |
| enum class PictureInPictureTag { IncludePictureInPicture }; |
| #endif // ENABLE(VIDEO_PRESENTATION_MODE) |
| |
| enum class PlaybackSpeed { |
| x0_5, |
| x1_0, |
| x1_25, |
| x1_5, |
| x2_0, |
| }; |
| |
| using MenuData = std::variant< |
| #if ENABLE(VIDEO_PRESENTATION_MODE) |
| PictureInPictureTag, |
| #endif // ENABLE(VIDEO_PRESENTATION_MODE) |
| RefPtr<AudioTrack>, |
| RefPtr<TextTrack>, |
| RefPtr<VTTCue>, |
| PlaybackSpeed |
| >; |
| HashMap<MenuItemIdentifier, MenuData> idMap; |
| |
| auto createSubmenu = [] (const String& title, const String& icon, Vector<MenuItem>&& children) -> MenuItem { |
| #if USE(UICONTEXTMENU) |
| return { MediaControlsContextMenuItem::invalidID, title, icon, /* checked */ false, WTFMove(children) }; |
| #elif ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| UNUSED_PARAM(icon); |
| return { ContextMenuItemTagNoAction, title, /* enabled */ true, /* checked */ false, WTFMove(children) }; |
| #endif |
| }; |
| |
| auto createMenuItem = [&] (MenuData data, const String& title, bool checked = false, const String& icon = nullString()) -> MenuItem { |
| auto id = idMap.size() + 1; |
| idMap.add(id, data); |
| |
| #if USE(UICONTEXTMENU) |
| return { id, title, icon, checked, /* children */ { } }; |
| #elif ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| UNUSED_PARAM(icon); |
| return { CheckableActionType, static_cast<ContextMenuAction>(ContextMenuItemBaseCustomTag + id), title, /* enabled */ true, checked }; |
| #endif |
| }; |
| |
| Vector<MenuItem> items; |
| |
| #if ENABLE(VIDEO_PRESENTATION_MODE) |
| if (optionsJSONObject->getBoolean("includePictureInPicture"_s).value_or(false)) { |
| ASSERT(is<HTMLVideoElement>(mediaElement)); |
| ASSERT(downcast<HTMLVideoElement>(mediaElement).webkitSupportsPresentationMode(HTMLVideoElement::VideoPresentationMode::PictureInPicture)); |
| items.append(createMenuItem(PictureInPictureTag::IncludePictureInPicture, WEB_UI_STRING_KEY("Picture in Picture", "Picture in Picture (Media Controls Menu)", "Picture in Picture media controls context menu title"), false, "pip.enter"_s)); |
| } |
| #endif // ENABLE(VIDEO_PRESENTATION_MODE) |
| |
| if (optionsJSONObject->getBoolean("includeLanguages"_s).value_or(false)) { |
| if (auto* audioTracks = mediaElement.audioTracks(); audioTracks && audioTracks->length() > 1) { |
| auto& captionPreferences = page->group().ensureCaptionPreferences(); |
| auto languageMenuItems = captionPreferences.sortedTrackListForMenu(audioTracks).map([&](auto& audioTrack) { |
| return createMenuItem(audioTrack, captionPreferences.displayNameForTrack(audioTrack.get()), audioTrack->enabled()); |
| }); |
| |
| if (!languageMenuItems.isEmpty()) |
| items.append(createSubmenu(WEB_UI_STRING_KEY("Languages", "Languages (Media Controls Menu)", "Languages media controls context menu title"), "globe"_s, WTFMove(languageMenuItems))); |
| } |
| } |
| |
| if (optionsJSONObject->getBoolean("includeSubtitles"_s).value_or(false)) { |
| if (auto* textTracks = mediaElement.textTracks(); textTracks && textTracks->length()) { |
| auto& captionPreferences = page->group().ensureCaptionPreferences(); |
| auto sortedTextTracks = captionPreferences.sortedTrackListForMenu(textTracks, { TextTrack::Kind::Subtitles, TextTrack::Kind::Captions, TextTrack::Kind::Descriptions }); |
| bool allTracksDisabled = notFound == sortedTextTracks.findIf([] (const auto& textTrack) { |
| return textTrack->mode() == TextTrack::Mode::Showing; |
| }); |
| bool usesAutomaticTrack = captionPreferences.captionDisplayMode() == CaptionUserPreferences::Automatic && allTracksDisabled; |
| auto subtitleMenuItems = sortedTextTracks.map([&](auto& textTrack) { |
| bool checked = false; |
| if (allTracksDisabled && textTrack == &TextTrack::captionMenuOffItem() && (captionPreferences.captionDisplayMode() == CaptionUserPreferences::ForcedOnly || captionPreferences.captionDisplayMode() == CaptionUserPreferences::Manual)) |
| checked = true; |
| else if (usesAutomaticTrack && textTrack == &TextTrack::captionMenuAutomaticItem()) |
| checked = true; |
| else if (!usesAutomaticTrack && textTrack->mode() == TextTrack::Mode::Showing) |
| checked = true; |
| return createMenuItem(textTrack, captionPreferences.displayNameForTrack(textTrack.get()), checked); |
| }); |
| |
| if (!subtitleMenuItems.isEmpty()) |
| items.append(createSubmenu(WEB_UI_STRING_KEY("Subtitles", "Subtitles (Media Controls Menu)", "Subtitles media controls context menu title"), "captions.bubble"_s, WTFMove(subtitleMenuItems))); |
| } |
| } |
| |
| if (optionsJSONObject->getBoolean("includeChapters"_s).value_or(false)) { |
| if (auto* textTracks = mediaElement.textTracks(); textTracks && textTracks->length()) { |
| auto& captionPreferences = page->group().ensureCaptionPreferences(); |
| |
| for (auto& textTrack : captionPreferences.sortedTrackListForMenu(textTracks, { TextTrack::Kind::Chapters })) { |
| Vector<MenuItem> chapterMenuItems; |
| |
| if (auto* cues = textTrack->cues()) { |
| for (unsigned i = 0; i < cues->length(); ++i) { |
| auto* cue = cues->item(i); |
| if (!is<VTTCue>(cue)) |
| continue; |
| |
| auto& vttCue = downcast<VTTCue>(*cue); |
| chapterMenuItems.append(createMenuItem(RefPtr { &vttCue }, vttCue.text())); |
| } |
| } |
| |
| if (!chapterMenuItems.isEmpty()) { |
| items.append(createSubmenu(captionPreferences.displayNameForTrack(textTrack.get()), "list.bullet"_s, WTFMove(chapterMenuItems))); |
| |
| /* Only show the first valid chapters track. */ |
| break; |
| } |
| } |
| } |
| } |
| |
| if (optionsJSONObject->getBoolean("includePlaybackRates"_s).value_or(false)) { |
| auto playbackRate = mediaElement.playbackRate(); |
| |
| items.append(createSubmenu(WEB_UI_STRING_KEY("Playback Speed", "Playback Speed (Media Controls Menu)", "Playback Speed media controls context menu title"), "speedometer"_s, { |
| createMenuItem(PlaybackSpeed::x0_5, WEB_UI_STRING_KEY("0.5×", "0.5× (Media Controls Menu Playback Speed)", "0.5× media controls context menu playback speed label"), playbackRate == 0.5), |
| createMenuItem(PlaybackSpeed::x1_0, WEB_UI_STRING_KEY("1×", "1× (Media Controls Menu Playback Speed)", "1× media controls context menu playback speed label"), playbackRate == 1.0), |
| createMenuItem(PlaybackSpeed::x1_25, WEB_UI_STRING_KEY("1.25×", "1.25× (Media Controls Menu Playback Speed)", "1.25× media controls context menu playback speed label"), playbackRate == 1.25), |
| createMenuItem(PlaybackSpeed::x1_5, WEB_UI_STRING_KEY("1.5×", "1.5× (Media Controls Menu Playback Speed)", "1.5× media controls context menu playback speed label"), playbackRate == 1.5), |
| createMenuItem(PlaybackSpeed::x2_0, WEB_UI_STRING_KEY("2×", "2× (Media Controls Menu Playback Speed)", "2× media controls context menu playback speed label"), playbackRate == 2.0), |
| })); |
| } |
| |
| #if ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| if ((items.size() == 1 && items[0].type() == SubmenuType) || optionsJSONObject->getBoolean("promoteSubMenus"_s).value_or(false)) { |
| for (auto&& item : std::exchange(items, { })) { |
| if (!items.isEmpty()) |
| items.append({ SeparatorType, invalidMenuItemIdentifier, /* title */ nullString() }); |
| |
| ASSERT(item.type() == SubmenuType); |
| items.append({ ActionType, invalidMenuItemIdentifier, item.title(), /* enabled */ false, /* checked */ false }); |
| items.appendVector(WTF::map(item.subMenuItems(), [] (const auto& item) -> ContextMenuItem { |
| // The disabled inline item used instead of an actual submenu should be indented less than the submenu items. |
| constexpr unsigned indentationLevel = 1; |
| if (item.type() == SubmenuType) |
| return { item.action(), item.title(), item.enabled(), item.checked(), item.subMenuItems(), indentationLevel }; |
| return { item.type(), item.action(), item.title(), item.enabled(), item.checked(), indentationLevel }; |
| })); |
| } |
| } |
| #endif // ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| |
| if (items.isEmpty()) |
| return false; |
| |
| ASSERT(!idMap.isEmpty()); |
| |
| m_showMediaControlsContextMenuCallback = WTFMove(callback); |
| |
| auto handleItemSelected = [weakThis = WeakPtr { *this }, idMap = WTFMove(idMap)] (MenuItemIdentifier selectedItemID) { |
| if (!weakThis) |
| return; |
| Ref strongThis = *weakThis; |
| |
| auto invokeCallbackAtScopeExit = makeScopeExit([strongThis] { |
| if (auto showMediaControlsContextMenuCallback = std::exchange(strongThis->m_showMediaControlsContextMenuCallback, nullptr)) |
| showMediaControlsContextMenuCallback->handleEvent(); |
| }); |
| |
| if (selectedItemID == invalidMenuItemIdentifier) |
| return; |
| |
| if (!strongThis->m_mediaElement) |
| return; |
| auto& mediaElement = *strongThis->m_mediaElement; |
| |
| UserGestureIndicator gestureIndicator(ProcessingUserGesture, &mediaElement.document()); |
| |
| auto selectedItem = idMap.get(selectedItemID); |
| WTF::switchOn(selectedItem, |
| #if ENABLE(VIDEO_PRESENTATION_MODE) |
| [&] (PictureInPictureTag) { |
| // Media controls are not shown when in PiP so we can assume that we're not in PiP. |
| downcast<HTMLVideoElement>(mediaElement).webkitSetPresentationMode(HTMLVideoElement::VideoPresentationMode::PictureInPicture); |
| }, |
| #endif // ENABLE(VIDEO_PRESENTATION_MODE) |
| [&] (RefPtr<AudioTrack>& selectedAudioTrack) { |
| for (auto& track : idMap.values()) { |
| if (auto* audioTrack = std::get_if<RefPtr<AudioTrack>>(&track)) |
| (*audioTrack)->setEnabled(*audioTrack == selectedAudioTrack); |
| } |
| }, |
| [&] (RefPtr<TextTrack>& selectedTextTrack) { |
| for (auto& track : idMap.values()) { |
| if (auto* textTrack = std::get_if<RefPtr<TextTrack>>(&track)) |
| (*textTrack)->setMode(TextTrack::Mode::Disabled); |
| } |
| mediaElement.setSelectedTextTrack(selectedTextTrack.get()); |
| }, |
| [&] (RefPtr<VTTCue>& cue) { |
| mediaElement.setCurrentTime(cue->startMediaTime()); |
| }, |
| [&] (PlaybackSpeed playbackSpeed) { |
| switch (playbackSpeed) { |
| case PlaybackSpeed::x0_5: |
| mediaElement.setDefaultPlaybackRate(0.5); |
| mediaElement.setPlaybackRate(0.5); |
| return; |
| |
| case PlaybackSpeed::x1_0: |
| mediaElement.setDefaultPlaybackRate(1.0); |
| mediaElement.setPlaybackRate(1.0); |
| return; |
| |
| case PlaybackSpeed::x1_25: |
| mediaElement.setDefaultPlaybackRate(1.25); |
| mediaElement.setPlaybackRate(1.25); |
| return; |
| |
| case PlaybackSpeed::x1_5: |
| mediaElement.setDefaultPlaybackRate(1.5); |
| mediaElement.setPlaybackRate(1.5); |
| return; |
| |
| case PlaybackSpeed::x2_0: |
| mediaElement.setDefaultPlaybackRate(2.0); |
| mediaElement.setPlaybackRate(2.0); |
| return; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| } |
| ); |
| |
| }; |
| |
| auto bounds = target.boundsInRootViewSpace(); |
| #if USE(UICONTEXTMENU) |
| page->chrome().client().showMediaControlsContextMenu(bounds, WTFMove(items), WTFMove(handleItemSelected)); |
| #elif ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS) |
| target.addEventListener(eventNames().contextmenuEvent, MediaControlsContextMenuEventListener::create(MediaControlsContextMenuProvider::create(WTFMove(items), WTFMove(handleItemSelected))), { /*capture */ true, /* passive */ std::nullopt, /* once */ true }); |
| page->contextMenuController().showContextMenuAt(*target.document().frame(), bounds.center()); |
| #endif |
| |
| return true; |
| #else // USE(UICONTEXTMENU) || (ENABLE(CONTEXT_MENUS) && USE(ACCESSIBILITY_CONTEXT_MENUS)) |
| return false; |
| #endif |
| } |
| |
| #endif // ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) |
| |
| #endif // ENABLE(MODERN_MEDIA_CONTROLS) |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(VIDEO) |