diff options
Diffstat (limited to 'Source/WebCore/Modules/mediacontrols')
11 files changed, 7887 insertions, 0 deletions
diff --git a/Source/WebCore/Modules/mediacontrols/MediaControlsHost.cpp b/Source/WebCore/Modules/mediacontrols/MediaControlsHost.cpp new file mode 100644 index 000000000..8d8eeff0c --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/MediaControlsHost.cpp @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2013, 2014 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(MEDIA_CONTROLS_SCRIPT) + +#include "MediaControlsHost.h" + +#include "CaptionUserPreferences.h" +#include "Element.h" +#include "HTMLMediaElement.h" +#include "Logging.h" +#include "MediaControlElements.h" +#include "Page.h" +#include "PageGroup.h" +#include "RenderTheme.h" +#include "TextTrack.h" +#include "TextTrackList.h" +#include "UUID.h" +#include <runtime/JSCJSValueInlines.h> + +namespace WebCore { + +const AtomicString& MediaControlsHost::automaticKeyword() +{ + static NeverDestroyed<const AtomicString> automatic("automatic", AtomicString::ConstructFromLiteral); + return automatic; +} + +const AtomicString& MediaControlsHost::forcedOnlyKeyword() +{ + static NeverDestroyed<const AtomicString> forcedOn("forced-only", AtomicString::ConstructFromLiteral); + return forcedOn; +} + +const AtomicString& MediaControlsHost::alwaysOnKeyword() +{ + static NeverDestroyed<const AtomicString> alwaysOn("always-on", AtomicString::ConstructFromLiteral); + return alwaysOn; +} + +const AtomicString& MediaControlsHost::manualKeyword() +{ + static NeverDestroyed<const AtomicString> alwaysOn("manual", AtomicString::ConstructFromLiteral); + return alwaysOn; +} + + +Ref<MediaControlsHost> MediaControlsHost::create(HTMLMediaElement* mediaElement) +{ + return adoptRef(*new MediaControlsHost(mediaElement)); +} + +MediaControlsHost::MediaControlsHost(HTMLMediaElement* mediaElement) + : m_mediaElement(mediaElement) +{ + ASSERT(mediaElement); +} + +MediaControlsHost::~MediaControlsHost() +{ +} + +Vector<RefPtr<TextTrack>> MediaControlsHost::sortedTrackListForMenu(TextTrackList& trackList) +{ + Page* page = m_mediaElement->document().page(); + if (!page) + return { }; + + return page->group().captionPreferences().sortedTrackListForMenu(&trackList); +} + +Vector<RefPtr<AudioTrack>> MediaControlsHost::sortedTrackListForMenu(AudioTrackList& trackList) +{ + Page* page = m_mediaElement->document().page(); + if (!page) + return { }; + + return page->group().captionPreferences().sortedTrackListForMenu(&trackList); +} + +String MediaControlsHost::displayNameForTrack(const std::optional<TextOrAudioTrack>& track) +{ + if (!track) + return emptyString(); + + Page* page = m_mediaElement->document().page(); + if (!page) + return emptyString(); + + return WTF::visit([&page](auto& track) { + return page->group().captionPreferences().displayNameForTrack(track.get()); + }, track.value()); +} + +TextTrack* MediaControlsHost::captionMenuOffItem() +{ + return TextTrack::captionMenuOffItem(); +} + +TextTrack* MediaControlsHost::captionMenuAutomaticItem() +{ + return TextTrack::captionMenuAutomaticItem(); +} + +AtomicString MediaControlsHost::captionDisplayMode() const +{ + Page* page = m_mediaElement->document().page(); + if (!page) + return emptyAtom; + + switch (page->group().captionPreferences().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) +{ + m_mediaElement->setSelectedTextTrack(track); +} + +Element* MediaControlsHost::textTrackContainer() +{ + if (!m_textTrackContainer) { + m_textTrackContainer = MediaControlTextTrackContainerElement::create(m_mediaElement->document()); + m_textTrackContainer->setMediaController(m_mediaElement); + } + return m_textTrackContainer.get(); +} + +void MediaControlsHost::updateTextTrackContainer() +{ + if (m_textTrackContainer) + m_textTrackContainer->updateDisplay(); +} + +void MediaControlsHost::enteredFullscreen() +{ + if (m_textTrackContainer) + m_textTrackContainer->enteredFullscreen(); +} + +void MediaControlsHost::exitedFullscreen() +{ + if (m_textTrackContainer) + m_textTrackContainer->exitedFullscreen(); +} + +void MediaControlsHost::updateCaptionDisplaySizes() +{ + if (m_textTrackContainer) + m_textTrackContainer->updateSizes(true); +} + +bool MediaControlsHost::allowsInlineMediaPlayback() const +{ + return !m_mediaElement->mediaSession().requiresFullscreenForVideoPlayback(*m_mediaElement); +} + +bool MediaControlsHost::supportsFullscreen() const +{ + return m_mediaElement->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard); +} + +bool MediaControlsHost::isVideoLayerInline() const +{ + return m_mediaElement->isVideoLayerInline(); +} + +bool MediaControlsHost::isInMediaDocument() const +{ + return m_mediaElement->document().isMediaDocument(); +} + +void MediaControlsHost::setPreparedToReturnVideoLayerToInline(bool value) +{ + m_mediaElement->setPreparedToReturnVideoLayerToInline(value); +} + +bool MediaControlsHost::userGestureRequired() const +{ + return !m_mediaElement->mediaSession().playbackPermitted(*m_mediaElement); +} + +String MediaControlsHost::externalDeviceDisplayName() const +{ +#if ENABLE(WIRELESS_PLAYBACK_TARGET) + MediaPlayer* 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 + MediaPlayer* player = m_mediaElement->player(); + if (!player) { + LOG(Media, "MediaControlsHost::externalDeviceType - returning \"none\" because player is NULL"); + return DeviceType::None; + } + + switch (player->wirelessPlaybackTargetType()) { + case MediaPlayer::TargetTypeNone: + return DeviceType::None; + case MediaPlayer::TargetTypeAirPlay: + return DeviceType::Airplay; + case MediaPlayer::TargetTypeTVOut: + return DeviceType::Tvout; + } + + ASSERT_NOT_REACHED(); + return DeviceType::None; +#endif +} + +bool MediaControlsHost::controlsDependOnPageScaleFactor() const +{ + return m_mediaElement->mediaControlsDependOnPageScaleFactor(); +} + +void MediaControlsHost::setControlsDependOnPageScaleFactor(bool value) +{ + m_mediaElement->setMediaControlsDependOnPageScaleFactor(value); +} + +String MediaControlsHost::generateUUID() const +{ + return createCanonicalUUIDString(); +} + +String MediaControlsHost::shadowRootCSSText() const +{ + Page* page = m_mediaElement->document().page(); + if (!page) + return emptyString(); + return RenderTheme::themeForPage(page)->modernMediaControlsStyleSheet(); +} + +String MediaControlsHost::base64StringForIconAndPlatform(const String& iconName, const String& platform) const +{ + Page* page = m_mediaElement->document().page(); + if (!page) + return emptyString(); + return RenderTheme::themeForPage(page)->mediaControlsBase64StringForIconAndPlatform(iconName, platform); +} + +} + +#endif diff --git a/Source/WebCore/Modules/mediacontrols/MediaControlsHost.h b/Source/WebCore/Modules/mediacontrols/MediaControlsHost.h new file mode 100644 index 000000000..1f034e522 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/MediaControlsHost.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2013, 2014 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. + */ + +#pragma once + +#if ENABLE(MEDIA_CONTROLS_SCRIPT) + +#include <bindings/ScriptObject.h> +#include <wtf/RefCounted.h> +#include <wtf/Variant.h> +#include <wtf/Vector.h> +#include <wtf/text/WTFString.h> + +namespace WebCore { + +class AudioTrack; +class AudioTrackList; +class Element; +class HTMLMediaElement; +class MediaControlTextTrackContainerElement; +class TextTrack; +class TextTrackList; + +class MediaControlsHost : public RefCounted<MediaControlsHost> { +public: + static Ref<MediaControlsHost> create(HTMLMediaElement*); + ~MediaControlsHost(); + + static const AtomicString& automaticKeyword(); + static const AtomicString& forcedOnlyKeyword(); + static const AtomicString& alwaysOnKeyword(); + static const AtomicString& manualKeyword(); + + Vector<RefPtr<TextTrack>> sortedTrackListForMenu(TextTrackList&); + Vector<RefPtr<AudioTrack>> sortedTrackListForMenu(AudioTrackList&); + + using TextOrAudioTrack = WTF::Variant<RefPtr<TextTrack>, RefPtr<AudioTrack>>; + String displayNameForTrack(const std::optional<TextOrAudioTrack>&); + + TextTrack* captionMenuOffItem(); + TextTrack* captionMenuAutomaticItem(); + AtomicString captionDisplayMode() const; + void setSelectedTextTrack(TextTrack*); + Element* textTrackContainer(); + void updateTextTrackContainer(); + bool allowsInlineMediaPlayback() const; + bool supportsFullscreen() const; + bool isVideoLayerInline() const; + bool isInMediaDocument() const; + bool userGestureRequired() const; + void setPreparedToReturnVideoLayerToInline(bool); + + void updateCaptionDisplaySizes(); + void enteredFullscreen(); + void exitedFullscreen(); + + String externalDeviceDisplayName() const; + + enum class DeviceType { None, Airplay, Tvout }; + DeviceType externalDeviceType() const; + + bool controlsDependOnPageScaleFactor() const; + void setControlsDependOnPageScaleFactor(bool v); + + String generateUUID() const; + + String shadowRootCSSText() const; + String base64StringForIconAndPlatform(const String& iconName, const String& platform) const; + +private: + MediaControlsHost(HTMLMediaElement*); + + HTMLMediaElement* m_mediaElement; + RefPtr<MediaControlTextTrackContainerElement> m_textTrackContainer; +}; + +} + +#endif diff --git a/Source/WebCore/Modules/mediacontrols/MediaControlsHost.idl b/Source/WebCore/Modules/mediacontrols/MediaControlsHost.idl new file mode 100644 index 000000000..3e4fcb670 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/MediaControlsHost.idl @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013, 2014 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. + */ + +enum DeviceType { + "none", + "airplay", + "tvout" +}; + +[ + Conditional=MEDIA_CONTROLS_SCRIPT, + ImplementationLacksVTable, + NoInterfaceObject, +] interface MediaControlsHost { + sequence<TextTrack> sortedTrackListForMenu(TextTrackList trackList); + sequence<AudioTrack> sortedTrackListForMenu(AudioTrackList trackList); + DOMString displayNameForTrack((TextTrack or AudioTrack)? track); + readonly attribute TextTrack captionMenuOffItem; + readonly attribute TextTrack captionMenuAutomaticItem; + readonly attribute DOMString captionDisplayMode; + void setSelectedTextTrack(TextTrack? track); + void setPreparedToReturnVideoLayerToInline(boolean prepared); + readonly attribute HTMLElement textTrackContainer; + readonly attribute boolean allowsInlineMediaPlayback; + readonly attribute boolean supportsFullscreen; + readonly attribute boolean isVideoLayerInline; + readonly attribute boolean userGestureRequired; + readonly attribute boolean isInMediaDocument; + + readonly attribute DOMString externalDeviceDisplayName; + readonly attribute DeviceType externalDeviceType; + + attribute boolean controlsDependOnPageScaleFactor; + + void updateTextTrackContainer(); + void enteredFullscreen(); + void exitedFullscreen(); + + DOMString generateUUID(); + + [EnabledAtRuntime=ModernMediaControls] readonly attribute DOMString shadowRootCSSText; + [EnabledAtRuntime=ModernMediaControls] DOMString base64StringForIconAndPlatform(DOMString iconName, DOMString platform); +}; diff --git a/Source/WebCore/Modules/mediacontrols/assets-apple-iOS.svg b/Source/WebCore/Modules/mediacontrols/assets-apple-iOS.svg new file mode 100644 index 000000000..db2a2d334 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/assets-apple-iOS.svg @@ -0,0 +1,52 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190 44"> + + <!-- + NOTE: THIS FILE IS NOT PART OF THE BUILD PROCESS. EDITS + HERE ARE NOT AUTOMATICALLY COPIED TO THE CSS FILES. + + This file holds the raw assets used in mediaControlsApple.css. + This is not part of the build process at the moment, but manually + copied into the CSS. However, editing from CSS is a burden, so + the idea is that you develop here and then paste into CSS. + --> + + <!-- PLAY --> + + <g transform="translate(0 0)"> + <rect width="44" height="44" fill="rgb(255, 255, 200)"/> + <svg width="44" height="44"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"> + <path fill="rgba(0, 0, 0, 0.8)" d="M10,11 32,22 10,33z"/> + </svg> + </svg> + </g> + + <!-- PAUSE --> + + <g transform="translate(46 0)"> + <rect width="44" height="44" fill="rgb(255, 255, 200)"/> + <svg width="44" height="44"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"> + <g fill="rgba(0, 0, 0, 0.8)"> + <rect x="13" y="11" width="6" height="22"/> + <rect x="24" y="11" width="6" height="22"/> + </g> + </svg> + </svg> + </g> + + <!-- FULLSCREEN --> + + <g transform="translate(92 0)"> + <rect width="44" height="44" fill="rgb(255, 255, 200)"/> + <svg width="44" height="44"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"> + <g stroke="rgba(0, 0, 0, 0.8)" fill="none"> + <path d="M 14,20 v -6 h 6 M 14,14 l 6,6"/> + <path d="M 30,24 v 6 h -6 M 30,30 l -6,-6"/> + </g> + </svg> + </svg> + </g> + +</svg>
\ No newline at end of file diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsApple.css b/Source/WebCore/Modules/mediacontrols/mediaControlsApple.css new file mode 100644 index 000000000..b7884d285 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsApple.css @@ -0,0 +1,1153 @@ +/* + * Copyright (C) 2013, 2014, 2015 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, + * 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. + */ + +audio { + width: 200px; + height: 25px; +} + +body:-webkit-full-page-media { + background-color: rgb(38, 38, 38); +} + +video:-webkit-full-page-media { + margin: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +video:-webkit-full-page-media::-webkit-media-controls-panel { + bottom: 0px; +} + +video:-webkit-full-page-media::-webkit-media-controls-panel.no-video { + opacity: 1; +} + +::-webkit-media-controls { + width: inherit; + height: inherit; + position: relative; + display: -webkit-flex !important; + -webkit-align-items: stretch; + -webkit-justify-content: flex-end; + -webkit-flex-direction: column; + font: -webkit-small-control; + white-space: nowrap; + -webkit-font-smoothing: subpixel-antialiased; +} + +::-webkit-media-controls.placeholder-showing { + overflow: hidden; +} + +video::-webkit-media-text-track-container, +audio::-webkit-media-text-track-container { + position: relative; + -webkit-flex: 1 1 auto; +} + +video::-webkit-media-controls-panel, +audio::-webkit-media-controls-panel { + box-sizing: border-box; + position: relative; + bottom: 0; + width: 100%; + min-height: 25px; + height: 25px; + line-height: 25px; + -webkit-user-select: none; + -webkit-user-drag: element; + background-color: transparent; + + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-user-select: none; + + direction: ltr; + + transition: opacity 0.25s linear; + + -webkit-text-zoom: reset +} + +video::-webkit-media-controls-panel { + opacity: 0; + padding-top: 20px; + min-height: 45px; + height: 45px; +} + +video::-webkit-media-controls-panel.show, +video::-webkit-media-controls-panel.paused, +video::-webkit-media-controls-panel:hover { + cursor: inherit; + opacity: 1; +} + +video::-webkit-media-controls-panel.picture-in-picture { + pointer-events: none; +} + +audio::-webkit-media-show-controls { + display: none !important; +} +video::-webkit-media-show-controls { + position: absolute; + left: 0; + bottom: 0; + display: block; + margin: 0; + padding: 0; + width: 100%; + height: 10px; + opacity: 0; + border: 0; + background: none; + -webkit-appearance: none; +} + +video::-webkit-media-controls-panel-background-container, +audio::-webkit-media-controls-panel-background-container { + position: absolute; + left: 0; + top: 0; + width: 100%; +} + +video::-webkit-media-controls-panel-background-container { + z-index: 0; +} + +video::-webkit-media-controls-panel-background-container { + min-height: 45px; + height: 45px; + -webkit-clip-path: inset(20px 0px 0px 0px); +} + +video::-webkit-media-controls-panel-tint, +audio::-webkit-media-controls-panel-tint { + position: absolute; + left: 0; + top: 0; + width: 100%; + background-color: transparent; +} + +video::-webkit-media-controls-panel-tint { + min-height: 45px; + height: 45px; + background-color: rgb(41, 41, 41); + mix-blend-mode: lighten; +} + +video::-webkit-media-controls-panel-background, +audio::-webkit-media-controls-panel-background { + position: absolute; + left: 0; + top: 0; + width: 100%; + min-height: 25px; + height: 25px; + background-color: rgb(41, 41, 41); +} + +video::-webkit-media-controls-panel-background { + min-height: 45px; + height: 45px; + background-color: rgba(30, 30, 30, 0.45); + -webkit-backdrop-filter: saturate(180%) blur(20px); +} + +video::-webkit-media-controls-panel button, +audio::-webkit-media-controls-panel button { + -webkit-appearance: none; + display: block; + padding: 0; + border: 0; + height: 15px; + background-color: transparent; + color: white; + background-origin: content-box; + background-repeat: no-repeat; + background-position: center; +} + +audio::-webkit-media-controls-panel button { + z-index: 0; +} + +video::-webkit-media-controls-panel button { + mix-blend-mode: plus-lighter; + -webkit-transform: translateZ(0); +} + +video::-webkit-media-controls-panel button:focus, +audio::-webkit-media-controls-panel button:focus { + outline: 0; +} + +video::-webkit-media-controls-rewind-button, +audio::-webkit-media-controls-rewind-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 17" fill="rgba(255,255,255,0.45)"><path d="m 7.9131,2 0,-1.548 -2.586,2.155 0,-2.171 -2.582,2.208 2.582,2.175 0,-2.139 2.586,2.155 0,-1.276 c 3.138,0.129 5.491,2.681 5.543,5.838 l -1.031,0 0.016,0.215 1.015,0 c -0.06,3.19 -2.629,5.765 -5.819,5.833 l 0,-1.018 -0.214,0 0,1.021 c -3.21,-0.047 -5.801,-2.631 -5.862,-5.836 l 1.045,0 -0.016,-0.215 -1.045,0 c -0.052,-0.288 -0.318,-0.654 -0.766,-0.654 -0.538,0 -0.755,0.484 -0.755,0.75 0,4.146 3.331,7.506 7.476,7.506 4.146,0 7.506,-3.36 7.506,-7.506 0,-4.059 -3.066,-7.357 -7.093,-7.493"/><path d="m 5.1729,11.0518 c -0.38,0 -0.668,-0.129 -0.945,-0.366 -0.083,-0.071 -0.186,-0.134 -0.338,-0.134 -0.277,0 -0.511,0.238 -0.511,0.521 0,0.154 0.083,0.301 0.179,0.383 0.394,0.346 0.911,0.563 1.601,0.563 1.077,0 1.739,-0.451 1.739,-1.608 l 0,-0.013 c 0,-0.911 -0.641,-1.265 -1.296,-1.376 l 0.945,-0.919 c 0.193,-0.19 0.317,-0.337 0.317,-0.604 0,-0.294 -0.228,-0.477 -0.538,-0.477 l -2.354,0 c -0.248,0 -0.455,0.21 -0.455,0.464 0,0.253 0.207,0.463 0.455,0.463 l 1.485,0 -0.939,0.961 c -0.166,0.169 -0.228,0.295 -0.228,0.444 0,0.25 0.207,0.463 0.455,0.463 l 0.166,0 c 0.594,0 0.945,0.222 0.945,0.624 l 0,0.012 c 0,0.367 -0.282,0.599 -0.453,0.599"/><path d="m 10.354,9.5342 c 0,0.876 -0.379,1.525 -0.979,1.525 -0.599,0 -0.992,-0.655 -0.992,-1.539 l 0,-0.012 c 0,-0.884 0.388,-1.527 0.979,-1.527 0.592,0 0.992,0.663 0.992,1.539 l 0,0.014 z m -0.979,-2.512 c -1.197,0 -2.008,1.097 -2.008,2.498 l 0,0.014 c 0,1.401 0.792,2.484 1.995,2.484 1.205,0 2.01,-1.097 2.01,-2.498 l 0,-0.012 c 0,-1.402 -0.805,-2.486 -1.997,-2.486"/></svg>'); + width: 16px; + min-width: 16px; + height: 18px; + margin-bottom: 1px; + margin-left: 8px; + margin-right: 8px; +} + +video::-webkit-media-controls-play-button, +audio::-webkit-media-controls-play-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 15" fill="rgba(255,255,255,0.45)"><rect x="0" y="1" width="4" height="13"/><rect x="8" y="1" width="4" height="13"/></svg>'); + margin-left: 8px; + margin-right: 8px; + width: 12px; + min-width: 12px; +} + +video::-webkit-media-controls-play-button.paused, +audio::-webkit-media-controls-play-button.paused { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 15" fill="rgba(255,255,255,0.45)"><path d="M 0,1 12,7.5 0,14 z"/></svg>'); + width: 12px; +} + +video::-webkit-media-controls-panel .mute-box, +audio::-webkit-media-controls-panel .mute-box { + width: 14px; + min-width: 14px; + height: 25px; + margin-right: 8px; + margin-left: 8px; + position: relative; + display: -webkit-flex; + -webkit-flex-direction: column; + -webkit-justify-content: center; + -webkit-align-items: center; +} + +video::-webkit-media-controls-mute-button, +audio::-webkit-media-controls-mute-button, +video::-webkit-media-controls-volume-max-button { + width: 14px; + min-width: 14px; + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="rgba(255,255,255,0.45)" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="rgba(255,255,255,0.45)" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="rgba(255,255,255,0.45)" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/><path d="M 9,5.5 C 10.75,7 10.75,9 9,10.5"/><path d="M 10,3.5 C 13,4.5 13,11.5 10,12.5" /><path d="M 11,1.5 C 15.67,3.5 15.67,12.5 11,14.5"/></svg>'); +} + +video::-webkit-media-controls-panel .volume-box, +audio::-webkit-media-controls-panel .volume-box { + position: absolute; + box-sizing: border-box; + width: 63px; + bottom: 5px; + left: -25px; + + -webkit-clip-path: inset(20px 20px 20px 20px round 4px 4px 0px 0px); + background-color: transparent; + overflow: hidden; + + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-justify-content: flex-end; + + opacity: 0; + /* make zero height (rather than display:none) for AX and FKA */ + height: 0px; /* will become 116px when shown */ + +} + +audio::-webkit-media-controls-panel .volume-box { + background-color: black; +} + +video::-webkit-media-controls-volume-slider-container-background, +audio::-webkit-media-controls-volume-slider-container-background { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + background-color: rgba(30, 30, 30, 0.45); +} + +video::-webkit-media-controls-volume-slider-container-background { + background-color: rgba(30, 30, 30, 0.45); + -webkit-backdrop-filter: saturate(180%) blur(20px); +} + +video::-webkit-media-controls-volume-slider-container-tint, +audio::-webkit-media-controls-volume-slider-container-tint { + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + background-color: rgb(41, 41, 41); +} + +video::-webkit-media-controls-volume-slider-container-tint { + mix-blend-mode: lighten; +} + +/* FIXME: needs CSS4 !subject selector to show when slider inside .volume-box is focused */ +video::-webkit-media-controls-panel .mute-box:hover .volume-box, +video::-webkit-media-controls-panel .volume-box:hover, +video::-webkit-media-controls-panel .volume-box:active, +audio::-webkit-media-controls-panel .mute-box:hover .volume-box, +audio::-webkit-media-controls-panel .volume-box:hover, +audio::-webkit-media-controls-panel .volume-box:active { + opacity: 1; + /* resize to usable amount (rather than display:none) for AX and FKA */ + height: 116px; +} + +audio::-webkit-media-controls-volume-slider, +video::-webkit-media-controls-volume-slider { + -webkit-appearance: none !important; + box-sizing: border-box !important; + height: 9px !important; + min-width: 64px !important; + width: 64px !important; + padding: 0 !important; + margin: 0 !important; + + background-color: transparent !important; + background-size: 100% 100%; + background-repeat: no-repeat; + -webkit-transform-origin: 0 0; + -webkit-transform: rotate(-90deg) translateY(28px) translateX(-40px); +} + +video::-webkit-media-controls-volume-slider { + mix-blend-mode: plus-lighter; +} + +video::-webkit-media-controls-volume-slider::-webkit-slider-thumb, +audio::-webkit-media-controls-volume-slider::-webkit-slider-thumb { + -webkit-appearance: none !important; + width: 7px !important; + height: 7px !important; + visibility: hidden; +} + +video::-webkit-media-controls-mute-button.muted, +audio::-webkit-media-controls-mute-button.muted, +video::-webkit-media-controls-volume-min-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="rgba(255,255,255,0.45)" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="rgba(255,255,255,0.45)" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="rgba(255,255,255,0.45)" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/></svg>'); +} + +video::-webkit-media-controls-wireless-playback-picker-button, +audio::-webkit-media-controls-wireless-playback-picker-button { + margin-right: 8px; + margin-left: 8px; + width: 16px; + min-width: 16px; +} + +video::-webkit-media-controls-wireless-playback-picker-button.playing, +audio::-webkit-media-controls-wireless-playback-picker-button.playing { + mix-blend-mode: normal; +} + +video::-webkit-media-controls-toggle-closed-captions-button, +audio::-webkit-media-controls-toggle-closed-captions-button { + width: 16px; + min-width: 16px; + margin-right: 8px; + margin-left: 8px; + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 15" stroke="rgba(255,255,255,0.45)" fill="none"><defs> <clipPath id="cut-hole"><rect x="0" y="1" width="16" height="10"/><rect x="0" y="11" width="9" height="1"/><rect x="11" y="11" width="5" height="1"/></clipPath></defs> <rect x="0.5" y="1.5" rx="1" ry="1" width="15" height="10" clip-path="url(#cut-hole)"/><path d="M 2,6.5 h 4"/><path d="M 7,6.5 h 7"/><path d="M 3,8.5 h 2"/><path d="M 6,8.5 h 3"/><path d="M 10,8.5 h 3"/><path d="M 8.5,11.5 L 8.5,13.75 L 11,11"/></svg>'); + outline: 0; +} + +video::-webkit-media-controls-closed-captions-container, +audio::-webkit-media-controls-closed-captions-container { + -webkit-appearance: media-closed-captions-container; + position: absolute; + display: block; + right: 38px; + bottom: 29px; + max-width: calc(100% - 48px); /* right + 10px */ + max-height: calc(100% - 39px); /* bottom + 10px */ + overflow-x: hidden; + overflow-y: scroll; + background-color: rgba(0, 0, 0, 0.8); + border: 1px solid rgba(128, 128, 128, 0.75); + border-radius: 6px; + cursor: default; + z-index: 2; + text-align: initial; +} + +video::-webkit-media-controls-closed-captions-container .list, +audio::-webkit-media-controls-closed-captions-container .list { + display: block; + -webkit-user-select: none; +} + +video::-webkit-media-controls-closed-captions-container h3, +audio::-webkit-media-controls-closed-captions-container h3 { + margin: 0; + color: rgb(140, 140, 140); + text-shadow: 0 1px 0 black; + -webkit-margin-start: 16px; + padding-top: 4px; + font-size: 11px; +} + +video::-webkit-media-controls-closed-captions-container ul, +audio::-webkit-media-controls-closed-captions-container ul { + list-style-type: none; + margin: 0 0 8px 0; + padding: 0; +} + +video::-webkit-media-controls-closed-captions-container li, +audio::-webkit-media-controls-closed-captions-container li { + position: relative; + color: white; + background-image: none; + text-shadow: 0 1px 0 black; + margin: 0; + padding-left: 28px; + padding-right: 28px; + padding-top: 0.15em; + padding-bottom: 0.2em; + box-sizing: border-box; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; +} + +video::-webkit-media-controls-closed-captions-container li:focus, +audio::-webkit-media-controls-closed-captions-container li:focus { + outline: 0; + background-color: rgba(140, 140, 140, 0.5); +} + +video::-webkit-media-controls-closed-captions-container li:hover, +audio::-webkit-media-controls-closed-captions-container li:hover { + background-color: rgba(26, 68, 243, 0.6); +} + +video::-webkit-media-controls-closed-captions-container li .checkmark-container, +audio::-webkit-media-controls-closed-captions-container li .checkmark-container { + display: none; + position: absolute; + top: 0.25em; + left: 1em; + width: 1.1em; + height: 1.1em; +} + +video::-webkit-media-controls-closed-captions-container li.selected .checkmark-container, +audio::-webkit-media-controls-closed-captions-container li.selected .checkmark-container { + display: inline-block; + content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><polygon fill="rgb(163, 163, 163)" points="252.301,4.477 126.667,194.104 43.358,108.3 6.868,161.408 132.515,290.814 297.732,49.926"/></svg>'); +} + +video::-webkit-media-controls-closed-captions-container li.selected:hover .checkmark-container, +audio::-webkit-media-controls-closed-captions-container li.selected:hover .checkmark-container { + content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><polygon fill="rgba(255,255,255,0.45)" points="252.301,4.477 126.667,194.104 43.358,108.3 6.868,161.408 132.515,290.814 297.732,49.926"/></svg>'); +} + +video::-webkit-media-controls-panel .picture-in-picture-button { + margin-right: 7px; + margin-left: 8px; + margin-top: 1px; + width: 16px; + min-width: 18px; + height: 12px; + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="rgba(255,255,255,0.45)"><polygon points="6 8 1 8 1 1 14 1 14 6 15 6 15 0 0 0 0 9 6 9 6 8"/><rect x="7" y="7" width="9" height="5"/><polyline transform="translate(4, 4) rotate(180) translate(-4, -4)" points="5.33 2 5.33 3 3.67 3 5.67 5 5 5.67 3 3.67 3 5.33 2 5.33 2 2"/></svg>'); + } + +video::-webkit-media-controls-panel .picture-in-picture-button.return-from-picture-in-picture { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="rgba(255,255,255,0.45)"><polygon points="6 8 1 8 1 1 14 1 14 6 15 6 15 0 0 0 0 9 6 9 6 8"/><rect x="7" y="7" width="9" height="5"/><polyline points="5.33 2 5.33 3 3.67 3 5.67 5 5 5.67 3 3.67 3 5.33 2 5.33 2 2"/></svg>'); + } + +video::-webkit-media-controls-fullscreen-button, +audio::-webkit-media-controls-fullscreen-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 15" stroke="rgba(255,255,255,0.45)" fill="none"><path d="M 7,1.5 L 12.5,1.5 L 12.5,7"/><path d="M 0.5,8 L 0.5,13.5 L 6,13.5"/><path stroke-linecap="round" d="M 12.5,1.5 L 7.5,6.5"/><path stroke-linecap="round" d="M 0.5,13.5 L 5.5,8.5"/></svg>'); + margin-right: 7px; + margin-left: 8px; + width: 14px; + min-width: 14px; +} + +video::-webkit-media-controls-fullscreen-button.exit, +audio::-webkit-media-controls-fullscreen-button.exit { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 15" stroke="rgba(255,255,255,0.45)" fill="none"><path d="M 7.5,1 L 7.5,6.5 L 13,6.5"/><path d="M 0,8.5 L 5.5,8.5 L 5.5,14"/><path stroke-linecap="round" d="M 7.5,6.5 L 12.5,1.5"/><path stroke-linecap="round" d="M 5.5,8.5 L 0.5,13.5"/></svg>'); + margin-right: 11px; + margin-left: 0px; + margin-top: 6px; +} + +video::-webkit-media-controls-status-display, +audio::-webkit-media-controls-status-display { + cursor: default; + overflow: hidden; + color: rgb(156, 156, 156); + opacity: 0.99; + + letter-spacing: normal; + word-spacing: normal; + line-height: 25px; + text-transform: none; + text-indent: 0; + text-decoration: none; + text-align: left; + + padding: 0 12px; + + -webkit-flex: 1 1 0; +} + +video::-webkit-media-controls-status-display { + color: white; + opacity: .45; + mix-blend-mode: plus-lighter; +} + +video::-webkit-media-controls-timeline, +audio::-webkit-media-controls-timeline { + -webkit-appearance: none !important; + -webkit-flex: 1 1 0 !important; + height: 17px !important; + margin: 0 !important; + background-size: 100% 100% !important; + background-repeat: no-repeat; + background-color: transparent; +} + +video::-webkit-media-controls-timeline { + mix-blend-mode: plus-lighter; +} + +video::-webkit-media-controls-timeline::-webkit-slider-thumb, +audio::-webkit-media-controls-timeline::-webkit-slider-thumb { + -webkit-appearance: none !important; + width: 3px !important; + height: 15px !important; +} + +video::-webkit-media-controls-current-time-display, +video::-webkit-media-controls-time-remaining-display, +audio::-webkit-media-controls-current-time-display, +audio::-webkit-media-controls-time-remaining-display { + -webkit-user-select: none; + -webkit-flex: 0 0 0; + display: -webkit-flex; + cursor: default; + overflow: hidden; + color: rgb(156, 156, 156); + letter-spacing: normal; + word-spacing: normal; + line-height: normal; + text-transform: none; + text-indent: 0px; + text-decoration: none; + position: relative; + bottom: 0.5px; + font-family: -apple-system-monospaced-numbers; + font-size: 11px !important; + font-style: normal !important; + font-weight: normal !important; + -webkit-text-size-adjust: none; +} + +video::-webkit-media-controls-current-time-display, +video::-webkit-media-controls-time-remaining-display { + color: white; + opacity: .45; + mix-blend-mode: plus-lighter; +} + +video::-webkit-media-controls-current-time-display, +audio::-webkit-media-controls-current-time-display { + margin-left: 8px; + margin-right: 8px; + width: 32px; + min-width: 32px; + -webkit-justify-content: flex-end; +} + +video::-webkit-media-controls-time-remaining-display, +audio::-webkit-media-controls-time-remaining-display { + margin-left: 8px; + margin-right: 8px; + width: 37px; + min-width: 37px; + -webkit-justify-content: flex-start; +} + +video::-webkit-media-controls-time-remaining-display.five-digit-time, +audio::-webkit-media-controls-time-remaining-display.five-digit-time { + min-width: 47px; +} + +video::-webkit-media-controls-current-time-display.five-digit-time, +audio::-webkit-media-controls-current-time-display.five-digit-time { + min-width: 42px; +} + +video::-webkit-media-controls-time-remaining-display.six-digit-time, +audio::-webkit-media-controls-time-remaining-display.six-digit-time { + min-width: 54px; +} + +video::-webkit-media-controls-current-time-display.six-digit-time, +audio::-webkit-media-controls-current-time-display.six-digit-time { + min-width: 49px; +} + +video::-webkit-media-controls-timeline-container, +audio::-webkit-media-controls-timeline-container { + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-user-select: none; + -webkit-flex: 1 1 0; + min-width: 0; + position: relative; + padding: 0; +} + +video::-webkit-media-controls-panel .thumbnail-track, +audio::-webkit-media-controls-panel .thumbnail-track { + position: relative; + -webkit-flex: 1 1 0; + min-width: 0; + height: 17px; + margin: 0 2px; + display: -webkit-flex; + -webkit-align-items: stretch; + -webkit-flex-direction: column; +} + +video::-webkit-media-controls-panel .thumbnail, +audio::-webkit-media-controls-panel .thumbnail { + position: absolute; + opacity: 0; + transition: opacity 0.25s linear; + bottom: 15px; + width: 100px; + height: 58px; + margin-left: -50px; + border: 5px solid black; + box-shadow: 0 0 3px white; + border-radius: 3px; +} + +video::-webkit-media-controls-panel .thumbnail-image, +audio::-webkit-media-controls-panel .thumbnail-image { + width: 100%; + height: 100%; +} + +video::-webkit-media-controls-panel .thumbnail.show, +audio::-webkit-media-controls-panel .thumbnail.show { + opacity: 1; +} + +video::-webkit-media-controls-panel.hidden, +audio::-webkit-media-controls-panel.hidden { + display: none; +} + +video::-webkit-media-controls-panel .hidden, +audio::-webkit-media-controls-panel .hidden, +video::-webkit-media-controls-panel .dropped, +audio::-webkit-media-controls-panel .dropped { + display: none; +} + +/* Full Screen */ + +/* + Page stylesheets are not allowed to override the style of media + controls while in full screen mode, so many if not all the rules + defined in this section will be marked as !important to enforce + this restriction +*/ + +video:-webkit-full-screen::-webkit-media-controls-panel { + -webkit-align-items: flex-start !important; + -webkit-justify-content: flex-end !important; + -webkit-clip-path: inset(20px round 6px); + + width: 480px !important; + height: 104px !important; + margin: 0 auto 12px auto !important; + padding: 20px; + padding-top: 30px !important; + + background-color: transparent; + border-radius: 6px !important; + + transition: opacity 0.3s linear !important; +} + +video:-webkit-full-screen::-webkit-media-controls-panel-tint, +audio:-webkit-full-screen::-webkit-media-controls-panel-tint { + position: absolute; + left: 0; + top: 0; + width: 480px !important; + height: 104px !important; + border-radius: 6px !important; + background-color: rgb(41, 41, 41); + mix-blend-mode: lighten; +} + +video:-webkit-full-screen::-webkit-media-controls-panel-background, +audio:-webkit-full-screen::-webkit-media-controls-panel-background { + position: absolute; + left: 0; + top: 0; + width: 480px !important; + height: 104px !important; + border-radius: 6px !important; + background-color: rgba(30, 30, 30, 0.45); + -webkit-backdrop-filter: saturate(180%) blur(20px); +} + +video:-webkit-animating-full-screen-transition::-webkit-media-controls-panel { + opacity: 0 ! important; + transition: opacity 0 ! important; +} + +video:-webkit-full-screen::-webkit-media-controls-panel .volume-box { + -webkit-transform: none; + -webkit-clip-path: none; + opacity: 1; + left: 32px; + top: 35px; + width: 96px; + height: 17px; + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + background-color: transparent; + border: none; +} + +video:-webkit-full-screen::-webkit-media-controls-panel .volume-box:not(.uses-ltr-user-interface-layout-direction) { + transform: translateX(23px) scaleX(-1); +} + +video:-webkit-full-screen::-webkit-media-controls-volume-slider { + -webkit-transform: none; + background-color: transparent; + min-width: 60px !important; + width: 60px !important; + height: 9px !important; + margin: 0 !important; +} + +video:-webkit-full-screen::-webkit-media-controls-mute-button, +audio:-webkit-full-screen::-webkit-media-controls-mute-button, +video:-webkit-full-screen::-webkit-media-controls-volume-max-button { + width: 14px !important; + margin-left: 6px !important; + margin-bottom: 2px !important; + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="rgba(255,255,255,0.45)" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="rgba(255,255,255,0.45)" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="rgba(255,255,255,0.45)" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/><path d="M 9,5.5 C 10.75,7 10.75,9 9,10.5"/><path d="M 10,3.5 C 13,4.5 13,11.5 10,12.5" /><path d="M 11,1.5 C 15.67,3.5 15.67,12.5 11,14.5"/></svg>'); +} +video:-webkit-full-screen::-webkit-media-controls-mute-button, +audio:-webkit-full-screen::-webkit-media-controls-mute-button, +video:-webkit-full-screen::-webkit-media-controls-volume-min-button { + width: 14px !important; + margin-right: 2px !important; + margin-bottom: 2px !important; + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="rgba(255,255,255,0.45)" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="rgba(255,255,255,0.45)" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="rgba(255,255,255,0.45)" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/><path d="M 9,5.5 C 10.75,7 10.75,9 9,10.5"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-play-button { + position: absolute; + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 23" fill="rgba(255,255,255,0.45)"><path d="M 0,0 0,22 8,22 8,0 z"/><path d="M 13,0 13,22 21,22 21,0 z"/></svg>'); + + width: 21px; + height: 23px; + left: 230px; + top: 32px; + margin: 0; + padding: 0; +} + +video:-webkit-full-screen::-webkit-media-controls-play-button.paused { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 23" fill="rgba(255,255,255,0.45)"><path d="M 0,0 21,11.5 0,23 z"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-seek-back-button { + position: absolute; + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 15" fill="rgba(255,255,255,0.45)"><path d="M 24,0 12,7 24,15 z"/><path d="M 12,0 0,7 12,15 z"/></svg>'); + + width: 24px; + height: 15px; + left: 176px; + top: 36px; +} + +video:-webkit-full-screen::-webkit-media-controls-return-to-realtime-button { + position: absolute; + display: -webkit-flex; + width: 29px; + height: 16px; + left: 262px; + top: 13px; +} + +video:-webkit-full-screen::-webkit-media-controls-seek-forward-button { + position: absolute; + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 15" fill="rgba(255,255,255,0.45)"><path d="M 0,0 12,7 0,15 z"/><path d="M 12,0 24,7 12,15 z"/></svg>'); + + width: 24px; + height: 15px; + left: 275px; + top: 36px; +} + +video:-webkit-full-screen::-webkit-media-controls-timeline-container { + height: auto; + width: 440px; + position: absolute; + bottom: 28px; + left: 20px; +} + +video:-webkit-full-screen::-webkit-media-controls-current-time-display { + margin-left: 12px; +} + +video:-webkit-full-screen::-webkit-media-controls-time-remaining-display { + margin-right: 12px; +} + +video:-webkit-full-screen::-webkit-media-controls-status-display { + width: 440px; + position: absolute; + bottom: 25px; + text-align: center; + padding: 0; + left: 20px; +} + +video:-webkit-full-screen::-webkit-media-controls-toggle-closed-captions-button, +audio:-webkit-full-screen::-webkit-media-controls-toggle-closed-captions-button { + margin-top: 6px; + margin-right:24px; + margin-left: 0px; +} + +video:-webkit-full-screen::-webkit-media-controls-wireless-playback-picker-button, +audio:-webkit-full-screen::-webkit-media-controls-wireless-playback-picker-button { + margin-top: 6px; + margin-right:24px; + margin-left: 0px; +} + +video:-webkit-full-screen::-webkit-media-controls-closed-captions-container { + bottom: 100px; + right: calc(50% - 183px); /* 183px is 221px (half the media controller's width) minus 38px (the right position of the captions icon). */ + max-width: calc(50% + 173px); /* right + 10px */ + max-height: calc(100% - 124px); /* bottom + 10px */ +} + +video::-webkit-media-text-track-container { + position: relative; + width: 100%; + overflow: hidden; + padding-bottom: 5px; + z-index: 0; + + text-align: center; + color: rgba(255, 255, 255, 1); + + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0; + text-decoration: none; + pointer-events: none; + -webkit-user-select: none; + + -webkit-flex: 1 1; + + -webkit-line-box-contain: block inline-box replaced; +} + +video::cue { + background-color: rgba(0, 0, 0, 0.8); +} + +video::-webkit-media-text-track-display { + position: absolute; + overflow: hidden; + white-space: pre-wrap; + -webkit-box-sizing: border-box; + font: 22px sans-serif; +} + +video::-webkit-media-text-track-display-backdrop { + display: inline-block; +} + +video::cue(:future) { + color: gray; +} + +video::-webkit-media-text-track-container b { + font-weight: bold; +} + +video::-webkit-media-text-track-container u { + text-decoration: underline; +} + +video::-webkit-media-text-track-container i { + font-style: italic; +} + +video::-webkit-media-text-track-container .hidden, +audio::-webkit-media-text-track-container .hidden { + display: none; +} + +/* ============ ACTIVE VERSIONS OF ALL BUTTONS ============= */ + +video::-webkit-media-controls-mute-button:active, +audio::-webkit-media-controls-mute-button:active, +video::-webkit-media-controls-volume-max-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="white" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="white" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="white" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/><path d="M 9,5.5 C 10.75,7 10.75,9 9,10.5"/><path d="M 10,3.5 C 13,4.5 13,11.5 10,12.5" /><path d="M 11,1.5 C 15.67,3.5 15.67,12.5 11,14.5"/></svg>'); +} + +video::-webkit-media-controls-panel button.muted:active, +audio::-webkit-media-controls-panel button.muted:active, +video::-webkit-media-controls-volume-min-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="white" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="white" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="white" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/></svg>'); +} + +video::-webkit-media-controls-toggle-closed-captions-button:active, +audio::-webkit-media-controls-toggle-closed-captions-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 15" stroke="white" fill="none"><defs> <clipPath id="cut-hole"><rect x="0" y="1" width="16" height="10"/><rect x="0" y="11" width="9" height="1"/><rect x="11" y="11" width="5" height="1"/></clipPath></defs> <rect x="0.5" y="1.5" rx="1" ry="1" width="15" height="10" clip-path="url(#cut-hole)"/><path d="M 2,6.5 h 4"/><path d="M 7,6.5 h 7"/><path d="M 3,8.5 h 2"/><path d="M 6,8.5 h 3"/><path d="M 10,8.5 h 3"/><path d="M 8.5,11.5 L 8.5,13.75 L 11,11"/></svg>'); +} + +video::-webkit-media-controls-rewind-button:active, +audio::-webkit-media-controls-rewind-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 17" fill="white"><path d="m 7.9131,2 0,-1.548 -2.586,2.155 0,-2.171 -2.582,2.208 2.582,2.175 0,-2.139 2.586,2.155 0,-1.276 c 3.138,0.129 5.491,2.681 5.543,5.838 l -1.031,0 0.016,0.215 1.015,0 c -0.06,3.19 -2.629,5.765 -5.819,5.833 l 0,-1.018 -0.214,0 0,1.021 c -3.21,-0.047 -5.801,-2.631 -5.862,-5.836 l 1.045,0 -0.016,-0.215 -1.045,0 c -0.052,-0.288 -0.318,-0.654 -0.766,-0.654 -0.538,0 -0.755,0.484 -0.755,0.75 0,4.146 3.331,7.506 7.476,7.506 4.146,0 7.506,-3.36 7.506,-7.506 0,-4.059 -3.066,-7.357 -7.093,-7.493"/><path d="m 5.1729,11.0518 c -0.38,0 -0.668,-0.129 -0.945,-0.366 -0.083,-0.071 -0.186,-0.134 -0.338,-0.134 -0.277,0 -0.511,0.238 -0.511,0.521 0,0.154 0.083,0.301 0.179,0.383 0.394,0.346 0.911,0.563 1.601,0.563 1.077,0 1.739,-0.451 1.739,-1.608 l 0,-0.013 c 0,-0.911 -0.641,-1.265 -1.296,-1.376 l 0.945,-0.919 c 0.193,-0.19 0.317,-0.337 0.317,-0.604 0,-0.294 -0.228,-0.477 -0.538,-0.477 l -2.354,0 c -0.248,0 -0.455,0.21 -0.455,0.464 0,0.253 0.207,0.463 0.455,0.463 l 1.485,0 -0.939,0.961 c -0.166,0.169 -0.228,0.295 -0.228,0.444 0,0.25 0.207,0.463 0.455,0.463 l 0.166,0 c 0.594,0 0.945,0.222 0.945,0.624 l 0,0.012 c 0,0.367 -0.282,0.599 -0.453,0.599"/><path d="m 10.354,9.5342 c 0,0.876 -0.379,1.525 -0.979,1.525 -0.599,0 -0.992,-0.655 -0.992,-1.539 l 0,-0.012 c 0,-0.884 0.388,-1.527 0.979,-1.527 0.592,0 0.992,0.663 0.992,1.539 l 0,0.014 z m -0.979,-2.512 c -1.197,0 -2.008,1.097 -2.008,2.498 l 0,0.014 c 0,1.401 0.792,2.484 1.995,2.484 1.205,0 2.01,-1.097 2.01,-2.498 l 0,-0.012 c 0,-1.402 -0.805,-2.486 -1.997,-2.486"/></svg>'); +} + +video::-webkit-media-controls-panel button.paused:active, +audio::-webkit-media-controls-panel button.paused:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 15" fill="white"><path d="M 0,1 12,7.5 0,14 z"/></svg>'); +} + +video::-webkit-media-controls-play-button:active, +audio::-webkit-media-controls-play-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 15" fill="white"><rect x="0" y="1" width="4" height="13"/><rect x="8" y="1" width="4" height="13"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-mute-button:active, +audio:-webkit-full-screen::-webkit-media-controls-mute-button:active, +video:-webkit-full-screen::-webkit-media-controls-volume-max-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="white" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="white" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="white" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/><path d="M 9,5.5 C 10.75,7 10.75,9 9,10.5"/><path d="M 10,3.5 C 13,4.5 13,11.5 10,12.5" /><path d="M 11,1.5 C 15.67,3.5 15.67,12.5 11,14.5"/></svg>'); +} +video:-webkit-full-screen::-webkit-media-controls-mute-button:active, +audio:-webkit-full-screen::-webkit-media-controls-mute-button:active, +video:-webkit-full-screen::-webkit-media-controls-volume-min-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" stroke="white" fill="none" stroke-width="1.25" stroke-linecap="round"><defs><clipPath id="cut-hole"><rect x="3" y="0" width="3.5" height="15"/></clipPath></defs><path stroke="none" fill="white" shape-rendering="crispEdges" d="M 0,10 0,6 3,6 3,10 z"/><path stroke="none" fill="white" clip-path="url(#cut-hole)" shape-rendering="crispEdges" d="M 3.5,6 6.5,3 6.5,13 3.5,10 z"/><path d="M 9,5.5 C 10.75,7 10.75,9 9,10.5"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-play-button:active{ + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 23" fill="white"><path d="M 0,0 0,22 8,22 8,0 z"/><path d="M 13,0 13,22 21,22 21,0 z"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-panel button.paused:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 23" fill="white"><path d="M 0,0 21,11.5 0,23 z"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-seek-back-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 15" fill="white"><path d="M 24,0 12,7 24,15 z"/><path d="M 12,0 0,7 12,15 z"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-seek-forward-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 15" fill="white"><path d="M 0,0 12,7 0,15 z"/><path d="M 12,0 24,7 12,15 z"/></svg>'); +} + +video::-webkit-media-controls-fullscreen-button:active, +audio::-webkit-media-controls-fullscreen-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 15" stroke="white" fill="none"><path d="M 7,1.5 L 12.5,1.5 L 12.5,7"/><path d="M 0.5,8 L 0.5,13.5 L 6,13.5"/><path stroke-linecap="round" d="M 12.5,1.5 L 7.5,6.5"/><path stroke-linecap="round" d="M 0.5,13.5 L 5.5,8.5"/></svg>'); +} + +video::-webkit-media-controls-panel button.exit:active, +audio::-webkit-media-controls-panel button.exit:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 15" stroke="white" fill="none"><path d="M 7.5,1 L 7.5,6.5 L 13,6.5"/><path d="M 0,8.5 L 5.5,8.5 L 5.5,14"/><path stroke-linecap="round" d="M 7.5,6.5 L 12.5,1.5"/><path stroke-linecap="round" d="M 5.5,8.5 L 0.5,13.5"/></svg>'); +} + +video::-webkit-media-controls-wireless-playback-picker-button:active, +audio::-webkit-media-controls-wireless-playback-picker-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 15" stroke="white"><defs> <clipPath fill-rule="evenodd" id="cut-hole"><path d="M 0,0.5 L 16,0.5 L 16,15.5 L 0,15.5 z M 0,14.5 L 16,14.5 L 8,5 z"/></clipPath></defs><rect fill="none" clip-path="url(#cut-hole)" x="0.5" y="2" width="15" height="8"/><path stroke="none" fill="white" d="M 3.5,13.25 L 12.5,13.25 L 8,8 z"/></svg>'); +} + +video::-webkit-media-controls-panel .picture-in-picture-button:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="white"><polygon points="6 8 1 8 1 1 14 1 14 6 15 6 15 0 0 0 0 9 6 9 6 8"/><rect x="7" y="7" width="9" height="5"/><path d="M5.5,4.45a0.5,0.5,0,0,0-.5.5v1.3L2.58,3.83a0.5,0.5,0,0,0-.71.71L4.33,7H3A0.5,0.5,0,0,0,3,8H5.5A0.5,0.5,0,0,0,6,7.5V4.95A0.5,0.5,0,0,0,5.5,4.45Z" transform="translate(0 -2)"/></svg>'); +} + +video::-webkit-media-controls-panel .picture-in-picture-button.return-from-picture-in-picture:active { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="white"><polygon points="6 8 1 8 1 1 14 1 14 6 15 6 15 0 0 0 0 9 6 9 6 8"/><rect x="7" y="7" width="9" height="5"/><path d="M2.22,7.23a0.5,0.5,0,0,0,.5-0.5V5.43L5.15,7.85a0.5,0.5,0,0,0,.71-0.71L3.39,4.68H4.77a0.5,0.5,0,0,0,0-1H2.22a0.5,0.5,0,0,0-.5.5V6.73A0.5,0.5,0,0,0,2.22,7.23Z" transform="translate(0 -2)"/></svg>'); +} + +/* ==================== AIRPLAY PLACARD ==================== */ + +video::-webkit-media-controls-wireless-playback-status, +audio::-webkit-media-controls-wireless-playback-status { + display: block; + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 131 90"><g fill="none" stroke="-apple-system-gray" stroke-width="4"><rect x="2" y="2" width="127" height="76"/><line x1="30" y1="88" x2="101" y2="88"/></g></svg>'); + background-color: rgb(51, 51, 53); + background-repeat: no-repeat; + background-position: 50% calc(.5 * (100% - 25px) - 21pt); + background-size: 131px auto; + color: -apple-system-gray; + font-family: -apple-system; + vertical-align: text-bottom; +} + +video::-webkit-media-controls-wireless-playback-text, +audio::-webkit-media-controls-wireless-playback-text { + cursor: default; + position: absolute; + width: 100%; + top: calc(.5 * (100% - 25px) + (.5 * (90px + 42pt) - 42pt)); + -webkit-user-select: none; + margin: 0px; + height: 42pt; +} + +video::-webkit-media-controls-wireless-playback-text-top, +audio::-webkit-media-controls-wireless-playback-text-top { + position: absolute; + top: 15pt; + width: 100%; + line-height: 12pt; + height: 13pt; + font-size: 12pt; + text-align: center; + margin: 0px; +} + +video::-webkit-media-controls-wireless-playback-text-bottom, +audio::-webkit-media-controls-wireless-playback-text-bottom { + position: absolute; + bottom: 0; + left: 5%; + width: 90%; + line-height: 10pt; + height: 11pt; + font-size: 10pt; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; +} + +video::-webkit-media-controls-wireless-playback-status.small, +audio::-webkit-media-controls-wireless-playback-status.small { + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62 43"><g fill="none" stroke="-apple-system-gray" stroke-width="2"><rect x="1" y="1" width="60" height="36"/><line x1="14" y1="42" x2="48" y2="42"/></g></svg>'); + background-position: 50% calc(.5 * (100% - 25px) - 5pt); + background-size: 62px auto; +} + +video::-webkit-media-controls-wireless-playback-text-top.small, +audio::-webkit-media-controls-wireless-playback-text-top.small { + top: 4pt; +} + +video::-webkit-media-controls-wireless-playback-text-bottom.small, +audio::-webkit-media-controls-wireless-playback-text-bottom.small { + display: none; +} + +video::-webkit-media-controls-wireless-playback-status.picture-in-picture, +audio::-webkit-media-controls-wireless-playback-status.picture-in-picture +{ + background-size: 304px auto; + background-position: 50% calc(.5 * (100% - 25px)); + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 304 150"><g fill="none" stroke="-apple-system-gray" stroke-width="3"><path d="m 172,106 -81,0 c -3.5,0 -6,-2.5 -6,-6 l 0,-89 c 0,-3.5 2.5,-6 6,-6 l 122,0 c 3.5,0 6,2.5 6,6 l 0,57" /><path d="m 233,119 -53,0 c -3,0 -3,-0 -3,-3 l 0,-40 c 0,-3 0,-3 3,-3 l 53,0 c 3,0 3,0 3,3 l 0,40 c 0,3 0,3 -3,3 z" /></g></svg>'); +} + +video::-webkit-media-controls-wireless-playback-text-top.picture-in-picture, +audio::-webkit-media-controls-wireless-playback-text-top.picture-in-picture { + top: initial; + bottom: 0; +} + +video::-webkit-media-controls-wireless-playback-text-bottom.picture-in-picture, +audio::-webkit-media-controls-wireless-playback-text-bottom.picture-in-picture { + display: none; +} + +video::-webkit-media-controls-wireless-playback-status.hidden, +audio::-webkit-media-controls-wireless-playback-status.hidden { + display: none; +} + +video::-webkit-media-controls-panel.picture-in-picture { + opacity: 0; + pointer-events: none; +} + +video:-webkit-full-screen::-webkit-media-controls-panel .picture-in-picture-button { + margin-top: 7px; + margin-right: 24px; + margin-left: 0px; +} + +/* Time display clones that we use in updateLayoutForDisplayedWidth(). */ +::-webkit-media-controls-current-time-display.clone, +::-webkit-media-controls-time-remaining-display.clone { + position: absolute; + display: inline; + top: 100%; + mix-blend-mode: normal; +} diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js b/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js new file mode 100644 index 000000000..d1893734c --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js @@ -0,0 +1,2509 @@ +function createControls(root, video, host) +{ + return new Controller(root, video, host); +}; + +function Controller(root, video, host) +{ + this.video = video; + this.root = root; + this.host = host; + this.controls = {}; + this.listeners = {}; + this.isLive = false; + this.statusHidden = true; + this.hasWirelessPlaybackTargets = false; + this.canToggleShowControlsButton = false; + this.isListeningForPlaybackTargetAvailabilityEvent = false; + this.currentTargetIsWireless = false; + this.wirelessPlaybackDisabled = false; + this.isVolumeSliderActive = false; + this.currentDisplayWidth = 0; + this._scrubbing = false; + this._pageScaleFactor = 1; + + this.addVideoListeners(); + this.createBase(); + this.createControls(); + this.createTimeClones(); + this.updateBase(); + this.updateControls(); + this.updateDuration(); + this.updateProgress(); + this.updateTime(); + this.updateReadyState(); + this.updatePlaying(); + this.updateThumbnail(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + this.updateFullscreenButtons(); + this.updateVolume(); + this.updateHasAudio(); + this.updateHasVideo(); + this.updateWirelessTargetAvailable(); + this.updateWirelessPlaybackStatus(); + this.updatePictureInPicturePlaceholder(); + this.scheduleUpdateLayoutForDisplayedWidth(); + + this.listenFor(this.root, 'resize', this.handleRootResize); +}; + +/* Enums */ +Controller.InlineControls = 0; +Controller.FullScreenControls = 1; + +Controller.PlayAfterSeeking = 0; +Controller.PauseAfterSeeking = 1; + +/* Globals */ +Controller.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets. +Controller.gSimulatePictureInPictureAvailable = false; // Used for testing when picture-in-picture is not available. + +Controller.prototype = { + + /* Constants */ + HandledVideoEvents: { + loadstart: 'handleLoadStart', + error: 'handleError', + abort: 'handleAbort', + suspend: 'handleSuspend', + stalled: 'handleStalled', + waiting: 'handleWaiting', + emptied: 'handleReadyStateChange', + loadedmetadata: 'handleReadyStateChange', + loadeddata: 'handleReadyStateChange', + canplay: 'handleReadyStateChange', + canplaythrough: 'handleReadyStateChange', + timeupdate: 'handleTimeUpdate', + durationchange: 'handleDurationChange', + playing: 'handlePlay', + pause: 'handlePause', + progress: 'handleProgress', + volumechange: 'handleVolumeChange', + webkitfullscreenchange: 'handleFullscreenChange', + webkitbeginfullscreen: 'handleFullscreenChange', + webkitendfullscreen: 'handleFullscreenChange', + }, + PlaceholderPollingDelay: 33, + HideControlsDelay: 4 * 1000, + RewindAmount: 30, + MaximumSeekRate: 8, + SeekDelay: 1500, + ClassNames: { + active: 'active', + dropped: 'dropped', + exit: 'exit', + failed: 'failed', + hidden: 'hidden', + hiding: 'hiding', + threeDigitTime: 'three-digit-time', + fourDigitTime: 'four-digit-time', + fiveDigitTime: 'five-digit-time', + sixDigitTime: 'six-digit-time', + list: 'list', + muteBox: 'mute-box', + muted: 'muted', + paused: 'paused', + pictureInPicture: 'picture-in-picture', + playing: 'playing', + returnFromPictureInPicture: 'return-from-picture-in-picture', + selected: 'selected', + show: 'show', + small: 'small', + thumbnail: 'thumbnail', + thumbnailImage: 'thumbnail-image', + thumbnailTrack: 'thumbnail-track', + volumeBox: 'volume-box', + noVideo: 'no-video', + down: 'down', + out: 'out', + pictureInPictureButton: 'picture-in-picture-button', + placeholderShowing: 'placeholder-showing', + usesLTRUserInterfaceLayoutDirection: 'uses-ltr-user-interface-layout-direction', + appleTV: 'appletv', + }, + KeyCodes: { + enter: 13, + escape: 27, + space: 32, + pageUp: 33, + pageDown: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40 + }, + MinimumTimelineWidth: 80, + ButtonWidth: 32, + + extend: function(child) + { + // This function doesn't actually do what we want it to. In particular it + // is not copying the getters and setters to the child class, since they are + // not enumerable. What we should do is use ES6 classes, or assign the __proto__ + // directly. + // FIXME: Use ES6 classes. + + for (var property in this) { + if (!child.hasOwnProperty(property)) + child[property] = this[property]; + } + }, + + get idiom() + { + return "apple"; + }, + + UIString: function(developmentString, replaceString, replacementString) + { + var localized = UIStringTable[developmentString]; + if (replaceString && replacementString) + return localized.replace(replaceString, replacementString); + + if (localized) + return localized; + + console.error("Localization for string \"" + developmentString + "\" not found."); + return "LOCALIZED STRING NOT FOUND"; + }, + + listenFor: function(element, eventName, handler, useCapture) + { + if (typeof useCapture === 'undefined') + useCapture = false; + + if (!(this.listeners[eventName] instanceof Array)) + this.listeners[eventName] = []; + this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture}); + element.addEventListener(eventName, this, useCapture); + }, + + stopListeningFor: function(element, eventName, handler, useCapture) + { + if (typeof useCapture === 'undefined') + useCapture = false; + + if (!(this.listeners[eventName] instanceof Array)) + return; + + this.listeners[eventName] = this.listeners[eventName].filter(function(entry) { + return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture); + }); + element.removeEventListener(eventName, this, useCapture); + }, + + addVideoListeners: function() + { + for (var name in this.HandledVideoEvents) { + this.listenFor(this.video, name, this.HandledVideoEvents[name]); + }; + + /* text tracks */ + this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange); + this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd); + this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove); + + /* audio tracks */ + this.listenFor(this.video.audioTracks, 'change', this.handleAudioTrackChange); + this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd); + this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove); + + /* video tracks */ + this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo); + this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo); + this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo); + + /* controls attribute */ + this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this)); + this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] }); + + this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange); + + if ('webkitPresentationMode' in this.video) + this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange); + }, + + removeVideoListeners: function() + { + for (var name in this.HandledVideoEvents) { + this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]); + }; + + /* text tracks */ + this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange); + this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd); + this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove); + + /* audio tracks */ + this.stopListeningFor(this.video.audioTracks, 'change', this.handleAudioTrackChange); + this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd); + this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove); + + /* video tracks */ + this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo); + this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo); + this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo); + + /* controls attribute */ + this.controlsObserver.disconnect(); + delete(this.controlsObserver); + + this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange); + this.setShouldListenForPlaybackTargetAvailabilityEvent(false); + + if ('webkitPresentationMode' in this.video) + this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange); + }, + + handleEvent: function(event) + { + var preventDefault = false; + + try { + if (event.target === this.video) { + var handlerName = this.HandledVideoEvents[event.type]; + var handler = this[handlerName]; + if (handler && handler instanceof Function) + handler.call(this, event); + } + + if (!(this.listeners[event.type] instanceof Array)) + return; + + this.listeners[event.type].forEach(function(entry) { + if (entry.element === event.currentTarget && entry.handler instanceof Function) + preventDefault |= entry.handler.call(this, event); + }, this); + } catch(e) { + if (window.console) + console.error(e); + } + + if (preventDefault) { + event.stopPropagation(); + event.preventDefault(); + } + }, + + createBase: function() + { + var base = this.base = document.createElement('div'); + base.setAttribute('pseudo', '-webkit-media-controls'); + this.listenFor(base, 'mousemove', this.handleWrapperMouseMove); + this.listenFor(this.video, 'mouseout', this.handleWrapperMouseOut); + if (this.host.textTrackContainer) + base.appendChild(this.host.textTrackContainer); + }, + + shouldHaveAnyUI: function() + { + return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length) || this.currentPlaybackTargetIsWireless(); + }, + + shouldShowControls: function() + { + if (!this.isAudio() && !this.host.allowsInlineMediaPlayback) + return true; + + return this.video.controls || this.isFullScreen(); + }, + + shouldHaveControls: function() + { + return this.shouldShowControls() || this.isFullScreen() || this.presentationMode() === 'picture-in-picture' || this.currentPlaybackTargetIsWireless(); + }, + + + setNeedsTimelineMetricsUpdate: function() + { + this.timelineMetricsNeedsUpdate = true; + }, + + scheduleUpdateLayoutForDisplayedWidth: function() + { + setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0); + }, + + updateTimelineMetricsIfNeeded: function() + { + if (this.timelineMetricsNeedsUpdate && !this.controlsAreHidden()) { + this.timelineLeft = this.controls.timeline.offsetLeft; + this.timelineWidth = this.controls.timeline.offsetWidth; + this.timelineHeight = this.controls.timeline.offsetHeight; + this.timelineMetricsNeedsUpdate = false; + } + }, + + updateBase: function() + { + if (this.shouldHaveAnyUI()) { + if (!this.base.parentNode) { + this.root.appendChild(this.base); + } + } else { + if (this.base.parentNode) { + this.base.parentNode.removeChild(this.base); + } + } + }, + + createControls: function() + { + var panel = this.controls.panel = document.createElement('div'); + panel.setAttribute('pseudo', '-webkit-media-controls-panel'); + panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback'))); + panel.setAttribute('role', 'toolbar'); + this.listenFor(panel, 'mousedown', this.handlePanelMouseDown); + this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd); + this.listenFor(panel, 'click', this.handlePanelClick); + this.listenFor(panel, 'dblclick', this.handlePanelClick); + this.listenFor(panel, 'dragstart', this.handlePanelDragStart); + + var panelBackgroundContainer = this.controls.panelBackgroundContainer = document.createElement('div'); + panelBackgroundContainer.setAttribute('pseudo', '-webkit-media-controls-panel-background-container'); + + var panelTint = this.controls.panelTint = document.createElement('div'); + panelTint.setAttribute('pseudo', '-webkit-media-controls-panel-tint'); + this.listenFor(panelTint, 'mousedown', this.handlePanelMouseDown); + this.listenFor(panelTint, 'transitionend', this.handlePanelTransitionEnd); + this.listenFor(panelTint, 'click', this.handlePanelClick); + this.listenFor(panelTint, 'dblclick', this.handlePanelClick); + this.listenFor(panelTint, 'dragstart', this.handlePanelDragStart); + + var panelBackground = this.controls.panelBackground = document.createElement('div'); + panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background'); + + var rewindButton = this.controls.rewindButton = document.createElement('button'); + rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button'); + rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount)); + this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked); + + var seekBackButton = this.controls.seekBackButton = document.createElement('button'); + seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button'); + seekBackButton.setAttribute('aria-label', this.UIString('Rewind')); + this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown); + this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp); + + var seekForwardButton = this.controls.seekForwardButton = document.createElement('button'); + seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button'); + seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward')); + this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown); + this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp); + + var playButton = this.controls.playButton = document.createElement('button'); + playButton.setAttribute('pseudo', '-webkit-media-controls-play-button'); + playButton.setAttribute('aria-label', this.UIString('Play')); + this.listenFor(playButton, 'click', this.handlePlayButtonClicked); + + var statusDisplay = this.controls.statusDisplay = document.createElement('div'); + statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display'); + statusDisplay.classList.add(this.ClassNames.hidden); + + var timelineBox = this.controls.timelineBox = document.createElement('div'); + timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container'); + + var currentTime = this.controls.currentTime = document.createElement('div'); + currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display'); + currentTime.setAttribute('aria-label', this.UIString('Elapsed')); + currentTime.setAttribute('role', 'timer'); + + var timeline = this.controls.timeline = document.createElement('input'); + timeline.setAttribute('pseudo', '-webkit-media-controls-timeline'); + timeline.setAttribute('aria-label', this.UIString('Duration')); + timeline.type = 'range'; + timeline.value = 0; + this.listenFor(timeline, 'input', this.handleTimelineInput); + this.listenFor(timeline, 'change', this.handleTimelineChange); + this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver); + this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut); + this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove); + this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown); + this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp); + this.listenFor(timeline, 'keydown', this.handleTimelineKeyDown); + timeline.step = .01; + + this.timelineContextName = "_webkit-media-controls-timeline-" + this.host.generateUUID(); + timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')'; + + var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div'); + thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack); + + var thumbnail = this.controls.thumbnail = document.createElement('div'); + thumbnail.classList.add(this.ClassNames.thumbnail); + + var thumbnailImage = this.controls.thumbnailImage = document.createElement('img'); + thumbnailImage.classList.add(this.ClassNames.thumbnailImage); + + var remainingTime = this.controls.remainingTime = document.createElement('div'); + remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display'); + remainingTime.setAttribute('aria-label', this.UIString('Remaining')); + remainingTime.setAttribute('role', 'timer'); + + var muteBox = this.controls.muteBox = document.createElement('div'); + muteBox.classList.add(this.ClassNames.muteBox); + this.listenFor(muteBox, 'mouseover', this.handleMuteBoxOver); + + var muteButton = this.controls.muteButton = document.createElement('button'); + muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button'); + muteButton.setAttribute('aria-label', this.UIString('Mute')); + // Make the mute button a checkbox since it only has on/off states. + muteButton.setAttribute('role', 'checkbox'); + this.listenFor(muteButton, 'click', this.handleMuteButtonClicked); + + var minButton = this.controls.minButton = document.createElement('button'); + minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button'); + minButton.setAttribute('aria-label', this.UIString('Minimum Volume')); + this.listenFor(minButton, 'click', this.handleMinButtonClicked); + + var maxButton = this.controls.maxButton = document.createElement('button'); + maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button'); + maxButton.setAttribute('aria-label', this.UIString('Maximum Volume')); + this.listenFor(maxButton, 'click', this.handleMaxButtonClicked); + + var volumeBox = this.controls.volumeBox = document.createElement('div'); + volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container'); + volumeBox.classList.add(this.ClassNames.volumeBox); + + var volumeBoxBackground = this.controls.volumeBoxBackground = document.createElement('div'); + volumeBoxBackground.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-background'); + + var volumeBoxTint = this.controls.volumeBoxTint = document.createElement('div'); + volumeBoxTint.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-tint'); + + var volume = this.controls.volume = document.createElement('input'); + volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider'); + volume.setAttribute('aria-label', this.UIString('Volume')); + volume.type = 'range'; + volume.min = 0; + volume.max = 1; + volume.step = .05; + this.listenFor(volume, 'input', this.handleVolumeSliderInput); + this.listenFor(volume, 'change', this.handleVolumeSliderChange); + this.listenFor(volume, 'mousedown', this.handleVolumeSliderMouseDown); + this.listenFor(volume, 'mouseup', this.handleVolumeSliderMouseUp); + + this.volumeContextName = "_webkit-media-controls-volume-" + this.host.generateUUID(); + volume.style.backgroundImage = '-webkit-canvas(' + this.volumeContextName + ')'; + + var captionButton = this.controls.captionButton = document.createElement('button'); + captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button'); + captionButton.setAttribute('aria-label', this.UIString('Captions')); + captionButton.setAttribute('aria-haspopup', 'true'); + captionButton.setAttribute('aria-owns', 'audioAndTextTrackMenu'); + this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked); + + var fullscreenButton = this.controls.fullscreenButton = document.createElement('button'); + fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button'); + fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen')); + this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked); + + var pictureInPictureButton = this.controls.pictureInPictureButton = document.createElement('button'); + pictureInPictureButton.setAttribute('pseudo', '-webkit-media-controls-picture-in-picture-button'); + pictureInPictureButton.setAttribute('aria-label', this.UIString('Display Picture in Picture')); + pictureInPictureButton.classList.add(this.ClassNames.pictureInPictureButton); + this.listenFor(pictureInPictureButton, 'click', this.handlePictureInPictureButtonClicked); + + var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div'); + inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status'); + inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Video Playback Placeholder')); + this.listenFor(inlinePlaybackPlaceholder, 'click', this.handlePlaceholderClick); + this.listenFor(inlinePlaybackPlaceholder, 'dblclick', this.handlePlaceholderClick); + if (!Controller.gSimulatePictureInPictureAvailable) + inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden); + + var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div'); + inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text'); + + var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p'); + inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top'); + + var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p'); + inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom'); + + var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button'); + wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button'); + wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display')); + this.listenFor(wirelessTargetPicker, 'click', this.handleWirelessPickerButtonClicked); + + // Show controls button is an accessibility workaround since the controls are now removed from the DOM. http://webkit.org/b/145684 + var showControlsButton = this.showControlsButton = document.createElement('button'); + showControlsButton.setAttribute('pseudo', '-webkit-media-show-controls'); + this.showShowControlsButton(false); + showControlsButton.setAttribute('aria-label', this.UIString('Show Controls')); + this.listenFor(showControlsButton, 'click', this.handleShowControlsClick); + this.base.appendChild(showControlsButton); + + if (!Controller.gSimulateWirelessPlaybackTarget) + wirelessTargetPicker.classList.add(this.ClassNames.hidden); + }, + + createTimeClones: function() + { + var currentTimeClone = this.currentTimeClone = document.createElement('div'); + currentTimeClone.setAttribute('pseudo', '-webkit-media-controls-current-time-display'); + currentTimeClone.setAttribute('aria-hidden', 'true'); + currentTimeClone.classList.add('clone'); + this.base.appendChild(currentTimeClone); + + var remainingTimeClone = this.remainingTimeClone = document.createElement('div'); + remainingTimeClone.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display'); + remainingTimeClone.setAttribute('aria-hidden', 'true'); + remainingTimeClone.classList.add('clone'); + this.base.appendChild(remainingTimeClone); + }, + + setControlsType: function(type) + { + if (type === this.controlsType) + return; + this.controlsType = type; + + this.reconnectControls(); + this.updateShouldListenForPlaybackTargetAvailabilityEvent(); + }, + + setIsLive: function(live) + { + if (live === this.isLive) + return; + this.isLive = live; + + this.updateStatusDisplay(); + + this.reconnectControls(); + }, + + reconnectControls: function() + { + this.disconnectControls(); + + if (this.controlsType === Controller.InlineControls) + this.configureInlineControls(); + else if (this.controlsType == Controller.FullScreenControls) + this.configureFullScreenControls(); + if (this.shouldHaveControls() || this.currentPlaybackTargetIsWireless()) + this.addControls(); + }, + + disconnectControls: function(event) + { + for (var item in this.controls) { + var control = this.controls[item]; + if (control && control.parentNode) + control.parentNode.removeChild(control); + } + }, + + configureInlineControls: function() + { + this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText); + this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop); + this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom); + this.controls.panel.appendChild(this.controls.panelBackgroundContainer); + this.controls.panelBackgroundContainer.appendChild(this.controls.panelBackground); + this.controls.panelBackgroundContainer.appendChild(this.controls.panelTint); + this.controls.panel.appendChild(this.controls.playButton); + if (!this.isLive) + this.controls.panel.appendChild(this.controls.rewindButton); + this.controls.panel.appendChild(this.controls.statusDisplay); + if (!this.isLive) { + this.controls.panel.appendChild(this.controls.timelineBox); + this.controls.timelineBox.appendChild(this.controls.currentTime); + this.controls.timelineBox.appendChild(this.controls.thumbnailTrack); + this.controls.thumbnailTrack.appendChild(this.controls.timeline); + this.controls.thumbnailTrack.appendChild(this.controls.thumbnail); + this.controls.thumbnail.appendChild(this.controls.thumbnailImage); + this.controls.timelineBox.appendChild(this.controls.remainingTime); + } + this.controls.panel.appendChild(this.controls.muteBox); + this.controls.muteBox.appendChild(this.controls.volumeBox); + this.controls.volumeBox.appendChild(this.controls.volumeBoxBackground); + this.controls.volumeBox.appendChild(this.controls.volumeBoxTint); + this.controls.volumeBox.appendChild(this.controls.volume); + this.controls.muteBox.appendChild(this.controls.muteButton); + this.controls.panel.appendChild(this.controls.wirelessTargetPicker); + this.controls.panel.appendChild(this.controls.captionButton); + if (!this.isAudio()) { + this.updatePictureInPictureButton(); + this.controls.panel.appendChild(this.controls.fullscreenButton); + } + + this.controls.panel.style.removeProperty('left'); + this.controls.panel.style.removeProperty('top'); + this.controls.panel.style.removeProperty('bottom'); + }, + + configureFullScreenControls: function() + { + this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText); + this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop); + this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom); + this.controls.panel.appendChild(this.controls.panelBackground); + this.controls.panel.appendChild(this.controls.panelTint); + this.controls.panel.appendChild(this.controls.volumeBox); + this.controls.volumeBox.appendChild(this.controls.minButton); + this.controls.volumeBox.appendChild(this.controls.volume); + this.controls.volumeBox.appendChild(this.controls.maxButton); + this.controls.panel.appendChild(this.controls.seekBackButton); + this.controls.panel.appendChild(this.controls.playButton); + this.controls.panel.appendChild(this.controls.seekForwardButton); + this.controls.panel.appendChild(this.controls.wirelessTargetPicker); + this.controls.panel.appendChild(this.controls.captionButton); + if (!this.isAudio()) { + this.updatePictureInPictureButton(); + this.controls.panel.appendChild(this.controls.fullscreenButton); + } + if (!this.isLive) { + this.controls.panel.appendChild(this.controls.timelineBox); + this.controls.timelineBox.appendChild(this.controls.currentTime); + this.controls.timelineBox.appendChild(this.controls.thumbnailTrack); + this.controls.thumbnailTrack.appendChild(this.controls.timeline); + this.controls.thumbnailTrack.appendChild(this.controls.thumbnail); + this.controls.thumbnail.appendChild(this.controls.thumbnailImage); + this.controls.timelineBox.appendChild(this.controls.remainingTime); + } else + this.controls.panel.appendChild(this.controls.statusDisplay); + }, + + updateControls: function() + { + if (this.isFullScreen()) + this.setControlsType(Controller.FullScreenControls); + else + this.setControlsType(Controller.InlineControls); + + this.setNeedsUpdateForDisplayedWidth(); + this.updateLayoutForDisplayedWidth(); + this.setNeedsTimelineMetricsUpdate(); + + if (this.shouldShowControls()) { + this.controls.panel.classList.add(this.ClassNames.show); + this.controls.panel.classList.remove(this.ClassNames.hidden); + this.resetHideControlsTimer(); + this.showShowControlsButton(false); + } else { + this.controls.panel.classList.remove(this.ClassNames.show); + this.controls.panel.classList.add(this.ClassNames.hidden); + this.showShowControlsButton(true); + } + }, + + isPlayable: function() + { + return this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error; + }, + + updateStatusDisplay: function(event) + { + this.updateShouldListenForPlaybackTargetAvailabilityEvent(); + if (this.video.error !== null) + this.controls.statusDisplay.innerText = this.UIString('Error'); + else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) + this.controls.statusDisplay.innerText = this.UIString('Live Broadcast'); + else if (!this.isPlayable() && this.video.networkState === HTMLMediaElement.NETWORK_LOADING) + this.controls.statusDisplay.innerText = this.UIString('Loading'); + else + this.controls.statusDisplay.innerText = ''; + + this.setStatusHidden(!this.isLive && this.isPlayable()); + }, + + handleLoadStart: function(event) + { + this.updateStatusDisplay(); + this.updateProgress(); + }, + + handleError: function(event) + { + this.updateStatusDisplay(); + }, + + handleAbort: function(event) + { + this.updateStatusDisplay(); + }, + + handleSuspend: function(event) + { + this.updateStatusDisplay(); + }, + + handleStalled: function(event) + { + this.updateStatusDisplay(); + this.updateProgress(); + }, + + handleWaiting: function(event) + { + this.updateStatusDisplay(); + }, + + handleReadyStateChange: function(event) + { + this.updateReadyState(); + this.updateDuration(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + this.updateFullscreenButtons(); + this.updateWirelessTargetAvailable(); + this.updateWirelessTargetPickerButton(); + this.updateProgress(); + this.updateControls(); + }, + + handleTimeUpdate: function(event) + { + if (!this.scrubbing) { + this.updateTime(); + this.updateProgress(); + } + this.drawTimelineBackground(); + }, + + handleDurationChange: function(event) + { + this.updateDuration(); + this.updateTime(); + this.updateProgress(); + }, + + handlePlay: function(event) + { + this.setPlaying(true); + }, + + handlePause: function(event) + { + this.setPlaying(false); + }, + + handleProgress: function(event) + { + this.updateProgress(); + }, + + handleVolumeChange: function(event) + { + this.updateVolume(); + }, + + handleTextTrackChange: function(event) + { + this.updateCaptionContainer(); + }, + + handleTextTrackAdd: function(event) + { + var track = event.track; + + if (this.trackHasThumbnails(track) && track.mode === 'disabled') + track.mode = 'hidden'; + + this.updateThumbnail(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + }, + + handleTextTrackRemove: function(event) + { + this.updateThumbnail(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + }, + + handleAudioTrackChange: function(event) + { + this.updateHasAudio(); + }, + + handleAudioTrackAdd: function(event) + { + this.updateHasAudio(); + this.updateCaptionButton(); + }, + + handleAudioTrackRemove: function(event) + { + this.updateHasAudio(); + this.updateCaptionButton(); + }, + + presentationMode: function() { + if ('webkitPresentationMode' in this.video) + return this.video.webkitPresentationMode; + + if (this.isFullScreen()) + return 'fullscreen'; + + return 'inline'; + }, + + isFullScreen: function() + { + if (!this.video.webkitDisplayingFullscreen) + return false; + + if ('webkitPresentationMode' in this.video && this.video.webkitPresentationMode === 'picture-in-picture') + return false; + + return true; + }, + + updatePictureInPictureButton: function() + { + var shouldShowPictureInPictureButton = (Controller.gSimulatePictureInPictureAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('picture-in-picture'))) && this.hasVideo(); + if (shouldShowPictureInPictureButton) { + if (!this.controls.pictureInPictureButton.parentElement) { + if (this.controls.fullscreenButton.parentElement == this.controls.panel) + this.controls.panel.insertBefore(this.controls.pictureInPictureButton, this.controls.fullscreenButton); + else + this.controls.panel.appendChild(this.controls.pictureInPictureButton); + } + this.controls.pictureInPictureButton.classList.remove(this.ClassNames.hidden); + } else + this.controls.pictureInPictureButton.classList.add(this.ClassNames.hidden); + }, + + timelineStepFromVideoDuration: function() + { + var step; + var duration = this.video.duration; + if (duration <= 10) + step = .5; + else if (duration <= 60) + step = 1; + else if (duration <= 600) + step = 10; + else if (duration <= 3600) + step = 30; + else + step = 60; + + return step; + }, + + incrementTimelineValue: function() + { + var value = this.video.currentTime + this.timelineStepFromVideoDuration(); + return value > this.video.duration ? this.video.duration : value; + }, + + decrementTimelineValue: function() + { + var value = this.video.currentTime - this.timelineStepFromVideoDuration(); + return value < 0 ? 0 : value; + }, + + showInlinePlaybackPlaceholderWhenSafe: function() { + if (this.presentationMode() != 'picture-in-picture') + return; + + if (!this.host.isVideoLayerInline) { + this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden); + this.base.classList.add(this.ClassNames.placeholderShowing); + } else + setTimeout(this.showInlinePlaybackPlaceholderWhenSafe.bind(this), this.PlaceholderPollingDelay); + }, + + shouldReturnVideoLayerToInline: function() + { + var presentationMode = this.presentationMode(); + return presentationMode === 'inline' || presentationMode === 'fullscreen'; + }, + + updatePictureInPicturePlaceholder: function() + { + var presentationMode = this.presentationMode(); + + switch (presentationMode) { + case 'inline': + this.controls.panel.classList.remove(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden); + this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture); + this.base.classList.remove(this.ClassNames.placeholderShowing); + + this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture); + break; + case 'picture-in-picture': + this.controls.panel.classList.add(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture); + this.showInlinePlaybackPlaceholderWhenSafe(); + + this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in Picture in Picture'); + this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholderTextBottom.innerText = ""; + this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture); + + this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture); + break; + default: + this.controls.panel.classList.remove(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture); + this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture); + + this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture); + break; + } + }, + + handlePresentationModeChange: function(event) + { + this.updatePictureInPicturePlaceholder(); + this.updateControls(); + this.updateCaptionContainer(); + this.resetHideControlsTimer(); + if (this.presentationMode() != 'fullscreen' && this.video.paused && this.controlsAreHidden()) + this.showControls(); + this.host.setPreparedToReturnVideoLayerToInline(this.shouldReturnVideoLayerToInline()); + }, + + handleFullscreenChange: function(event) + { + this.updateBase(); + this.updateControls(); + this.updateFullscreenButtons(); + this.updateWirelessPlaybackStatus(); + + if (this.isFullScreen()) { + this.controls.fullscreenButton.classList.add(this.ClassNames.exit); + this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen')); + this.host.enteredFullscreen(); + } else { + this.controls.fullscreenButton.classList.remove(this.ClassNames.exit); + this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen')); + this.host.exitedFullscreen(); + } + + if ('webkitPresentationMode' in this.video) + this.handlePresentationModeChange(event); + }, + + handleShowControlsClick: function(event) + { + if (!this.video.controls && !this.isFullScreen()) + return; + + if (this.controlsAreHidden()) + this.showControls(true); + }, + + handleWrapperMouseMove: function(event) + { + if (!this.video.controls && !this.isFullScreen()) + return; + + if (this.controlsAreHidden()) + this.showControls(); + this.resetHideControlsTimer(); + + if (!this.isDragging) + return; + var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y); + this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px'; + this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px'; + event.stopPropagation() + }, + + handleWrapperMouseOut: function(event) + { + this.hideControls(); + this.clearHideControlsTimer(); + }, + + handleWrapperMouseUp: function(event) + { + this.isDragging = false; + this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true); + }, + + handlePanelMouseDown: function(event) + { + if (event.target != this.controls.panelTint && event.target != this.controls.inlinePlaybackPlaceholder) + return; + + if (!this.isFullScreen()) + return; + + this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true); + this.isDragging = true; + this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY); + this.initialOffset = new WebKitPoint( + parseInt(this.controls.panel.style.left) | 0, + parseInt(this.controls.panel.style.top) | 0 + ); + }, + + handlePanelTransitionEnd: function(event) + { + var opacity = window.getComputedStyle(this.controls.panel).opacity; + if (!parseInt(opacity) && !this.controlsAlwaysVisible() && (this.video.controls || this.isFullScreen())) { + this.base.removeChild(this.controls.inlinePlaybackPlaceholder); + this.base.removeChild(this.controls.panel); + } + }, + + handlePanelClick: function(event) + { + // Prevent clicks in the panel from playing or pausing the video in a MediaDocument. + event.preventDefault(); + }, + + handlePanelDragStart: function(event) + { + // Prevent drags in the panel from triggering a drag event on the <video> element. + event.preventDefault(); + }, + + handlePlaceholderClick: function(event) + { + // Prevent clicks in the placeholder from playing or pausing the video in a MediaDocument. + event.preventDefault(); + }, + + handleRewindButtonClicked: function(event) + { + var newTime = Math.max( + this.video.currentTime - this.RewindAmount, + this.video.seekable.start(0)); + this.video.currentTime = newTime; + return true; + }, + + canPlay: function() + { + return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA; + }, + + handlePlayButtonClicked: function(event) + { + if (this.canPlay()) { + this.canToggleShowControlsButton = true; + this.video.play(); + } else + this.video.pause(); + return true; + }, + + handleTimelineInput: function(event) + { + if (this.scrubbing) + this.video.pause(); + + this.video.fastSeek(this.controls.timeline.value); + this.updateControlsWhileScrubbing(); + }, + + handleTimelineChange: function(event) + { + this.video.currentTime = this.controls.timeline.value; + this.updateProgress(); + }, + + handleTimelineDown: function(event) + { + this.controls.thumbnail.classList.add(this.ClassNames.show); + }, + + handleTimelineUp: function(event) + { + this.controls.thumbnail.classList.remove(this.ClassNames.show); + }, + + handleTimelineMouseOver: function(event) + { + this.controls.thumbnail.classList.add(this.ClassNames.show); + }, + + handleTimelineMouseOut: function(event) + { + this.controls.thumbnail.classList.remove(this.ClassNames.show); + }, + + handleTimelineMouseMove: function(event) + { + if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden)) + return; + + this.updateTimelineMetricsIfNeeded(); + this.controls.thumbnail.classList.add(this.ClassNames.show); + var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY)); + var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth; + percent = Math.max(Math.min(1, percent), 0); + this.controls.thumbnail.style.left = percent * 100 + '%'; + + var thumbnailTime = percent * this.video.duration; + for (var i = 0; i < this.video.textTracks.length; ++i) { + var track = this.video.textTracks[i]; + if (!this.trackHasThumbnails(track)) + continue; + + if (!track.cues) + continue; + + for (var j = 0; j < track.cues.length; ++j) { + var cue = track.cues[j]; + if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) { + this.controls.thumbnailImage.src = cue.text; + return; + } + } + } + }, + + handleTimelineMouseDown: function(event) + { + this.scrubbing = true; + }, + + handleTimelineMouseUp: function(event) + { + this.scrubbing = false; + }, + + handleTimelineKeyDown: function(event) + { + if (event.keyCode == this.KeyCodes.left) + this.controls.timeline.value = this.decrementTimelineValue(); + else if (event.keyCode == this.KeyCodes.right) + this.controls.timeline.value = this.incrementTimelineValue(); + }, + + handleMuteButtonClicked: function(event) + { + this.video.muted = !this.video.muted; + if (this.video.muted) + this.controls.muteButton.setAttribute('aria-checked', 'true'); + else + this.controls.muteButton.setAttribute('aria-checked', 'false'); + this.drawVolumeBackground(); + return true; + }, + + handleMuteBoxOver: function(event) + { + this.drawVolumeBackground(); + }, + + handleMinButtonClicked: function(event) + { + if (this.video.muted) { + this.video.muted = false; + this.controls.muteButton.setAttribute('aria-checked', 'false'); + } + this.video.volume = 0; + return true; + }, + + handleMaxButtonClicked: function(event) + { + if (this.video.muted) { + this.video.muted = false; + this.controls.muteButton.setAttribute('aria-checked', 'false'); + } + this.video.volume = 1; + }, + + updateVideoVolume: function() + { + if (this.video.muted) { + this.video.muted = false; + this.controls.muteButton.setAttribute('aria-checked', 'false'); + } + this.video.volume = this.controls.volume.value; + this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`); + }, + + handleVolumeSliderInput: function(event) + { + this.updateVideoVolume(); + this.drawVolumeBackground(); + }, + + handleVolumeSliderChange: function(event) + { + this.updateVideoVolume(); + }, + + handleVolumeSliderMouseDown: function(event) + { + this.isVolumeSliderActive = true; + this.drawVolumeBackground(); + }, + + handleVolumeSliderMouseUp: function(event) + { + this.isVolumeSliderActive = false; + this.drawVolumeBackground(); + }, + + handleCaptionButtonClicked: function(event) + { + if (this.captionMenu) + this.destroyCaptionMenu(); + else + this.buildCaptionMenu(); + return true; + }, + + hasVideo: function() + { + return this.video.videoTracks && this.video.videoTracks.length; + }, + + updateFullscreenButtons: function() + { + var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVideo(); + this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden && !this.isFullScreen()); + this.updatePictureInPictureButton(); + this.setNeedsUpdateForDisplayedWidth(); + this.updateLayoutForDisplayedWidth(); + }, + + handleFullscreenButtonClicked: function(event) + { + if (this.isFullScreen()) + this.video.webkitExitFullscreen(); + else + this.video.webkitEnterFullscreen(); + return true; + }, + + updateWirelessTargetPickerButton: function() { + var wirelessTargetPickerColor; + if (this.controls.wirelessTargetPicker.classList.contains('playing')) + wirelessTargetPickerColor = "-apple-wireless-playback-target-active"; + else + wirelessTargetPickerColor = "rgba(255,255,255,0.45)"; + if (window.devicePixelRatio == 2) + this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,0.5 L 16,0.5 L 16,15.5 L 0,15.5 z M 0,14.5 L 16,14.5 L 8,5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 3.5,13.25 L 12.5,13.25 L 8,8 z'/></svg>\")"; + else + this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,1 L 16,1 L 16,16 L 0,16 z M 0,15 L 16,15 L 8,5.5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2.5' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 2.75,14 L 13.25,14 L 8,8.75 z'/></svg>\")"; + }, + + handleControlsChange: function() + { + try { + this.updateBase(); + + if (this.shouldHaveControls() && !this.hasControls()) + this.addControls(); + else if (!this.shouldHaveControls() && this.hasControls()) + this.removeControls(); + } catch(e) { + if (window.console) + console.error(e); + } + }, + + nextRate: function() + { + return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2)); + }, + + handleSeekBackMouseDown: function(event) + { + this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking); + this.video.play(); + this.video.playbackRate = this.nextRate() * -1; + this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay); + }, + + seekBackFaster: function() + { + this.video.playbackRate = this.nextRate() * -1; + }, + + handleSeekBackMouseUp: function(event) + { + this.video.playbackRate = this.video.defaultPlaybackRate; + if (this.actionAfterSeeking === Controller.PauseAfterSeeking) + this.video.pause(); + else if (this.actionAfterSeeking === Controller.PlayAfterSeeking) + this.video.play(); + if (this.seekInterval) + clearInterval(this.seekInterval); + }, + + handleSeekForwardMouseDown: function(event) + { + this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking); + this.video.play(); + this.video.playbackRate = this.nextRate(); + this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay); + }, + + seekForwardFaster: function() + { + this.video.playbackRate = this.nextRate(); + }, + + handleSeekForwardMouseUp: function(event) + { + this.video.playbackRate = this.video.defaultPlaybackRate; + if (this.actionAfterSeeking === Controller.PauseAfterSeeking) + this.video.pause(); + else if (this.actionAfterSeeking === Controller.PlayAfterSeeking) + this.video.play(); + if (this.seekInterval) + clearInterval(this.seekInterval); + }, + + updateDuration: function() + { + var duration = this.video.duration; + this.controls.timeline.min = 0; + this.controls.timeline.max = duration; + + this.setIsLive(duration === Number.POSITIVE_INFINITY); + + var timeControls = [this.controls.currentTime, this.controls.remainingTime, this.currentTimeClone, this.remainingTimeClone]; + + function removeTimeClass(className) { + for (let element of timeControls) + element.classList.remove(className); + } + + function addTimeClass(className) { + for (let element of timeControls) + element.classList.add(className); + } + + // Reset existing style. + removeTimeClass(this.ClassNames.threeDigitTime); + removeTimeClass(this.ClassNames.fourDigitTime); + removeTimeClass(this.ClassNames.fiveDigitTime); + removeTimeClass(this.ClassNames.sixDigitTime); + + if (duration >= 60*60*10) + addTimeClass(this.ClassNames.sixDigitTime); + else if (duration >= 60*60) + addTimeClass(this.ClassNames.fiveDigitTime); + else if (duration >= 60*10) + addTimeClass(this.ClassNames.fourDigitTime); + else + addTimeClass(this.ClassNames.threeDigitTime); + }, + + progressFillStyle: function(context) + { + var height = this.timelineHeight; + var gradient = context.createLinearGradient(0, 0, 0, height); + gradient.addColorStop(0, 'rgb(2, 2, 2)'); + gradient.addColorStop(1, 'rgb(23, 23, 23)'); + return gradient; + }, + + updateProgress: function() + { + this.updateTimelineMetricsIfNeeded(); + this.drawTimelineBackground(); + }, + + addRoundedRect: function(ctx, x, y, width, height, radius) { + ctx.moveTo(x + radius, y); + ctx.arcTo(x + width, y, x + width, y + radius, radius); + ctx.lineTo(x + width, y + height - radius); + ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius); + ctx.lineTo(x + radius, y + height); + ctx.arcTo(x, y + height, x, y + height - radius, radius); + ctx.lineTo(x, y + radius); + ctx.arcTo(x, y, x + radius, y, radius); + }, + + drawTimelineBackground: function() { + var dpr = window.devicePixelRatio; + var width = this.timelineWidth * dpr; + var height = this.timelineHeight * dpr; + + if (!width || !height) + return; + + var played = this.controls.timeline.value / this.controls.timeline.max; + var buffered = 0; + for (var i = 0, end = this.video.buffered.length; i < end; ++i) + buffered = Math.max(this.video.buffered.end(i), buffered); + + buffered /= this.video.duration; + + var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height); + + width /= dpr; + height /= dpr; + + ctx.save(); + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, width, height); + + var timelineHeight = 3; + var trackHeight = 1; + var scrubberWidth = 3; + var scrubberHeight = 15; + var borderSize = 2; + var scrubberPosition = Math.max(0, Math.min(width - scrubberWidth, Math.round(width * played))); + + // Draw buffered section. + ctx.save(); + if (this.isAudio()) + ctx.fillStyle = "rgb(71, 71, 71)"; + else + ctx.fillStyle = "rgb(30, 30, 30)"; + ctx.fillRect(1, 8, Math.round(width * buffered) - borderSize, trackHeight); + ctx.restore(); + + // Draw timeline border. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, scrubberPosition, 7, width - scrubberPosition, timelineHeight, timelineHeight / 2.0); + this.addRoundedRect(ctx, scrubberPosition + 1, 8, width - scrubberPosition - borderSize , trackHeight, trackHeight / 2.0); + ctx.closePath(); + ctx.clip("evenodd"); + if (this.isAudio()) + ctx.fillStyle = "rgb(71, 71, 71)"; + else + ctx.fillStyle = "rgb(30, 30, 30)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + + // Draw played section. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, 0, 7, width, timelineHeight, timelineHeight / 2.0); + ctx.closePath(); + ctx.clip(); + if (this.isAudio()) + ctx.fillStyle = "rgb(116, 116, 116)"; + else + ctx.fillStyle = "rgb(75, 75, 75)"; + ctx.fillRect(0, 0, width * played, height); + ctx.restore(); + + // Draw the scrubber. + ctx.save(); + ctx.clearRect(scrubberPosition - 1, 0, scrubberWidth + borderSize, height, 0); + ctx.beginPath(); + this.addRoundedRect(ctx, scrubberPosition, 1, scrubberWidth, scrubberHeight, 1); + ctx.closePath(); + ctx.clip(); + if (this.isAudio()) + ctx.fillStyle = "rgb(181, 181, 181)"; + else + ctx.fillStyle = "rgb(140, 140, 140)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + + ctx.restore(); + }, + + drawVolumeBackground: function() { + var dpr = window.devicePixelRatio; + var width = this.controls.volume.offsetWidth * dpr; + var height = this.controls.volume.offsetHeight * dpr; + + if (!width || !height) + return; + + var ctx = document.getCSSCanvasContext('2d', this.volumeContextName, width, height); + + width /= dpr; + height /= dpr; + + ctx.save(); + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, width, height); + + var seekerPosition = this.controls.volume.value; + var trackHeight = 1; + var timelineHeight = 3; + var scrubberRadius = 3.5; + var scrubberDiameter = 2 * scrubberRadius; + var borderSize = 2; + + var scrubberPosition = Math.round(seekerPosition * (width - scrubberDiameter - borderSize)); + + + // Draw portion of volume under slider thumb. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, 0, 3, scrubberPosition + 2, timelineHeight, timelineHeight / 2.0); + ctx.closePath(); + ctx.clip(); + ctx.fillStyle = "rgb(75, 75, 75)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + + // Draw portion of volume above slider thumb. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, scrubberPosition, 3, width - scrubberPosition, timelineHeight, timelineHeight / 2.0); + ctx.closePath(); + ctx.clip(); + ctx.fillStyle = "rgb(30, 30, 30)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + + // Clear a hole in the slider for the scrubber. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, scrubberPosition, 0, scrubberDiameter + borderSize, height, (scrubberDiameter + borderSize) / 2.0); + ctx.closePath(); + ctx.clip(); + ctx.clearRect(0, 0, width, height); + ctx.restore(); + + // Draw scrubber. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, scrubberPosition + 1, 1, scrubberDiameter, scrubberDiameter, scrubberRadius); + ctx.closePath(); + ctx.clip(); + if (this.isVolumeSliderActive) + ctx.fillStyle = "white"; + else + ctx.fillStyle = "rgb(140, 140, 140)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + + ctx.restore(); + }, + + formatTimeDescription: function(time) + { + if (isNaN(time)) + time = 0; + var absTime = Math.abs(time); + var intSeconds = Math.floor(absTime % 60).toFixed(0); + var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); + var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); + + var secondString = intSeconds == 1 ? 'Second' : 'Seconds'; + var minuteString = intMinutes == 1 ? 'Minute' : 'Minutes'; + var hourString = intHours == 1 ? 'Hour' : 'Hours'; + if (intHours > 0) + return `${intHours} ${this.UIString(hourString)}, ${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`; + if (intMinutes > 0) + return `${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`; + return `${intSeconds} ${this.UIString(secondString)}`; + }, + + formatTime: function(time) + { + if (isNaN(time)) + time = 0; + var absTime = Math.abs(time); + var intSeconds = Math.floor(absTime % 60).toFixed(0); + var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); + var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); + var sign = time < 0 ? '-' : String(); + + if (intHours > 0) + return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2); + + return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2) + }, + + updatePlaying: function() + { + this.setPlaying(!this.canPlay()); + }, + + setPlaying: function(isPlaying) + { + if (!this.video.controls && !this.isFullScreen()) + return; + + if (this.isPlaying === isPlaying) + return; + this.isPlaying = isPlaying; + + if (!isPlaying) { + this.controls.panel.classList.add(this.ClassNames.paused); + if (this.controls.panelBackground) + this.controls.panelBackground.classList.add(this.ClassNames.paused); + this.controls.playButton.classList.add(this.ClassNames.paused); + this.controls.playButton.setAttribute('aria-label', this.UIString('Play')); + this.showControls(); + } else { + this.controls.panel.classList.remove(this.ClassNames.paused); + if (this.controls.panelBackground) + this.controls.panelBackground.classList.remove(this.ClassNames.paused); + this.controls.playButton.classList.remove(this.ClassNames.paused); + this.controls.playButton.setAttribute('aria-label', this.UIString('Pause')); + this.resetHideControlsTimer(); + this.canToggleShowControlsButton = true; + } + }, + + updateForShowingControls: function() + { + this.updateLayoutForDisplayedWidth(); + this.setNeedsTimelineMetricsUpdate(); + this.updateTime(); + this.updateProgress(); + this.drawVolumeBackground(); + this.drawTimelineBackground(); + this.controls.panel.classList.add(this.ClassNames.show); + this.controls.panel.classList.remove(this.ClassNames.hidden); + if (this.controls.panelBackground) { + this.controls.panelBackground.classList.add(this.ClassNames.show); + this.controls.panelBackground.classList.remove(this.ClassNames.hidden); + } + }, + + showShowControlsButton: function (shouldShow) { + this.showControlsButton.hidden = !shouldShow; + if (shouldShow && this.shouldHaveControls()) + this.showControlsButton.focus(); + }, + + showControls: function(focusControls) + { + this.updateShouldListenForPlaybackTargetAvailabilityEvent(); + if (!this.video.controls && !this.isFullScreen()) + return; + + this.updateForShowingControls(); + if (this.shouldHaveControls() && !this.controls.panel.parentElement) { + this.base.appendChild(this.controls.inlinePlaybackPlaceholder); + this.base.appendChild(this.controls.panel); + if (focusControls) + this.controls.playButton.focus(); + } + this.showShowControlsButton(false); + }, + + hideControls: function() + { + if (this.controlsAlwaysVisible()) + return; + + this.clearHideControlsTimer(); + this.updateShouldListenForPlaybackTargetAvailabilityEvent(); + this.controls.panel.classList.remove(this.ClassNames.show); + if (this.controls.panelBackground) + this.controls.panelBackground.classList.remove(this.ClassNames.show); + this.showShowControlsButton(this.isPlayable() && this.isPlaying && this.canToggleShowControlsButton); + }, + + setNeedsUpdateForDisplayedWidth: function() + { + this.currentDisplayWidth = 0; + }, + + scheduleUpdateLayoutForDisplayedWidth: function() + { + setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0); + }, + + isControlVisible: function(control) + { + if (!control) + return false; + if (!this.root.contains(control)) + return false; + return !control.classList.contains(this.ClassNames.hidden) + }, + + updateLayoutForDisplayedWidth: function() + { + if (!this.controls || !this.controls.panel) + return; + + var visibleWidth = this.controls.panel.getBoundingClientRect().width; + if (this._pageScaleFactor > 1) + visibleWidth *= this._pageScaleFactor; + + if (visibleWidth <= 0 || visibleWidth == this.currentDisplayWidth) + return; + + this.currentDisplayWidth = visibleWidth; + + // Filter all the buttons which are not explicitly hidden. + var buttons = [this.controls.playButton, this.controls.rewindButton, this.controls.captionButton, + this.controls.fullscreenButton, this.controls.pictureInPictureButton, + this.controls.wirelessTargetPicker, this.controls.muteBox]; + var visibleButtons = buttons.filter(this.isControlVisible, this); + + // This tells us how much room we need in order to display every visible button. + var visibleButtonWidth = this.ButtonWidth * visibleButtons.length; + + var currentTimeWidth = this.currentTimeClone.getBoundingClientRect().width; + var remainingTimeWidth = this.remainingTimeClone.getBoundingClientRect().width; + + // Check if there is enough room for the scrubber. + var shouldDropTimeline = (visibleWidth - visibleButtonWidth - currentTimeWidth - remainingTimeWidth) < this.MinimumTimelineWidth; + this.controls.timeline.classList.toggle(this.ClassNames.dropped, shouldDropTimeline); + this.controls.currentTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline); + this.controls.thumbnailTrack.classList.toggle(this.ClassNames.dropped, shouldDropTimeline); + this.controls.remainingTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline); + + // Then controls in the following order: + var removeOrder = [this.controls.wirelessTargetPicker, this.controls.pictureInPictureButton, + this.controls.captionButton, this.controls.muteBox, this.controls.rewindButton, + this.controls.fullscreenButton]; + removeOrder.forEach(function(control) { + var shouldDropControl = visibleWidth < visibleButtonWidth && this.isControlVisible(control); + control.classList.toggle(this.ClassNames.dropped, shouldDropControl); + if (shouldDropControl) + visibleButtonWidth -= this.ButtonWidth; + }, this); + }, + + controlsAlwaysVisible: function() + { + if (this.presentationMode() === 'picture-in-picture') + return true; + + return this.isAudio() || this.currentPlaybackTargetIsWireless() || this.scrubbing; + }, + + controlsAreHidden: function() + { + return !this.controlsAlwaysVisible() && !this.controls.panel.classList.contains(this.ClassNames.show) && !this.controls.panel.parentElement; + }, + + removeControls: function() + { + if (this.controls.panel.parentNode) + this.controls.panel.parentNode.removeChild(this.controls.panel); + this.destroyCaptionMenu(); + }, + + addControls: function() + { + this.base.appendChild(this.controls.inlinePlaybackPlaceholder); + this.base.appendChild(this.controls.panel); + this.updateControls(); + }, + + hasControls: function() + { + return this.controls.panel.parentElement; + }, + + updateTime: function() + { + var currentTime = this.video.currentTime; + var timeRemaining = currentTime - this.video.duration; + this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime); + this.controls.currentTime.setAttribute('aria-label', `${this.UIString('Elapsed')} ${this.formatTimeDescription(currentTime)}`); + this.controls.timeline.value = this.video.currentTime; + this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining); + this.controls.remainingTime.setAttribute('aria-label', `${this.UIString('Remaining')} ${this.formatTimeDescription(timeRemaining)}`); + + // Mark the timeline value in percentage format in accessibility. + var timeElapsedPercent = currentTime / this.video.duration; + timeElapsedPercent = Math.max(Math.min(1, timeElapsedPercent), 0); + this.controls.timeline.setAttribute('aria-valuetext', `${parseInt(timeElapsedPercent * 100)}%`); + }, + + updateControlsWhileScrubbing: function() + { + if (!this.scrubbing) + return; + + var currentTime = (this.controls.timeline.value / this.controls.timeline.max) * this.video.duration; + var timeRemaining = currentTime - this.video.duration; + this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime); + this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining); + this.drawTimelineBackground(); + }, + + updateReadyState: function() + { + this.updateStatusDisplay(); + }, + + setStatusHidden: function(hidden) + { + if (this.statusHidden === hidden) + return; + + this.statusHidden = hidden; + + if (hidden) { + this.controls.statusDisplay.classList.add(this.ClassNames.hidden); + this.controls.currentTime.classList.remove(this.ClassNames.hidden); + this.controls.timeline.classList.remove(this.ClassNames.hidden); + this.controls.remainingTime.classList.remove(this.ClassNames.hidden); + this.setNeedsTimelineMetricsUpdate(); + this.showControls(); + } else { + this.controls.statusDisplay.classList.remove(this.ClassNames.hidden); + this.controls.currentTime.classList.add(this.ClassNames.hidden); + this.controls.timeline.classList.add(this.ClassNames.hidden); + this.controls.remainingTime.classList.add(this.ClassNames.hidden); + this.hideControls(); + } + this.updateWirelessTargetAvailable(); + }, + + trackHasThumbnails: function(track) + { + return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails'); + }, + + updateThumbnail: function() + { + for (var i = 0; i < this.video.textTracks.length; ++i) { + var track = this.video.textTracks[i]; + if (this.trackHasThumbnails(track)) { + this.controls.thumbnail.classList.remove(this.ClassNames.hidden); + return; + } + } + + this.controls.thumbnail.classList.add(this.ClassNames.hidden); + }, + + updateCaptionButton: function() + { + var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks); + var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks); + + if ((textTracks && textTracks.length) || (audioTracks && audioTracks.length > 1)) + this.controls.captionButton.classList.remove(this.ClassNames.hidden); + else + this.controls.captionButton.classList.add(this.ClassNames.hidden); + this.setNeedsUpdateForDisplayedWidth(); + this.updateLayoutForDisplayedWidth(); + }, + + updateCaptionContainer: function() + { + if (!this.host.textTrackContainer) + return; + + var hasClosedCaptions = this.video.webkitHasClosedCaptions; + var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden); + + if (hasClosedCaptions && hasHiddenClass) + this.host.textTrackContainer.classList.remove(this.ClassNames.hidden); + else if (!hasClosedCaptions && !hasHiddenClass) + this.host.textTrackContainer.classList.add(this.ClassNames.hidden); + + this.updateBase(); + this.host.updateTextTrackContainer(); + }, + + buildCaptionMenu: function() + { + var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks); + var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks); + + if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length)) + return; + + this.captionMenu = document.createElement('div'); + this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container'); + this.captionMenu.setAttribute('id', 'audioAndTextTrackMenu'); + this.base.appendChild(this.captionMenu); + this.captionMenuItems = []; + + var offItem = this.host.captionMenuOffItem; + var automaticItem = this.host.captionMenuAutomaticItem; + var displayMode = this.host.captionDisplayMode; + + var list = document.createElement('div'); + this.captionMenu.appendChild(list); + list.classList.add(this.ClassNames.list); + + if (audioTracks && audioTracks.length > 1) { + var heading = document.createElement('h3'); + heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label + list.appendChild(heading); + heading.innerText = this.UIString('Audio'); + + var ul = document.createElement('ul'); + ul.setAttribute('role', 'menu'); + ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading'); + list.appendChild(ul); + + for (var i = 0; i < audioTracks.length; ++i) { + var menuItem = document.createElement('li'); + menuItem.setAttribute('role', 'menuitemradio'); + menuItem.setAttribute('tabindex', '-1'); + this.captionMenuItems.push(menuItem); + this.listenFor(menuItem, 'click', this.audioTrackItemSelected); + this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp); + ul.appendChild(menuItem); + + var track = audioTracks[i]; + menuItem.innerText = this.host.displayNameForTrack(track); + menuItem.track = track; + + var itemCheckmark = document.createElement("img"); + itemCheckmark.classList.add("checkmark-container"); + menuItem.insertBefore(itemCheckmark, menuItem.firstChild); + + if (track.enabled) { + menuItem.classList.add(this.ClassNames.selected); + menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute('aria-checked', 'true'); + } + } + } + + if (textTracks && textTracks.length > 2) { + var heading = document.createElement('h3'); + heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label + list.appendChild(heading); + heading.innerText = this.UIString('Subtitles'); + + var ul = document.createElement('ul'); + ul.setAttribute('role', 'menu'); + ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading'); + list.appendChild(ul); + + for (var i = 0; i < textTracks.length; ++i) { + var menuItem = document.createElement('li'); + menuItem.setAttribute('role', 'menuitemradio'); + menuItem.setAttribute('tabindex', '-1'); + this.captionMenuItems.push(menuItem); + this.listenFor(menuItem, 'click', this.captionItemSelected); + this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp); + ul.appendChild(menuItem); + + var track = textTracks[i]; + menuItem.innerText = this.host.displayNameForTrack(track); + menuItem.track = track; + + var itemCheckmark = document.createElement("img"); + itemCheckmark.classList.add("checkmark-container"); + menuItem.insertBefore(itemCheckmark, menuItem.firstChild); + + if (track === offItem) { + var offMenu = menuItem; + continue; + } + + if (track === automaticItem) { + if (displayMode === 'automatic') { + menuItem.classList.add(this.ClassNames.selected); + menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute('aria-checked', 'true'); + } + continue; + } + + if (displayMode != 'automatic' && track.mode === 'showing') { + var trackMenuItemSelected = true; + menuItem.classList.add(this.ClassNames.selected); + menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute('aria-checked', 'true'); + } + + } + + if (offMenu && (displayMode === 'forced-only' || displayMode === 'manual') && !trackMenuItemSelected) { + offMenu.classList.add(this.ClassNames.selected); + offMenu.setAttribute('tabindex', '0'); + offMenu.setAttribute('aria-checked', 'true'); + } + } + + // focus first selected menuitem + for (var i = 0, c = this.captionMenuItems.length; i < c; i++) { + var item = this.captionMenuItems[i]; + if (item.classList.contains(this.ClassNames.selected)) { + item.focus(); + break; + } + } + + }, + + captionItemSelected: function(event) + { + this.host.setSelectedTextTrack(event.target.track); + this.destroyCaptionMenu(); + }, + + focusSiblingCaptionItem: function(event) + { + var currentItem = event.target; + var pendingItem = false; + switch(event.keyCode) { + case this.KeyCodes.left: + case this.KeyCodes.up: + pendingItem = currentItem.previousSibling; + break; + case this.KeyCodes.right: + case this.KeyCodes.down: + pendingItem = currentItem.nextSibling; + break; + } + if (pendingItem) { + currentItem.setAttribute('tabindex', '-1'); + pendingItem.setAttribute('tabindex', '0'); + pendingItem.focus(); + } + }, + + handleCaptionItemKeyUp: function(event) + { + switch (event.keyCode) { + case this.KeyCodes.enter: + case this.KeyCodes.space: + this.captionItemSelected(event); + break; + case this.KeyCodes.escape: + this.destroyCaptionMenu(); + break; + case this.KeyCodes.left: + case this.KeyCodes.up: + case this.KeyCodes.right: + case this.KeyCodes.down: + this.focusSiblingCaptionItem(event); + break; + default: + return; + } + // handled + event.stopPropagation(); + event.preventDefault(); + }, + + audioTrackItemSelected: function(event) + { + for (var i = 0; i < this.video.audioTracks.length; ++i) { + var track = this.video.audioTracks[i]; + track.enabled = (track == event.target.track); + } + + this.destroyCaptionMenu(); + }, + + focusSiblingAudioTrackItem: function(event) + { + var currentItem = event.target; + var pendingItem = false; + switch(event.keyCode) { + case this.KeyCodes.left: + case this.KeyCodes.up: + pendingItem = currentItem.previousSibling; + break; + case this.KeyCodes.right: + case this.KeyCodes.down: + pendingItem = currentItem.nextSibling; + break; + } + if (pendingItem) { + currentItem.setAttribute('tabindex', '-1'); + pendingItem.setAttribute('tabindex', '0'); + pendingItem.focus(); + } + }, + + handleAudioTrackItemKeyUp: function(event) + { + switch (event.keyCode) { + case this.KeyCodes.enter: + case this.KeyCodes.space: + this.audioTrackItemSelected(event); + break; + case this.KeyCodes.escape: + this.destroyCaptionMenu(); + break; + case this.KeyCodes.left: + case this.KeyCodes.up: + case this.KeyCodes.right: + case this.KeyCodes.down: + this.focusSiblingAudioTrackItem(event); + break; + default: + return; + } + // handled + event.stopPropagation(); + event.preventDefault(); + }, + + destroyCaptionMenu: function() + { + if (!this.captionMenu) + return; + + this.captionMenuItems.forEach(function(item){ + this.stopListeningFor(item, 'click', this.captionItemSelected); + this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp); + }, this); + + // FKA and AX: focus the trigger before destroying the element with focus + if (this.controls.captionButton) + this.controls.captionButton.focus(); + + if (this.captionMenu.parentNode) + this.captionMenu.parentNode.removeChild(this.captionMenu); + delete this.captionMenu; + delete this.captionMenuItems; + }, + + updateHasAudio: function() + { + if (this.video.audioTracks.length && !this.currentPlaybackTargetIsWireless()) + this.controls.muteBox.classList.remove(this.ClassNames.hidden); + else + this.controls.muteBox.classList.add(this.ClassNames.hidden); + + this.setNeedsUpdateForDisplayedWidth(); + this.updateLayoutForDisplayedWidth(); + }, + + updateHasVideo: function() + { + this.controls.panel.classList.toggle(this.ClassNames.noVideo, !this.hasVideo()); + // The availability of the picture-in-picture button as well as the full-screen + // button depends no the value returned by hasVideo(), so make sure we invalidate + // the availability of both controls. + this.updateFullscreenButtons(); + }, + + updateVolume: function() + { + if (this.video.muted || !this.video.volume) { + this.controls.muteButton.classList.add(this.ClassNames.muted); + this.controls.volume.value = 0; + } else { + this.controls.muteButton.classList.remove(this.ClassNames.muted); + this.controls.volume.value = this.video.volume; + } + this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`); + this.drawVolumeBackground(); + }, + + isAudio: function() + { + return this.video instanceof HTMLAudioElement; + }, + + clearHideControlsTimer: function() + { + if (this.hideTimer) + clearTimeout(this.hideTimer); + this.hideTimer = null; + }, + + resetHideControlsTimer: function() + { + if (this.hideTimer) { + clearTimeout(this.hideTimer); + this.hideTimer = null; + } + + if (this.isPlaying) + this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay); + }, + + handlePictureInPictureButtonClicked: function(event) { + if (!('webkitSetPresentationMode' in this.video)) + return; + + if (this.presentationMode() === 'picture-in-picture') + this.video.webkitSetPresentationMode('inline'); + else + this.video.webkitSetPresentationMode('picture-in-picture'); + }, + + currentPlaybackTargetIsWireless: function() { + if (Controller.gSimulateWirelessPlaybackTarget) + return true; + + if (!this.currentTargetIsWireless || this.wirelessPlaybackDisabled) + return false; + + return true; + }, + + updateShouldListenForPlaybackTargetAvailabilityEvent: function() { + var shouldListen = true; + if (this.video.error) + shouldListen = false; + if (!this.isAudio() && !this.video.paused && this.controlsAreHidden()) + shouldListen = false; + if (document.hidden) + shouldListen = false; + + this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen); + }, + + updateWirelessPlaybackStatus: function() { + if (this.currentPlaybackTargetIsWireless()) { + var deviceName = ""; + var deviceType = ""; + var type = this.host.externalDeviceType; + if (type == "airplay") { + deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##'); + deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV"); + } else if (type == "tvout") { + deviceType = this.UIString('##TVOUT_DEVICE_TYPE##'); + deviceName = this.UIString('##TVOUT_DEVICE_NAME##'); + } + + this.controls.inlinePlaybackPlaceholderTextTop.innerText = deviceType; + this.controls.inlinePlaybackPlaceholderTextBottom.innerText = deviceName; + this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName); + this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.appleTV); + this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden); + this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing); + if (!this.isFullScreen() && (this.video.offsetWidth <= 250 || this.video.offsetHeight <= 200)) { + this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.small); + this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.small); + this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.small); + } else { + this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.small); + this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.small); + this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.small); + } + this.controls.volumeBox.classList.add(this.ClassNames.hidden); + this.controls.muteBox.classList.add(this.ClassNames.hidden); + this.updateBase(); + this.showControls(); + } else { + this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden); + this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.appleTV); + this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing); + this.controls.volumeBox.classList.remove(this.ClassNames.hidden); + this.controls.muteBox.classList.remove(this.ClassNames.hidden); + } + this.setNeedsUpdateForDisplayedWidth(); + this.updateLayoutForDisplayedWidth(); + this.reconnectControls(); + this.updateWirelessTargetPickerButton(); + }, + + updateWirelessTargetAvailable: function() { + this.currentTargetIsWireless = this.video.webkitCurrentPlaybackTargetIsWireless; + this.wirelessPlaybackDisabled = this.video.webkitWirelessVideoPlaybackDisabled; + + var wirelessPlaybackTargetsAvailable = Controller.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets; + if (this.wirelessPlaybackDisabled) + wirelessPlaybackTargetsAvailable = false; + + if (wirelessPlaybackTargetsAvailable && this.isPlayable()) + this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden); + else + this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden); + this.setNeedsUpdateForDisplayedWidth(); + this.updateLayoutForDisplayedWidth(); + }, + + handleWirelessPickerButtonClicked: function(event) + { + this.video.webkitShowPlaybackTargetPicker(); + return true; + }, + + handleWirelessPlaybackChange: function(event) { + this.updateWirelessTargetAvailable(); + this.updateWirelessPlaybackStatus(); + this.setNeedsTimelineMetricsUpdate(); + }, + + handleWirelessTargetAvailableChange: function(event) { + var wirelessPlaybackTargetsAvailable = event.availability == "available"; + if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable) + return; + + this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable; + this.updateWirelessTargetAvailable(); + this.setNeedsTimelineMetricsUpdate(); + }, + + setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) { + if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen) + return; + + if (shouldListen && this.video.error) + return; + + this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen; + if (shouldListen) + this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange); + else + this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange); + }, + + get scrubbing() + { + return this._scrubbing; + }, + + set scrubbing(flag) + { + if (this._scrubbing == flag) + return; + this._scrubbing = flag; + + if (this._scrubbing) + this.wasPlayingWhenScrubbingStarted = !this.video.paused; + else if (this.wasPlayingWhenScrubbingStarted && this.video.paused) { + this.video.play(); + this.resetHideControlsTimer(); + } + }, + + get pageScaleFactor() + { + return this._pageScaleFactor; + }, + + set pageScaleFactor(newScaleFactor) + { + if (this._pageScaleFactor === newScaleFactor) + return; + + this._pageScaleFactor = newScaleFactor; + }, + + set usesLTRUserInterfaceLayoutDirection(usesLTRUserInterfaceLayoutDirection) + { + this.controls.volumeBox.classList.toggle(this.ClassNames.usesLTRUserInterfaceLayoutDirection, usesLTRUserInterfaceLayoutDirection); + }, + + handleRootResize: function(event) + { + this.updateLayoutForDisplayedWidth(); + this.setNeedsTimelineMetricsUpdate(); + this.updateTimelineMetricsIfNeeded(); + this.drawTimelineBackground(); + }, + + getCurrentControlsStatus: function () + { + var result = { + idiom: this.idiom, + status: "ok" + }; + + var elements = [ + { + name: "Show Controls", + object: this.showControlsButton, + extraProperties: ["hidden"], + }, + { + name: "Status Display", + object: this.controls.statusDisplay, + styleValues: ["display"], + extraProperties: ["textContent"], + }, + { + name: "Play Button", + object: this.controls.playButton, + extraProperties: ["hidden"], + }, + { + name: "Rewind Button", + object: this.controls.rewindButton, + extraProperties: ["hidden"], + }, + { + name: "Timeline Box", + object: this.controls.timelineBox, + }, + { + name: "Mute Box", + object: this.controls.muteBox, + extraProperties: ["hidden"], + }, + { + name: "Fullscreen Button", + object: this.controls.fullscreenButton, + extraProperties: ["hidden"], + }, + { + name: "AppleTV Device Picker", + object: this.controls.wirelessTargetPicker, + styleValues: ["display"], + extraProperties: ["hidden"], + }, + { + name: "Picture-in-picture Button", + object: this.controls.pictureInPictureButton, + extraProperties: ["parentElement", "hidden"], + }, + { + name: "Caption Button", + object: this.controls.captionButton, + extraProperties: ["hidden"], + }, + { + name: "Timeline", + object: this.controls.timeline, + extraProperties: ["hidden"], + }, + { + name: "Current Time", + object: this.controls.currentTime, + extraProperties: ["hidden"], + }, + { + name: "Thumbnail Track", + object: this.controls.thumbnailTrack, + extraProperties: ["hidden"], + }, + { + name: "Time Remaining", + object: this.controls.remainingTime, + extraProperties: ["hidden"], + }, + { + name: "Track Menu", + object: this.captionMenu, + }, + { + name: "Inline playback placeholder", + object: this.controls.inlinePlaybackPlaceholder, + }, + { + name: "Media Controls Panel", + object: this.controls.panel, + extraProperties: ["hidden"], + }, + { + name: "Control Base Element", + object: this.base || null, + }, + ]; + + elements.forEach(function (element) { + var obj = element.object; + delete element.object; + + element.computedStyle = {}; + if (obj && element.styleValues) { + var computedStyle = window.getComputedStyle(obj); + element.styleValues.forEach(function (propertyName) { + element.computedStyle[propertyName] = computedStyle[propertyName]; + }); + delete element.styleValues; + } + + element.bounds = obj ? obj.getBoundingClientRect() : null; + element.className = obj ? obj.className : null; + element.ariaLabel = obj ? obj.getAttribute('aria-label') : null; + + if (element.extraProperties) { + element.extraProperties.forEach(function (property) { + element[property] = obj ? obj[property] : null; + }); + delete element.extraProperties; + } + + element.element = obj; + }); + + result.elements = elements; + + return JSON.stringify(result); + } + +}; diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsBase.css b/Source/WebCore/Modules/mediacontrols/mediaControlsBase.css new file mode 100644 index 000000000..61cf47e89 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsBase.css @@ -0,0 +1,758 @@ +/* + * Copyright (C) 2013, 2014 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, + * 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. + */ + +audio { + width: 200px; + height: 25px; +} + +body:-webkit-full-page-media { + background-color: rgb(38, 38, 38); +} + +video:-webkit-full-page-media { + margin: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +video:-webkit-full-page-media::-webkit-media-controls-panel { + bottom: 0px; +} + +video:-webkit-full-page-media::-webkit-media-controls-panel.no-video { + opacity: 1; +} + +::-webkit-media-controls { + width: inherit; + height: inherit; + position: relative; + display: -webkit-flex; + -webkit-align-items: stretch; + -webkit-justify-content: flex-end; + -webkit-flex-direction: column; +} + +video::-webkit-media-text-track-container, +audio::-webkit-media-text-track-container { + position: relative; + -webkit-flex: 1 1 auto; +} + +video::-webkit-media-controls-panel-composited-parent { + -webkit-transform: translateZ(0); + width: 100%; +} + +video::-webkit-media-controls-panel, +audio::-webkit-media-controls-panel { + box-sizing: border-box; + position: relative; + bottom: 0; + width: 100%; + padding-top: 1px; + min-height: 25px; + height: 25px; + line-height: 25px; + -webkit-user-select: none; + background-color: transparent; + background-image: -webkit-linear-gradient(top, + rgba(0, 0, 0, .92) 0, + rgba(0, 0, 0, .92) 1px, + rgba(89, 89, 89, .92) 1px, + rgba(89, 89, 89, .92) 2px, + rgba(60, 60, 60, .92) 2px, + rgba(35, 35, 35, .92) 12px, + rgba(30, 30, 30, .92) 12px, + rgba(30, 30, 30, .92) 13px, + rgba(25, 25, 25, .92) 13px, + rgba(17, 17, 17, .92) 100% + ); + + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-user-select: none; + + direction: ltr; + + transition: opacity 0.25s linear; +} + +video::-webkit-media-controls-panel { + cursor: none; + opacity: 0; +} + +video::-webkit-media-controls-panel.show, +video::-webkit-media-controls-panel.paused, +video::-webkit-media-controls-panel:hover { + cursor: inherit; + opacity: 1; +} + +video::-webkit-media-controls-panel button, +audio::-webkit-media-controls-panel button { + -webkit-appearance: none; + display: block; + padding: 0; + border: 0; + height: 16px; + width: 16px; + background-color: transparent; + color: white; + background-origin: content-box; + background-repeat: no-repeat; + background-position: center; + -webkit-filter: drop-shadow(black 0 1px 1px); +} + +video::-webkit-media-controls-panel button:active, +audio::-webkit-media-controls-panel button:active { + -webkit-filter: drop-shadow(white 0 0 10px); +} + +video::-webkit-media-controls-rewind-button, +audio::-webkit-media-controls-rewind-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 17"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.44444" stop-color="rgb(216, 216, 216)"/><stop offset="0.44444" stop-color="rgb(208, 208, 208)"/><stop offset="0.55555" stop-color="rgb(208, 208, 208)"/><stop offset="0.55555" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="m 7.9131,2 0,-1.548 -2.586,2.155 0,-2.171 -2.582,2.208 2.582,2.175 0,-2.139 2.586,2.155 0,-1.276 c 3.138,0.129 5.491,2.681 5.543,5.838 l -1.031,0 0.016,0.215 1.015,0 c -0.06,3.19 -2.629,5.765 -5.819,5.833 l 0,-1.018 -0.214,0 0,1.021 c -3.21,-0.047 -5.801,-2.631 -5.862,-5.836 l 1.045,0 -0.016,-0.215 -1.045,0 c -0.052,-0.288 -0.318,-0.654 -0.766,-0.654 -0.538,0 -0.755,0.484 -0.755,0.75 0,4.146 3.331,7.506 7.476,7.506 4.146,0 7.506,-3.36 7.506,-7.506 0,-4.059 -3.066,-7.357 -7.093,-7.493" fill="url(#gradient)"/><path d="m 5.1729,11.0518 c -0.38,0 -0.668,-0.129 -0.945,-0.366 -0.083,-0.071 -0.186,-0.134 -0.338,-0.134 -0.277,0 -0.511,0.238 -0.511,0.521 0,0.154 0.083,0.301 0.179,0.383 0.394,0.346 0.911,0.563 1.601,0.563 1.077,0 1.739,-0.681 1.739,-1.608 l 0,-0.013 c 0,-0.911 -0.641,-1.265 -1.296,-1.376 l 0.945,-0.919 c 0.193,-0.19 0.317,-0.337 0.317,-0.604 0,-0.294 -0.228,-0.477 -0.538,-0.477 l -2.354,0 c -0.248,0 -0.455,0.21 -0.455,0.464 0,0.253 0.207,0.463 0.455,0.463 l 1.485,0 -0.939,0.961 c -0.166,0.169 -0.228,0.295 -0.228,0.444 0,0.25 0.207,0.463 0.455,0.463 l 0.166,0 c 0.594,0 0.945,0.222 0.945,0.624 l 0,0.012 c 0,0.367 -0.282,0.599 -0.683,0.599" fill="url(#gradient)"/><path d="m 10.354,9.5342 c 0,0.876 -0.379,1.525 -0.979,1.525 -0.599,0 -0.992,-0.655 -0.992,-1.539 l 0,-0.012 c 0,-0.884 0.388,-1.527 0.979,-1.527 0.592,0 0.992,0.663 0.992,1.539 l 0,0.014 z m -0.979,-2.512 c -1.197,0 -2.008,1.097 -2.008,2.498 l 0,0.014 c 0,1.401 0.792,2.484 1.995,2.484 1.205,0 2.01,-1.097 2.01,-2.498 l 0,-0.012 c 0,-1.402 -0.805,-2.486 -1.997,-2.486" fill="url(#gradient)"/></svg>'); + width: 16px; + height: 18px; + margin-bottom: 1px; + margin-left: 6px; + margin-right: 4px; +} + +video::-webkit-media-controls-play-button, +audio::-webkit-media-controls-play-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 0,0 v 16 h 6 v -16 h -6 z" fill="url(#gradient)"/><path d="M 9,0 v 16 h 6 v -16 h -6 z" fill="url(#gradient)"/></svg>'); + margin-left: 6px; + margin-right: 1px; +} + +video::-webkit-media-controls-play-button.paused, +audio::-webkit-media-controls-play-button.paused { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 0,0 15,7 0,15 z" fill="url(#gradient)"/></svg>'); +} + +video::-webkit-media-controls-panel .mute-box, +audio::-webkit-media-controls-panel .mute-box { + width: 22px; + height: 22px; + margin-right: 2px; + + position: relative; + display: -webkit-flex; + -webkit-flex-direction: column; + -webkit-justify-content: center; + -webkit-align-items: center; +} + +video::-webkit-media-controls-mute-button, +audio::-webkit-media-controls-mute-button, +video::-webkit-media-controls-volume-max-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="m 0,9 0,-4 3,0 3,-3 0,10 -3,-3 -3,0 z" style="fill:url(#gradient) "/><path d="m 10.449,1.087 c 1.963,1.055 3.322,3.291 3.322,5.881 0,2.642 -1.402,4.913 -3.424,5.945" style="fill:none;stroke:url(#gradient);stroke-width:1.25;stroke-linecap:round;"/><path d="m 9.13,3.134 c 1.289,0.681 2.181,2.142 2.181,3.835 0,1.743001 -0.939,3.24 -2.285,3.897" style="fill:none;stroke:url(#gradient);stroke-width:1.25;stroke-linecap:round;"/><path d="M 7.794,5.175 C 8.403001,5.491 8.827001,6.167 8.827001,6.971 8.827001,7.818 8.356,8.537001 7.688,8.826" style="fill:none;stroke:url(#gradient);stroke-width:1.25;stroke-linecap:round;"/></svg>'); + width: 14px; +} + +video::-webkit-media-controls-panel .volume-box, +audio::-webkit-media-controls-panel .volume-box { + position: absolute; + box-sizing: border-box; + height: 22px; + bottom: 0; + left: 0; + + -webkit-transform: rotate(-90deg); + -webkit-transform-origin: 11px 11px; + + background-color: transparent; + background-image: -webkit-linear-gradient( + left, + rgba(17, 17, 17, 0.92), + rgba(42, 42, 42, 0.92) + ); + border: 1px solid rgba(0, 0, 0, 0.95); + border-radius: 12px; + + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-justify-content: flex-end; + + opacity: 0; + /* make zero width (rather than display:none) for AX and FKA */ + width: 0; /* will become 114px when shown */ + +} + +/* FIXME: needs CSS4 !subject selector to show when slider inside .volume-box is focused */ +video::-webkit-media-controls-panel .mute-box:hover .volume-box, +video::-webkit-media-controls-panel .volume-box:hover, +video::-webkit-media-controls-panel .volume-box:active, +audio::-webkit-media-controls-panel .mute-box:hover .volume-box, +audio::-webkit-media-controls-panel .volume-box:hover, +audio::-webkit-media-controls-panel .volume-box:active { + opacity: 1; + /* resize to usable amount (rather than display:none) for AX and FKA */ + width: 114px; +} + +audio::-webkit-media-controls-volume-slider, +video::-webkit-media-controls-volume-slider { + -webkit-appearance: none !important; + box-sizing: border-box !important; + height: 10px !important; + width: 80px !important; + padding: 0 !important; + margin-right: 6px !important; + + border-radius: 5px !important; + background-color: transparent !important; + background-image: -webkit-linear-gradient( + top, + rgba(15, 15, 15, .85) 0, + rgba(23, 23, 23, .85) 50%, + rgba(15, 15, 15, .85) 100% + ) !important; + border: 1px solid rgba(0, 0, 0, 0.875) !important; +} + +video::-webkit-media-controls-volume-slider::-webkit-slider-thumb, +audio::-webkit-media-controls-volume-slider::-webkit-slider-thumb { + -webkit-appearance: none !important; + width: 8px !important; + height: 8px !important; + border-radius: 4px !important; + background-color: transparent !important; + + /* rotateZ() forces the layer into compositing mode. + Slider thumbs are small, so forcing a compositing layer is inexpensive, + and it keeps the slider from having to repaint while sliding. */ + -webkit-transform: rotateZ(0) !important; + background-image: -webkit-linear-gradient( + left, + rgba(99, 99, 99, 1), + rgba(144, 144, 144, 1) + ) !important; + box-shadow: inset -1px 0 0 rgba(255, 255, 255, .5), 0 1px rgba(255, 255, 255, .14) !important; +} +video::-webkit-media-controls-volume-slider::-webkit-slider-thumb::-webkit-slider-thumb:active, +video::-webkit-media-controls-volume-slider::-webkit-slider-thumb:active::-webkit-slider-thumb, +audio::-webkit-media-controls-volume-slider::-webkit-slider-thumb::-webkit-slider-thumb:active, +audio::-webkit-media-controls-volume-slider::-webkit-slider-thumb:active::-webkit-slider-thumb { + background-image: -webkit-linear-gradient( + right top, + rgba(160, 160, 160, 1), + rgba(221, 221, 221, 1) + ) !important; +} + +video::-webkit-media-controls-mute-button.muted, +audio::-webkit-media-controls-mute-button.muted, +video::-webkit-media-controls-volume-min-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="m 0,9 0,-4 3,0 3,-3 0,10 -3,-3 -3,0 z" fill="url(#gradient)"/></svg>'); +} + +video::-webkit-media-controls-toggle-closed-captions-button, +audio::-webkit-media-controls-toggle-closed-captions-button { + width: 16px; + height: 16px; + margin: 0 7px; + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 102 105"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.46875" stop-color="rgb(216, 216, 216)"/><stop offset="0.46875" stop-color="rgb(208, 208, 208)"/><stop offset="0.53125" stop-color="rgb(208, 208, 208)"/><stop offset="0.53125" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M98.766,43.224c0-23.163-21.775-41.94-48.637-41.94c-26.859,0-48.635,18.777-48.635,41.94c0,18.266,13.546,33.796,32.444,39.549c1.131,8.356,26.037,24.255,22.864,19.921c-4.462-6.096-5.159-13.183-5.07-17.566C77.85,84.397,98.766,65.923,98.766,43.224z" fill="url(#gradient)"/></svg>'); + outline: 0; +} + +video::-webkit-media-controls-closed-captions-container, +audio::-webkit-media-controls-closed-captions-container { + -webkit-appearance: media-closed-captions-container; + position: absolute; + display: block; + right: 38px; + bottom: 29px; + max-width: calc(100% - 48px); /* right + 10px */ + max-height: calc(100% - 39px); /* bottom + 10px */ + overflow-x: hidden; + overflow-y: scroll; + background-color: rgba(0, 0, 0, 0.85); + border: 3px solid rgba(128, 128, 128, 0.75); + border-radius: 10px; + cursor: default; + z-index: 2; + text-align: initial; +} + +video::-webkit-media-controls-closed-captions-container .list, +audio::-webkit-media-controls-closed-captions-container .list { + display: block; + font-family: "Helvetica Bold", Helvetica, sans-serif; + font-size: 10pt; + -webkit-user-select: none; +} + +video::-webkit-media-controls-closed-captions-container h3, +audio::-webkit-media-controls-closed-captions-container h3 { + margin: 0; + color: rgb(117, 117, 117); + text-shadow: 0 1px 0 black; + -webkit-margin-start: 23px; + padding-top: 4px; + font-weight: bold; + font-size: 10pt; +} + +video::-webkit-media-controls-closed-captions-container ul, +audio::-webkit-media-controls-closed-captions-container ul { + list-style-type: none; + margin: 0 0 4px 0; + padding: 0; + font-weight: bold; +} + +video::-webkit-media-controls-closed-captions-container li, +audio::-webkit-media-controls-closed-captions-container li { + position: relative; + color: white; + background-image: none; + text-shadow: 0 1px 0 black; + margin: 0; + padding-left: 37px; + padding-right: 35px; + padding-top: 0.15em; + padding-bottom: 0.2em; + box-sizing: border-box; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; +} + +video::-webkit-media-controls-closed-captions-container li:focus, +audio::-webkit-media-controls-closed-captions-container li:focus { + outline: 0; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, rgba(255, 255, 255, 0.3)), color-stop(1, rgba(255, 255, 255, 0.2))); +} + +video::-webkit-media-controls-closed-captions-container li:hover, +audio::-webkit-media-controls-closed-captions-container li:hover { + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, rgb(79, 112, 246)), color-stop(1, rgb(26, 68, 243))); + border-top: 1px solid rgb(70, 103, 234); + border-bottom: 1px solid rgb(3, 54, 229); +} + +video::-webkit-media-controls-closed-captions-container li.selected::before, +audio::-webkit-media-controls-closed-captions-container li.selected::before { + display: block; + content: ""; + position: absolute; + top: 0.25em; + width: 1.1em; + height: 1.1em; + -webkit-margin-start: -20px; + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><polygon fill="rgb(163, 163, 163)" points="252.301,4.477 126.667,194.104 43.358,108.3 6.868,161.408 132.515,290.814 297.732,49.926"/></svg>'); + background-repeat: no-repeat; +} + +video::-webkit-media-controls-closed-captions-container li.selected:hover::before, +audio::-webkit-media-controls-closed-captions-container li.selected:hover::before { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><polygon fill="white" points="252.301,4.477 126.667,194.104 43.358,108.3 6.868,161.408 132.515,290.814 297.732,49.926"/></svg>'); +} + +video::-webkit-media-controls-fullscreen-button, +audio::-webkit-media-controls-fullscreen-button { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" transform="rotate(90,0,0)"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 14,1 m 0,6 -2,-2 -2,2 c 0,0 -1,1 -2,0 -1,-1 0,-2 0,-2 l 2,-2 -2,-2 6,0 z" style="fill:url(#gradient) "/><path d="M 1,14 m 0,-6 2,2 2,-2 c 0,0 1,-1 2,0 1,1 0,2 0,2 l -2,2 2,2 -6,0 z" style="fill:url(#gradient) "/></svg>'); + margin: 0 7px; +} +video::-webkit-media-controls-fullscreen-button.exit, +audio::-webkit-media-controls-fullscreen-button.exit { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" transform="rotate(90,0,0)"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 7,8 m 0,6 -2,-2 -2,2 c 0,0 -1,1 -2,0 -1,-1 0,-2 0,-2 l 2,-2 -2,-2 6,0 z" style="fill:url(#gradient) "/><path d="M 8,7 m 0,-6 2,2 2,-2 c 0,0 1,-1 2,0 1,1 0,2 0,2 l -2,2 2,2 -6,0 z" style="fill:url(#gradient) "/></svg>'); +} +video::-webkit-media-controls-status-display, +audio::-webkit-media-controls-status-display { + cursor: default; + font: -webkit-small-control; + font-size: 9px; + overflow: hidden; + color: white; + text-shadow: black 0px 1px 1px; + + letter-spacing: normal; + word-spacing: normal; + line-height: 25px; + text-transform: none; + text-indent: 0; + text-decoration: none; + text-align: left; + + padding: 0 12px; + + -webkit-flex: 1 1 0; +} +video::-webkit-media-controls-timeline, +audio::-webkit-media-controls-timeline { + -webkit-appearance: none !important; + -webkit-flex: 1 1 0 !important; + height: 9px !important; + margin: 0 !important; + + border-radius: 4.5px !important; + background-color: rgb(74, 74, 74) !important; + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .68), 0 1px rgba(255, 255, 255, .08) !important; +} +video::-webkit-media-controls-timeline::-webkit-slider-thumb, +audio::-webkit-media-controls-timeline::-webkit-slider-thumb { + -webkit-appearance: none !important; + width:6px !important; + height: 6px !important; + background-color: white !important; + + /* rotateZ() forces the layer into compositing mode. + Slider thumbs are small, so forcing a compositing layer is inexpensive, + and it keeps the slider from having to repaint while sliding. */ + -webkit-transform: translateY(1px) rotateZ(-45deg) !important; + + background-image: -webkit-gradient( + linear, + left bottom, + right top, + color-stop(0, rgba(99, 99, 99, 1)), + color-stop(1, rgba(144, 144, 144, 1)) + ) !important; +} +video::-webkit-media-controls-timeline::-webkit-slider-thumb:active, +video::-webkit-media-controls-timeline:active::-webkit-slider-thumb, +audio::-webkit-media-controls-timeline::-webkit-slider-thumb:active, +audio::-webkit-media-controls-timeline:active::-webkit-slider-thumb, + { + background-image: -webkit-gradient( + linear, + left bottom, + right top, + color-stop(0, rgba(160, 160, 160, 1)), + color-stop(1, rgba(221, 221, 221, 1)) + ) !important; +} +video::-webkit-media-controls-current-time-display, +video::-webkit-media-controls-time-remaining-display, +audio::-webkit-media-controls-current-time-display, +audio::-webkit-media-controls-time-remaining-display { + -webkit-user-select: none; + -webkit-flex: 0 0 0; + display: -webkit-flex; + -webkit-justify-content: center; + -webkit-align-items: center; + cursor: default; + font: -webkit-small-control; + font-size: 9px; + overflow-y: hidden; + overflow-x: hidden; + width: 45px; + min-width: 45px; + color: white; + text-shadow: black 0px 1px 1px; + letter-spacing: normal; + word-spacing: normal; + line-height: normal; + text-transform: none; + text-indent: 0px; + text-decoration: none; +} + +video::-webkit-media-controls-timeline-container .hour-long-time, +audio::-webkit-media-controls-timeline-container .hour-long-time { + min-width: 67px; +} + +video::-webkit-media-controls-timeline-container, +audio::-webkit-media-controls-timeline-container { + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-user-select: none; + -webkit-flex: 1 1 0; + position: relative; + padding: 0; +} + +video::-webkit-media-controls-panel .thumbnail-track, +audio::-webkit-media-controls-panel .thumbnail-track { + position: relative; + -webkit-flex: 1 1 0; + height: 9px; + margin: 0 2px; + display: -webkit-flex; + -webkit-align-items: stretch; + -webkit-flex-direction: column; +} + +video::-webkit-media-controls-panel .thumbnail, +audio::-webkit-media-controls-panel .thumbnail { + position: absolute; + opacity: 0; + transition: opacity 0.25s linear; + bottom: 15px; + width: 100px; + height: 58px; + margin-left: -50px; + border: 5px solid black; + box-shadow: 0 0 3px white; + border-radius: 3px; +} + +video::-webkit-media-controls-panel .thumbnail-image, +audio::-webkit-media-controls-panel .thumbnail-image { + width: 100%; + height: 100%; +} + +video::-webkit-media-controls-panel .thumbnail.show, +audio::-webkit-media-controls-panel .thumbnail.show { + opacity: 1; +} + +video::-webkit-media-controls-panel .hidden, +audio::-webkit-media-controls-panel .hidden { + display: none; +} + +/* Full Screen */ + +/* + Page stylesheets are not allowed to override the style of media + controls while in full screen mode, so many if not all the rules + defined in this section will be marked as !important to enforce + this restriction +*/ + +video:-webkit-full-screen::-webkit-media-controls-panel { + -webkit-align-items: flex-start !important; + -webkit-justify-content: flex-end !important; + + width: 440px !important; + height: 60px !important; + margin: 0 auto 50px auto !important; + padding-top: 10px !important; + + background: -webkit-linear-gradient(top, + rgba(45, 45, 45, .97) 0, + rgba(30, 30, 30, .97) 19px, + rgba(25, 25, 25, .97) 19px, + rgba(25, 25, 25, .97) 20px, + rgba(19, 19, 19, .97) 20px, + rgba(12, 12, 12, .97) 100% + ) !important; + + box-shadow: + inset 0 -1px 1px rgba(0, 0, 0, 0.5), + inset 0 1px 0 0px rgba(255, 255, 255, 0.15), + inset 0 -1px 0 0px rgba(202, 202, 202, 0.09), + 0 0 0 1px rgba(0, 0, 0, 0.5); + border-radius: 8px !important; + + transition: opacity 0.3s linear !important; +} + +video:-webkit-animating-full-screen-transition::-webkit-media-controls-panel { + opacity: 0 ! important; + transition: opacity 0 ! important; +} + +video:-webkit-full-screen::-webkit-media-controls-panel .volume-box { + -webkit-transform: none; + opacity: 1; + left: 11px; + top: 13px; + width: 90px; + height: 14px; + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + background-image: none; + border: none; +} + +video:-webkit-full-screen::-webkit-media-controls-volume-slider { + height: 6px !important; + border-radius: 3px !important; + background-image: -webkit-linear-gradient(top, + rgba(16, 16, 16, .300) 0, + rgba(9, 9, 9, .629) 100% + ) !important; + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .68), 0 1px rgba(255, 255, 255, .08) !important; +} + +video:-webkit-full-screen::-webkit-media-controls-volume-slider::-webkit-slider-thumb { + width: 10px !important; + height: 10px !important; + border-radius: 5px !important; + + /* rotateZ() forces the layer into compositing mode. + Slider thumbs are small, so forcing a compositing layer is inexpensive, + and it keeps the slider from having to repaint while sliding. */ + -webkit-transform: rotateZ(270deg) !important; +} + +video:-webkit-full-screen::-webkit-media-controls-play-button { + position: absolute; + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 22"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.478" stop-color="rgb(216, 216, 216)"/><stop offset="0.478" stop-color="rgb(208, 208, 208)"/><stop offset="0.522" stop-color="rgb(208, 208, 208)"/><stop offset="0.522" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 0,0 0,22 8,22 8,0 z" fill="url(#gradient)"/><path d="M 13,0 13,22 21,22 21,0 z" fill="url(#gradient)"/></svg>'); + + width: 22px; + height: 23px; + left: 209px; + top: 9px; + margin: 0; + padding: 0; +} + +video:-webkit-full-screen::-webkit-media-controls-play-button.paused { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 22"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.478" stop-color="rgb(216, 216, 216)"/><stop offset="0.478" stop-color="rgb(208, 208, 208)"/><stop offset="0.522" stop-color="rgb(208, 208, 208)"/><stop offset="0.522" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 0,0 21,11 0,22 z" fill="url(#gradient)"/></svg>'); +} + +video:-webkit-full-screen::-webkit-media-controls-rewind-button { + position: absolute; + left: 162px; + top: 13px; + width: 18px; + height: 18px; +} + +video:-webkit-full-screen::-webkit-media-controls-seek-back-button { + position: absolute; + + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 15"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 22,0 11,7 22,15 z" fill="url(#gradient)"/><path d="M 11,0 0,7 11,15 z" fill="url(#gradient)"/></svg>'); + + width: 23px; + height: 16px; + left: 156px; + top: 13px; +} + +video:-webkit-full-screen::-webkit-media-controls-return-to-realtime-button { + position: absolute; + display: -webkit-flex; + width: 29px; + height: 16px; + left: 262px; + top: 13px; +} + +video:-webkit-full-screen::-webkit-media-controls-seek-forward-button { + position: absolute; + + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 15"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(216, 216, 216)"/><stop offset="0.4375" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(208, 208, 208)"/><stop offset="0.5" stop-color="rgb(200, 200, 200)"/><stop offset="1" stop-color="rgb(208, 208, 208)"/></linearGradient><path d="M 0,0 11,7 0,15 z" fill="url(#gradient)"/><path d="M 11,0 22,7 11,15 z" fill="url(#gradient)"/></svg>'); + + width: 23px; + height: 16px; + left: 256px; + top: 13px; +} + +video:-webkit-full-screen::-webkit-media-controls-timeline-container { + height: auto; + width: 420px; + position: absolute; + bottom: 9px; + left: 8px; + right: 8px; +} + +video:-webkit-full-screen::-webkit-media-controls-status-display { + width: 420px; + position: absolute; + bottom: 7px; + left: 8px; + right: 8px; +} + +video:-webkit-full-screen::-webkit-media-controls-closed-captions-container { + bottom: 114px; + right: calc(50% - 183px); /* 183px is 221px (half the media controller's width) minus 38px (the right position of the captions icon). */ + max-width: calc(50% + 173px); /* right + 10px */ + max-height: calc(100% - 124px); /* bottom + 10px */ +} + +video::-webkit-media-text-track-container { + position: relative; + width: 100%; + overflow: hidden; + padding-bottom: 5px; + + text-align: center; + color: rgba(255, 255, 255, 1); + + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0; + text-decoration: none; + pointer-events: none; + -webkit-user-select: none; + + -webkit-flex: 1 1; + + -webkit-line-box-contain: block inline-box replaced; +} + +video::cue { + background-color: rgba(0, 0, 0, 0.8); +} + +video::-webkit-media-text-track-display { + position: absolute; + overflow: hidden; + white-space: pre-wrap; + -webkit-box-sizing: border-box; + font: 22px sans-serif; +} + +video::-webkit-media-text-track-display-backdrop { + display: inline-block; +} + +video::cue(:future) { + color: gray; +} + +video::-webkit-media-text-track-container b { + font-weight: bold; +} + +video::-webkit-media-text-track-container u { + text-decoration: underline; +} + +video::-webkit-media-text-track-container i { + font-style: italic; +} + +video::-webkit-media-text-track-container .hidden, +audio::-webkit-media-text-track-container .hidden { + display: none; +} diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsBase.js b/Source/WebCore/Modules/mediacontrols/mediaControlsBase.js new file mode 100644 index 000000000..f78696d64 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsBase.js @@ -0,0 +1,1336 @@ +function createControls(root, video, host) +{ + return new Controller(root, video, host); +}; + +function Controller(root, video, host) +{ + this.video = video; + this.root = root; + this.host = host; + this.controls = {}; + this.listeners = {}; + this.isLive = false; + this.statusHidden = true; + this.hasVisualMedia = false; + + this.addVideoListeners(); + this.createBase(); + this.createControls(); + this.updateBase(); + this.updateControls(); + this.updateDuration(); + this.updateProgress(); + this.updateTime(); + this.updateReadyState(); + this.updatePlaying(); + this.updateThumbnail(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + this.updateFullscreenButton(); + this.updateVolume(); + this.updateHasAudio(); + this.updateHasVideo(); +}; + +/* Enums */ +Controller.InlineControls = 0; +Controller.FullScreenControls = 1; + +Controller.PlayAfterSeeking = 0; +Controller.PauseAfterSeeking = 1; + +/* Globals */ +Controller.gLastTimelineId = 0; + +Controller.prototype = { + + /* Constants */ + HandledVideoEvents: { + loadstart: 'handleLoadStart', + error: 'handleError', + abort: 'handleAbort', + suspend: 'handleSuspend', + stalled: 'handleStalled', + waiting: 'handleWaiting', + emptied: 'handleReadyStateChange', + loadedmetadata: 'handleReadyStateChange', + loadeddata: 'handleReadyStateChange', + canplay: 'handleReadyStateChange', + canplaythrough: 'handleReadyStateChange', + timeupdate: 'handleTimeUpdate', + durationchange: 'handleDurationChange', + playing: 'handlePlay', + pause: 'handlePause', + progress: 'handleProgress', + volumechange: 'handleVolumeChange', + webkitfullscreenchange: 'handleFullscreenChange', + webkitbeginfullscreen: 'handleFullscreenChange', + webkitendfullscreen: 'handleFullscreenChange', + }, + HideControlsDelay: 4 * 1000, + RewindAmount: 30, + MaximumSeekRate: 8, + SeekDelay: 1500, + ClassNames: { + active: 'active', + exit: 'exit', + failed: 'failed', + hidden: 'hidden', + hiding: 'hiding', + hourLongTime: 'hour-long-time', + list: 'list', + muteBox: 'mute-box', + muted: 'muted', + paused: 'paused', + playing: 'playing', + selected: 'selected', + show: 'show', + thumbnail: 'thumbnail', + thumbnailImage: 'thumbnail-image', + thumbnailTrack: 'thumbnail-track', + volumeBox: 'volume-box', + noVideo: 'no-video', + down: 'down', + out: 'out', + }, + KeyCodes: { + enter: 13, + escape: 27, + space: 32, + pageUp: 33, + pageDown: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40 + }, + + extend: function(child) + { + for (var property in this) { + if (!child.hasOwnProperty(property)) + child[property] = this[property]; + } + }, + + UIString: function(developmentString, replaceString, replacementString) + { + var localized = UIStringTable[developmentString]; + if (replaceString && replacementString) + return localized.replace(replaceString, replacementString); + + if (localized) + return localized; + + console.error("Localization for string \"" + developmentString + "\" not found."); + return "LOCALIZED STRING NOT FOUND"; + }, + + listenFor: function(element, eventName, handler, useCapture) + { + if (typeof useCapture === 'undefined') + useCapture = false; + + if (!(this.listeners[eventName] instanceof Array)) + this.listeners[eventName] = []; + this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture}); + element.addEventListener(eventName, this, useCapture); + }, + + stopListeningFor: function(element, eventName, handler, useCapture) + { + if (typeof useCapture === 'undefined') + useCapture = false; + + if (!(this.listeners[eventName] instanceof Array)) + return; + + this.listeners[eventName] = this.listeners[eventName].filter(function(entry) { + return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture); + }); + element.removeEventListener(eventName, this, useCapture); + }, + + addVideoListeners: function() + { + for (var name in this.HandledVideoEvents) { + this.listenFor(this.video, name, this.HandledVideoEvents[name]); + }; + + /* text tracks */ + this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange); + this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd); + this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove); + + /* audio tracks */ + this.listenFor(this.video.audioTracks, 'change', this.updateHasAudio); + this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio); + this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio); + + /* video tracks */ + this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo); + this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo); + this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo); + + /* controls attribute */ + this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this)); + this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] }); + }, + + removeVideoListeners: function() + { + for (var name in this.HandledVideoEvents) { + this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]); + }; + + /* text tracks */ + this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange); + this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd); + this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove); + + /* audio tracks */ + this.stopListeningFor(this.video.audioTracks, 'change', this.updateHasAudio); + this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio); + this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio); + + /* video tracks */ + this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo); + this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo); + this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo); + + /* controls attribute */ + this.controlsObserver.disconnect(); + delete(this.controlsObserver); + }, + + handleEvent: function(event) + { + var preventDefault = false; + + try { + if (event.target === this.video) { + var handlerName = this.HandledVideoEvents[event.type]; + var handler = this[handlerName]; + if (handler && handler instanceof Function) + handler.call(this, event); + } + + if (!(this.listeners[event.type] instanceof Array)) + return; + + this.listeners[event.type].forEach(function(entry) { + if (entry.element === event.currentTarget && entry.handler instanceof Function) + preventDefault |= entry.handler.call(this, event); + }, this); + } catch(e) { + if (window.console) + console.error(e); + } + + if (preventDefault) { + event.stopPropagation(); + event.preventDefault(); + } + }, + + createBase: function() + { + var base = this.base = document.createElement('div'); + base.setAttribute('pseudo', '-webkit-media-controls'); + this.listenFor(base, 'mousemove', this.handleWrapperMouseMove); + this.listenFor(base, 'mouseout', this.handleWrapperMouseOut); + if (this.host.textTrackContainer) + base.appendChild(this.host.textTrackContainer); + }, + + shouldHaveAnyUI: function() + { + return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length); + }, + + shouldHaveControls: function() + { + return this.video.controls || this.isFullScreen(); + }, + + setNeedsTimelineMetricsUpdate: function() + { + this.timelineMetricsNeedsUpdate = true; + }, + + updateTimelineMetricsIfNeeded: function() + { + if (this.timelineMetricsNeedsUpdate) { + this.timelineLeft = this.controls.timeline.offsetLeft; + this.timelineWidth = this.controls.timeline.offsetWidth; + this.timelineHeight = this.controls.timeline.offsetHeight; + this.timelineMetricsNeedsUpdate = false; + } + }, + + updateBase: function() + { + if (this.shouldHaveAnyUI()) { + if (!this.base.parentNode) { + this.root.appendChild(this.base); + } + } else { + if (this.base.parentNode) { + this.base.parentNode.removeChild(this.base); + } + } + }, + + createControls: function() + { + var panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div'); + panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent'); + + var panel = this.controls.panel = document.createElement('div'); + panel.setAttribute('pseudo', '-webkit-media-controls-panel'); + panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback'))); + panel.setAttribute('role', 'toolbar'); + this.listenFor(panel, 'mousedown', this.handlePanelMouseDown); + this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd); + this.listenFor(panel, 'click', this.handlePanelClick); + this.listenFor(panel, 'dblclick', this.handlePanelClick); + + var rewindButton = this.controls.rewindButton = document.createElement('button'); + rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button'); + rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount)); + this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked); + + var seekBackButton = this.controls.seekBackButton = document.createElement('button'); + seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button'); + seekBackButton.setAttribute('aria-label', this.UIString('Rewind')); + this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown); + this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp); + + var seekForwardButton = this.controls.seekForwardButton = document.createElement('button'); + seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button'); + seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward')); + this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown); + this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp); + + var playButton = this.controls.playButton = document.createElement('button'); + playButton.setAttribute('pseudo', '-webkit-media-controls-play-button'); + playButton.setAttribute('aria-label', this.UIString('Play')); + this.listenFor(playButton, 'click', this.handlePlayButtonClicked); + + var statusDisplay = this.controls.statusDisplay = document.createElement('div'); + statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display'); + statusDisplay.classList.add(this.ClassNames.hidden); + + var timelineBox = this.controls.timelineBox = document.createElement('div'); + timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container'); + + var currentTime = this.controls.currentTime = document.createElement('div'); + currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display'); + currentTime.setAttribute('aria-label', this.UIString('Elapsed')); + currentTime.setAttribute('role', 'timer'); + + var timeline = this.controls.timeline = document.createElement('input'); + this.timelineID = ++Controller.gLastTimelineId; + timeline.setAttribute('pseudo', '-webkit-media-controls-timeline'); + timeline.setAttribute('aria-label', this.UIString('Duration')); + timeline.style.backgroundImage = '-webkit-canvas(timeline-' + this.timelineID + ')'; + timeline.type = 'range'; + timeline.value = 0; + this.listenFor(timeline, 'input', this.handleTimelineChange); + this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver); + this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut); + this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove); + this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown); + this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp); + timeline.step = .01; + + var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div'); + thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack); + + var thumbnail = this.controls.thumbnail = document.createElement('div'); + thumbnail.classList.add(this.ClassNames.thumbnail); + + var thumbnailImage = this.controls.thumbnailImage = document.createElement('img'); + thumbnailImage.classList.add(this.ClassNames.thumbnailImage); + + var remainingTime = this.controls.remainingTime = document.createElement('div'); + remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display'); + remainingTime.setAttribute('aria-label', this.UIString('Remaining')); + remainingTime.setAttribute('role', 'timer'); + + var muteBox = this.controls.muteBox = document.createElement('div'); + muteBox.classList.add(this.ClassNames.muteBox); + + var muteButton = this.controls.muteButton = document.createElement('button'); + muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button'); + muteButton.setAttribute('aria-label', this.UIString('Mute')); + this.listenFor(muteButton, 'click', this.handleMuteButtonClicked); + + var minButton = this.controls.minButton = document.createElement('button'); + minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button'); + minButton.setAttribute('aria-label', this.UIString('Minimum Volume')); + this.listenFor(minButton, 'click', this.handleMinButtonClicked); + + var maxButton = this.controls.maxButton = document.createElement('button'); + maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button'); + maxButton.setAttribute('aria-label', this.UIString('Maximum Volume')); + this.listenFor(maxButton, 'click', this.handleMaxButtonClicked); + + var volumeBox = this.controls.volumeBox = document.createElement('div'); + volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container'); + volumeBox.classList.add(this.ClassNames.volumeBox); + + var volume = this.controls.volume = document.createElement('input'); + volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider'); + volume.setAttribute('aria-label', this.UIString('Volume')); + volume.type = 'range'; + volume.min = 0; + volume.max = 1; + volume.step = .01; + this.listenFor(volume, 'input', this.handleVolumeSliderInput); + + var captionButton = this.controls.captionButton = document.createElement('button'); + captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button'); + captionButton.setAttribute('aria-label', this.UIString('Captions')); + captionButton.setAttribute('aria-haspopup', 'true'); + captionButton.setAttribute('aria-owns', 'audioTrackMenu'); + this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked); + + var fullscreenButton = this.controls.fullscreenButton = document.createElement('button'); + fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button'); + fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen')); + this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked); + }, + + setControlsType: function(type) + { + if (type === this.controlsType) + return; + this.controlsType = type; + + this.reconnectControls(); + }, + + setIsLive: function(live) + { + if (live === this.isLive) + return; + this.isLive = live; + + this.updateStatusDisplay(); + + this.reconnectControls(); + }, + + reconnectControls: function() + { + this.disconnectControls(); + + if (this.controlsType === Controller.InlineControls) + this.configureInlineControls(); + else if (this.controlsType == Controller.FullScreenControls) + this.configureFullScreenControls(); + + if (this.shouldHaveControls()) + this.addControls(); + }, + + disconnectControls: function(event) + { + for (var item in this.controls) { + var control = this.controls[item]; + if (control && control.parentNode) + control.parentNode.removeChild(control); + } + }, + + configureInlineControls: function() + { + if (!this.isLive) + this.controls.panel.appendChild(this.controls.rewindButton); + this.controls.panel.appendChild(this.controls.playButton); + this.controls.panel.appendChild(this.controls.statusDisplay); + if (!this.isLive) { + this.controls.panel.appendChild(this.controls.timelineBox); + this.controls.timelineBox.appendChild(this.controls.currentTime); + this.controls.timelineBox.appendChild(this.controls.thumbnailTrack); + this.controls.thumbnailTrack.appendChild(this.controls.timeline); + this.controls.thumbnailTrack.appendChild(this.controls.thumbnail); + this.controls.thumbnail.appendChild(this.controls.thumbnailImage); + this.controls.timelineBox.appendChild(this.controls.remainingTime); + } + this.controls.panel.appendChild(this.controls.muteBox); + this.controls.muteBox.appendChild(this.controls.volumeBox); + this.controls.volumeBox.appendChild(this.controls.volume); + this.controls.muteBox.appendChild(this.controls.muteButton); + this.controls.panel.appendChild(this.controls.captionButton); + if (!this.isAudio()) + this.controls.panel.appendChild(this.controls.fullscreenButton); + + this.controls.panel.style.removeProperty('left'); + this.controls.panel.style.removeProperty('top'); + this.controls.panel.style.removeProperty('bottom'); + }, + + configureFullScreenControls: function() + { + this.controls.panel.appendChild(this.controls.volumeBox); + this.controls.volumeBox.appendChild(this.controls.minButton); + this.controls.volumeBox.appendChild(this.controls.volume); + this.controls.volumeBox.appendChild(this.controls.maxButton); + this.controls.panel.appendChild(this.controls.seekBackButton); + this.controls.panel.appendChild(this.controls.playButton); + this.controls.panel.appendChild(this.controls.seekForwardButton); + this.controls.panel.appendChild(this.controls.captionButton); + if (!this.isAudio()) + this.controls.panel.appendChild(this.controls.fullscreenButton); + if (!this.isLive) { + this.controls.panel.appendChild(this.controls.timelineBox); + this.controls.timelineBox.appendChild(this.controls.currentTime); + this.controls.timelineBox.appendChild(this.controls.thumbnailTrack); + this.controls.thumbnailTrack.appendChild(this.controls.timeline); + this.controls.thumbnailTrack.appendChild(this.controls.thumbnail); + this.controls.thumbnail.appendChild(this.controls.thumbnailImage); + this.controls.timelineBox.appendChild(this.controls.remainingTime); + } else + this.controls.panel.appendChild(this.controls.statusDisplay); + }, + + updateControls: function() + { + if (this.isFullScreen()) + this.setControlsType(Controller.FullScreenControls); + else + this.setControlsType(Controller.InlineControls); + + this.setNeedsTimelineMetricsUpdate(); + }, + + updateStatusDisplay: function(event) + { + if (this.video.error !== null) + this.controls.statusDisplay.innerText = this.UIString('Error'); + else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) + this.controls.statusDisplay.innerText = this.UIString('Live Broadcast'); + else if (this.video.networkState === HTMLMediaElement.NETWORK_LOADING) + this.controls.statusDisplay.innerText = this.UIString('Loading'); + else + this.controls.statusDisplay.innerText = ''; + + this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error); + }, + + handleLoadStart: function(event) + { + this.updateStatusDisplay(); + this.updateProgress(); + }, + + handleError: function(event) + { + this.updateStatusDisplay(); + }, + + handleAbort: function(event) + { + this.updateStatusDisplay(); + }, + + handleSuspend: function(event) + { + this.updateStatusDisplay(); + }, + + handleStalled: function(event) + { + this.updateStatusDisplay(); + this.updateProgress(); + }, + + handleWaiting: function(event) + { + this.updateStatusDisplay(); + }, + + handleReadyStateChange: function(event) + { + this.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0; + this.updateReadyState(); + this.updateDuration(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + this.updateFullscreenButton(); + this.updateProgress(); + }, + + handleTimeUpdate: function(event) + { + if (!this.scrubbing) + this.updateTime(); + }, + + handleDurationChange: function(event) + { + this.updateDuration(); + this.updateTime(true); + this.updateProgress(true); + }, + + handlePlay: function(event) + { + this.setPlaying(true); + }, + + handlePause: function(event) + { + this.setPlaying(false); + }, + + handleProgress: function(event) + { + this.updateProgress(); + }, + + handleVolumeChange: function(event) + { + this.updateVolume(); + }, + + handleTextTrackChange: function(event) + { + this.updateCaptionContainer(); + }, + + handleTextTrackAdd: function(event) + { + var track = event.track; + + if (this.trackHasThumbnails(track) && track.mode === 'disabled') + track.mode = 'hidden'; + + this.updateThumbnail(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + }, + + handleTextTrackRemove: function(event) + { + this.updateThumbnail(); + this.updateCaptionButton(); + this.updateCaptionContainer(); + }, + + isFullScreen: function() + { + return this.video.webkitDisplayingFullscreen; + }, + + handleFullscreenChange: function(event) + { + this.updateBase(); + this.updateControls(); + + if (this.isFullScreen()) { + this.controls.fullscreenButton.classList.add(this.ClassNames.exit); + this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen')); + this.host.enteredFullscreen(); + } else { + this.controls.fullscreenButton.classList.remove(this.ClassNames.exit); + this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen')); + this.host.exitedFullscreen(); + } + }, + + handleWrapperMouseMove: function(event) + { + this.showControls(); + this.resetHideControlsTimer(); + + if (!this.isDragging) + return; + var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y); + this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px'; + this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px'; + event.stopPropagation() + }, + + handleWrapperMouseOut: function(event) + { + this.hideControls(); + this.clearHideControlsTimer(); + }, + + handleWrapperMouseUp: function(event) + { + this.isDragging = false; + this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true); + }, + + handlePanelMouseDown: function(event) + { + if (event.target != this.controls.panel) + return; + + if (!this.isFullScreen()) + return; + + this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true); + this.isDragging = true; + this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY); + this.initialOffset = new WebKitPoint( + parseInt(this.controls.panel.style.left) | 0, + parseInt(this.controls.panel.style.top) | 0 + ); + }, + + handlePanelTransitionEnd: function(event) + { + var opacity = window.getComputedStyle(this.controls.panel).opacity; + if (parseInt(opacity) > 0) + this.controls.panel.classList.remove(this.ClassNames.hidden); + else + this.controls.panel.classList.add(this.ClassNames.hidden); + }, + + handlePanelClick: function(event) + { + // Prevent clicks in the panel from playing or pausing the video in a MediaDocument. + event.preventDefault(); + }, + + handleRewindButtonClicked: function(event) + { + var newTime = Math.max( + this.video.currentTime - this.RewindAmount, + this.video.seekable.start(0)); + this.video.currentTime = newTime; + return true; + }, + + canPlay: function() + { + return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA; + }, + + handlePlayButtonClicked: function(event) + { + if (this.canPlay()) + this.video.play(); + else + this.video.pause(); + return true; + }, + + handleTimelineChange: function(event) + { + this.video.fastSeek(this.controls.timeline.value); + }, + + handleTimelineDown: function(event) + { + this.controls.thumbnail.classList.add(this.ClassNames.show); + }, + + handleTimelineUp: function(event) + { + this.controls.thumbnail.classList.remove(this.ClassNames.show); + }, + + handleTimelineMouseOver: function(event) + { + this.controls.thumbnail.classList.add(this.ClassNames.show); + }, + + handleTimelineMouseOut: function(event) + { + this.controls.thumbnail.classList.remove(this.ClassNames.show); + }, + + handleTimelineMouseMove: function(event) + { + if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden)) + return; + + this.updateTimelineMetricsIfNeeded(); + this.controls.thumbnail.classList.add(this.ClassNames.show); + var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY)); + var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth; + percent = Math.max(Math.min(1, percent), 0); + this.controls.thumbnail.style.left = percent * 100 + '%'; + + var thumbnailTime = percent * this.video.duration; + for (var i = 0; i < this.video.textTracks.length; ++i) { + var track = this.video.textTracks[i]; + if (!this.trackHasThumbnails(track)) + continue; + + if (!track.cues) + continue; + + for (var j = 0; j < track.cues.length; ++j) { + var cue = track.cues[j]; + if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) { + this.controls.thumbnailImage.src = cue.text; + return; + } + } + } + }, + + handleTimelineMouseDown: function(event) + { + this.scrubbing = true; + }, + + handleTimelineMouseUp: function(event) + { + this.scrubbing = false; + + // Do a precise seek when we lift the mouse: + this.video.currentTime = this.controls.timeline.value; + }, + + handleMuteButtonClicked: function(event) + { + this.video.muted = !this.video.muted; + if (this.video.muted) + this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute')); + return true; + }, + + handleMinButtonClicked: function(event) + { + if (this.video.muted) { + this.video.muted = false; + this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); + } + this.video.volume = 0; + return true; + }, + + handleMaxButtonClicked: function(event) + { + if (this.video.muted) { + this.video.muted = false; + this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); + } + this.video.volume = 1; + }, + + handleVolumeSliderInput: function(event) + { + if (this.video.muted) { + this.video.muted = false; + this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); + } + this.video.volume = this.controls.volume.value; + }, + + handleCaptionButtonClicked: function(event) + { + if (this.captionMenu) + this.destroyCaptionMenu(); + else + this.buildCaptionMenu(); + return true; + }, + + updateFullscreenButton: function() + { + this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, (!this.video.webkitSupportsFullscreen || !this.hasVisualMedia)); + }, + + handleFullscreenButtonClicked: function(event) + { + if (this.isFullScreen()) + this.video.webkitExitFullscreen(); + else + this.video.webkitEnterFullscreen(); + return true; + }, + + handleControlsChange: function() + { + try { + this.updateBase(); + + if (this.shouldHaveControls()) + this.addControls(); + else + this.removeControls(); + } catch(e) { + if (window.console) + console.error(e); + } + }, + + nextRate: function() + { + return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2)); + }, + + handleSeekBackMouseDown: function(event) + { + this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking); + this.video.play(); + this.video.playbackRate = this.nextRate() * -1; + this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay); + }, + + seekBackFaster: function() + { + this.video.playbackRate = this.nextRate() * -1; + }, + + handleSeekBackMouseUp: function(event) + { + this.video.playbackRate = this.video.defaultPlaybackRate; + if (this.actionAfterSeeking === Controller.PauseAfterSeeking) + this.video.pause(); + else if (this.actionAfterSeeking === Controller.PlayAfterSeeking) + this.video.play(); + if (this.seekInterval) + clearInterval(this.seekInterval); + }, + + handleSeekForwardMouseDown: function(event) + { + this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking); + this.video.play(); + this.video.playbackRate = this.nextRate(); + this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay); + }, + + seekForwardFaster: function() + { + this.video.playbackRate = this.nextRate(); + }, + + handleSeekForwardMouseUp: function(event) + { + this.video.playbackRate = this.video.defaultPlaybackRate; + if (this.actionAfterSeeking === Controller.PauseAfterSeeking) + this.video.pause(); + else if (this.actionAfterSeeking === Controller.PlayAfterSeeking) + this.video.play(); + if (this.seekInterval) + clearInterval(this.seekInterval); + }, + + updateDuration: function() + { + var duration = this.video.duration; + this.controls.timeline.min = 0; + this.controls.timeline.max = duration; + + this.setIsLive(duration === Number.POSITIVE_INFINITY); + + this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60); + this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60); + }, + + progressFillStyle: function(context) + { + var height = this.timelineHeight; + var gradient = context.createLinearGradient(0, 0, 0, height); + gradient.addColorStop(0, 'rgb(2, 2, 2)'); + gradient.addColorStop(1, 'rgb(23, 23, 23)'); + return gradient; + }, + + updateProgress: function(forceUpdate) + { + if (!forceUpdate && this.controlsAreHidden()) + return; + + this.updateTimelineMetricsIfNeeded(); + + var width = this.timelineWidth; + var height = this.timelineHeight; + + var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height); + context.clearRect(0, 0, width, height); + + context.fillStyle = this.progressFillStyle(context); + + var duration = this.video.duration; + var buffered = this.video.buffered; + for (var i = 0, end = buffered.length; i < end; ++i) { + var startTime = buffered.start(i); + var endTime = buffered.end(i); + + var startX = width * startTime / duration; + var endX = width * endTime / duration; + context.fillRect(startX, 0, endX - startX, height); + } + }, + + formatTime: function(time) + { + if (isNaN(time)) + time = 0; + var absTime = Math.abs(time); + var intSeconds = Math.floor(absTime % 60).toFixed(0); + var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); + var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); + var sign = time < 0 ? '-' : String(); + + if (intHours > 0) + return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2); + + return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2) + }, + + updatePlaying: function() + { + this.setPlaying(!this.canPlay()); + }, + + setPlaying: function(isPlaying) + { + if (this.isPlaying === isPlaying) + return; + this.isPlaying = isPlaying; + + if (!isPlaying) { + this.controls.panel.classList.add(this.ClassNames.paused); + this.controls.playButton.classList.add(this.ClassNames.paused); + this.controls.playButton.setAttribute('aria-label', this.UIString('Play')); + this.showControls(); + } else { + this.controls.panel.classList.remove(this.ClassNames.paused); + this.controls.playButton.classList.remove(this.ClassNames.paused); + this.controls.playButton.setAttribute('aria-label', this.UIString('Pause')); + + this.hideControls(); + this.resetHideControlsTimer(); + } + }, + + showControls: function() + { + this.controls.panel.classList.add(this.ClassNames.show); + this.controls.panel.classList.remove(this.ClassNames.hidden); + + this.updateTime(); + this.setNeedsTimelineMetricsUpdate(); + }, + + hideControls: function() + { + this.controls.panel.classList.remove(this.ClassNames.show); + }, + + controlsAreAlwaysVisible: function() + { + return this.controls.panel.classList.contains(this.ClassNames.noVideo); + }, + + controlsAreHidden: function() + { + if (this.controlsAreAlwaysVisible()) + return false; + + var panel = this.controls.panel; + return (!panel.classList.contains(this.ClassNames.show) || panel.classList.contains(this.ClassNames.hidden)) + && (panel.parentElement.querySelector(':hover') !== panel); + }, + + removeControls: function() + { + if (this.controls.panel.parentNode) + this.controls.panel.parentNode.removeChild(this.controls.panel); + this.destroyCaptionMenu(); + }, + + addControls: function() + { + this.base.appendChild(this.controls.panelCompositedParent); + this.controls.panelCompositedParent.appendChild(this.controls.panel); + this.setNeedsTimelineMetricsUpdate(); + }, + + updateTime: function(forceUpdate) + { + if (!forceUpdate && this.controlsAreHidden()) + return; + + var currentTime = this.video.currentTime; + var timeRemaining = currentTime - this.video.duration; + this.controls.currentTime.innerText = this.formatTime(currentTime); + this.controls.timeline.value = this.video.currentTime; + this.controls.remainingTime.innerText = this.formatTime(timeRemaining); + }, + + updateReadyState: function() + { + this.updateStatusDisplay(); + }, + + setStatusHidden: function(hidden) + { + if (this.statusHidden === hidden) + return; + + this.statusHidden = hidden; + + if (hidden) { + this.controls.statusDisplay.classList.add(this.ClassNames.hidden); + this.controls.currentTime.classList.remove(this.ClassNames.hidden); + this.controls.timeline.classList.remove(this.ClassNames.hidden); + this.controls.remainingTime.classList.remove(this.ClassNames.hidden); + this.setNeedsTimelineMetricsUpdate(); + } else { + this.controls.statusDisplay.classList.remove(this.ClassNames.hidden); + this.controls.currentTime.classList.add(this.ClassNames.hidden); + this.controls.timeline.classList.add(this.ClassNames.hidden); + this.controls.remainingTime.classList.add(this.ClassNames.hidden); + } + }, + + trackHasThumbnails: function(track) + { + return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails'); + }, + + updateThumbnail: function() + { + for (var i = 0; i < this.video.textTracks.length; ++i) { + var track = this.video.textTracks[i]; + if (this.trackHasThumbnails(track)) { + this.controls.thumbnail.classList.remove(this.ClassNames.hidden); + return; + } + } + + this.controls.thumbnail.classList.add(this.ClassNames.hidden); + }, + + updateCaptionButton: function() + { + if (this.video.webkitHasClosedCaptions) + this.controls.captionButton.classList.remove(this.ClassNames.hidden); + else + this.controls.captionButton.classList.add(this.ClassNames.hidden); + }, + + updateCaptionContainer: function() + { + if (!this.host.textTrackContainer) + return; + + var hasClosedCaptions = this.video.webkitHasClosedCaptions; + var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden); + + if (hasClosedCaptions && hasHiddenClass) + this.host.textTrackContainer.classList.remove(this.ClassNames.hidden); + else if (!hasClosedCaptions && !hasHiddenClass) + this.host.textTrackContainer.classList.add(this.ClassNames.hidden); + + this.updateBase(); + this.host.updateTextTrackContainer(); + }, + + buildCaptionMenu: function() + { + var tracks = this.host.sortedTrackListForMenu(this.video.textTracks); + if (!tracks || !tracks.length) + return; + + this.captionMenu = document.createElement('div'); + this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container'); + this.captionMenu.setAttribute('id', 'audioTrackMenu'); + this.base.appendChild(this.captionMenu); + this.captionMenuItems = []; + + var offItem = this.host.captionMenuOffItem; + var automaticItem = this.host.captionMenuAutomaticItem; + var displayMode = this.host.captionDisplayMode; + + var list = document.createElement('div'); + this.captionMenu.appendChild(list); + list.classList.add(this.ClassNames.list); + + var heading = document.createElement('h3'); + heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label + list.appendChild(heading); + heading.innerText = this.UIString('Subtitles'); + + var ul = document.createElement('ul'); + ul.setAttribute('role', 'menu'); + ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading'); + list.appendChild(ul); + + for (var i = 0; i < tracks.length; ++i) { + var menuItem = document.createElement('li'); + menuItem.setAttribute('role', 'menuitemradio'); + menuItem.setAttribute('tabindex', '-1'); + this.captionMenuItems.push(menuItem); + this.listenFor(menuItem, 'click', this.captionItemSelected); + this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp); + ul.appendChild(menuItem); + + var track = tracks[i]; + menuItem.innerText = this.host.displayNameForTrack(track); + menuItem.track = track; + + if (track === offItem) { + var offMenu = menuItem; + continue; + } + + if (track === automaticItem) { + if (displayMode === 'automatic') { + menuItem.classList.add(this.ClassNames.selected); + menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute('aria-checked', 'true'); + } + continue; + } + + if (displayMode != 'automatic' && track.mode === 'showing') { + var trackMenuItemSelected = true; + menuItem.classList.add(this.ClassNames.selected); + menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute('aria-checked', 'true'); + } + + } + + if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) { + offMenu.classList.add(this.ClassNames.selected); + menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute('aria-checked', 'true'); + } + + // focus first selected menuitem + for (var i = 0, c = this.captionMenuItems.length; i < c; i++) { + var item = this.captionMenuItems[i]; + if (item.classList.contains(this.ClassNames.selected)) { + item.focus(); + break; + } + } + + }, + + captionItemSelected: function(event) + { + this.host.setSelectedTextTrack(event.target.track); + this.destroyCaptionMenu(); + }, + + focusSiblingCaptionItem: function(event) + { + var currentItem = event.target; + var pendingItem = false; + switch(event.keyCode) { + case this.KeyCodes.left: + case this.KeyCodes.up: + pendingItem = currentItem.previousSibling; + break; + case this.KeyCodes.right: + case this.KeyCodes.down: + pendingItem = currentItem.nextSibling; + break; + } + if (pendingItem) { + currentItem.setAttribute('tabindex', '-1'); + pendingItem.setAttribute('tabindex', '0'); + pendingItem.focus(); + } + }, + + handleCaptionItemKeyUp: function(event) + { + switch (event.keyCode) { + case this.KeyCodes.enter: + case this.KeyCodes.space: + this.captionItemSelected(event); + break; + case this.KeyCodes.escape: + this.destroyCaptionMenu(); + break; + case this.KeyCodes.left: + case this.KeyCodes.up: + case this.KeyCodes.right: + case this.KeyCodes.down: + this.focusSiblingCaptionItem(event); + break; + default: + return; + } + // handled + event.stopPropagation(); + event.preventDefault(); + }, + + destroyCaptionMenu: function() + { + if (!this.captionMenu) + return; + + this.captionMenuItems.forEach(function(item){ + this.stopListeningFor(item, 'click', this.captionItemSelected); + this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp); + }, this); + + // FKA and AX: focus the trigger before destroying the element with focus + if (this.controls.captionButton) + this.controls.captionButton.focus(); + + if (this.captionMenu.parentNode) + this.captionMenu.parentNode.removeChild(this.captionMenu); + delete this.captionMenu; + delete this.captionMenuItems; + }, + + updateHasAudio: function() + { + if (this.video.audioTracks.length) + this.controls.muteBox.classList.remove(this.ClassNames.hidden); + else + this.controls.muteBox.classList.add(this.ClassNames.hidden); + }, + + updateHasVideo: function() + { + if (this.video.videoTracks.length) + this.controls.panel.classList.remove(this.ClassNames.noVideo); + else + this.controls.panel.classList.add(this.ClassNames.noVideo); + }, + + updateVolume: function() + { + if (this.video.muted || !this.video.volume) { + this.controls.muteButton.classList.add(this.ClassNames.muted); + this.controls.volume.value = 0; + } else { + this.controls.muteButton.classList.remove(this.ClassNames.muted); + this.controls.volume.value = this.video.volume; + } + }, + + isAudio: function() + { + return this.video instanceof HTMLAudioElement; + }, + + clearHideControlsTimer: function() + { + if (this.hideTimer) + clearTimeout(this.hideTimer); + this.hideTimer = null; + }, + + resetHideControlsTimer: function() + { + if (this.hideTimer) + clearTimeout(this.hideTimer); + this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay); + }, +}; diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsGtk.js b/Source/WebCore/Modules/mediacontrols/mediaControlsGtk.js new file mode 100644 index 000000000..bca7776c8 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsGtk.js @@ -0,0 +1,258 @@ +function createControls(root, video, host) +{ + return new ControllerGtk(root, video, host); +}; + +function ControllerGtk(root, video, host) +{ + Controller.call(this, root, video, host); +}; + +function contains(list, obj) +{ + var i = list.length; + while (i--) + if (list[i] === obj) + return true; + return false; +}; + +ControllerGtk.prototype = { + + createControls: function() + { + Controller.prototype.createControls.apply(this); + + this.controls.currentTime.classList.add(this.ClassNames.hidden); + this.controls.remainingTime.classList.add(this.ClassNames.hidden); + + this.controls.volumeBox.classList.add(this.ClassNames.hiding); + + this.listenFor(this.controls.muteBox, 'mouseout', this.handleVolumeBoxMouseOut); + this.listenFor(this.controls.muteButton, 'mouseover', this.handleMuteButtonMouseOver); + this.listenFor(this.controls.volumeBox, 'mouseover', this.handleMuteButtonMouseOver); + this.listenFor(this.controls.volume, 'mouseover', this.handleMuteButtonMouseOver); + this.listenFor(this.controls.captionButton, 'mouseover', this.handleCaptionButtonMouseOver); + this.listenFor(this.controls.captionButton, 'mouseout', this.handleCaptionButtonMouseOut); + + var enclosure = this.controls.enclosure = document.createElement('div'); + enclosure.setAttribute('pseudo', '-webkit-media-controls-enclosure'); + }, + + configureInlineControls: function() + { + this.controls.panel.appendChild(this.controls.playButton); + this.controls.panel.appendChild(this.controls.timeline); + this.controls.panel.appendChild(this.controls.currentTime); + this.controls.panel.appendChild(this.controls.remainingTime); + this.controls.panel.appendChild(this.controls.captionButton); + this.controls.panel.appendChild(this.controls.fullscreenButton); + this.controls.panel.appendChild(this.controls.muteBox); + this.controls.muteBox.appendChild(this.controls.muteButton); + this.controls.muteBox.appendChild(this.controls.volumeBox); + this.controls.volumeBox.appendChild(this.controls.volume); + this.controls.enclosure.appendChild(this.controls.panel); + }, + + shouldHaveControls: function() + { + return this.video.controls || this.isFullScreen(); + }, + + reconnectControls: function() + { + Controller.prototype.disconnectControls.apply(this, arguments); + + this.configureInlineControls(); + + if (this.shouldHaveControls()) + this.addControls(); + }, + + setStatusHidden: function(hidden) + { + }, + + updateTime: function(forceUpdate) + { + if (!forceUpdate && this.controlsAreHidden()) + return; + + var currentTime = this.video.currentTime; + var duration = this.video.duration; + + this.controls.currentTime.innerText = this.formatTime(currentTime); + this.controls.timeline.value = currentTime; + if (duration === Infinity || duration === NaN) + this.controls.remainingTime.classList.add(this.ClassNames.hidden); + else { + this.controls.currentTime.innerText += " / " + this.formatTime(duration); + this.controls.remainingTime.innerText = this.formatTime(duration); + if (this.controls.currentTime.classList.contains(this.ClassNames.hidden)) + this.controls.remainingTime.classList.remove(this.ClassNames.hidden); + } + + if (currentTime > 0) + this.showCurrentTime(); + }, + + showCurrentTime: function() + { + this.controls.currentTime.classList.remove(this.ClassNames.hidden); + this.controls.remainingTime.classList.add(this.ClassNames.hidden); + }, + + handlePlay: function(event) + { + Controller.prototype.handlePlay.apply(this, arguments); + this.showCurrentTime(); + if (!this.isLive) + this.showCurrentTime(); + }, + + handleTimeUpdate: function(event) + { + this.updateTime(); + }, + + handleMuteButtonMouseOver: function(event) + { + if (this.video.offsetTop + this.controls.enclosure.offsetTop < 105) { + this.controls.volumeBox.classList.add(this.ClassNames.down); + this.controls.panel.classList.add(this.ClassNames.down); + } else { + this.controls.volumeBox.classList.remove(this.ClassNames.down); + this.controls.panel.classList.remove(this.ClassNames.down); + } + this.controls.volumeBox.classList.remove(this.ClassNames.hiding); + return true; + }, + + handleVolumeBoxMouseOut: function(event) + { + this.controls.volumeBox.classList.add(this.ClassNames.hiding); + return true; + }, + + removeControls: function() + { + if (this.controls.enclosure.parentNode) + this.controls.enclosure.parentNode.removeChild(this.controls.enclosure); + this.destroyCaptionMenu(); + }, + + addControls: function() + { + this.base.appendChild(this.controls.enclosure); + }, + + updateReadyState: function() + { + if (this.host.supportsFullscreen && this.video.videoTracks.length) + this.controls.fullscreenButton.classList.remove(this.ClassNames.hidden); + else + this.controls.fullscreenButton.classList.add(this.ClassNames.hidden); + this.updateVolume(); + }, + + updateDuration: function() + { + Controller.prototype.updateDuration.apply(this, arguments); + if (this.isLive) + this.controls.timeline.max = 0; + }, + + setIsLive: function(live) + { + Controller.prototype.setIsLive.apply(this, arguments); + this.controls.timeline.disabled = this.isLive; + }, + + updatePlaying: function() + { + Controller.prototype.updatePlaying.apply(this, arguments); + if (!this.canPlay()) + this.showControls(); + }, + + handleCaptionButtonClicked: function(event) + { + this.handleCaptionButtonShowMenu(event) + return true; + }, + + buildCaptionMenu: function() + { + Controller.prototype.buildCaptionMenu.apply(this, arguments); + + this.listenFor(this.captionMenu, 'mouseout', this.handleCaptionMouseOut); + this.listenFor(this.captionMenu, 'transitionend', this.captionMenuTransitionEnd); + + this.captionMenu.captionMenuTreeElements = this.captionMenu.getElementsByTagName("*"); + + // Caption menu has to be centered to the caption button. + var captionButtonCenter = this.controls.panel.offsetLeft + this.controls.captionButton.offsetLeft + + this.controls.captionButton.offsetWidth / 2; + var captionMenuLeft = (captionButtonCenter - this.captionMenu.offsetWidth / 2); + if (captionMenuLeft + this.captionMenu.offsetWidth > this.controls.panel.offsetLeft + this.controls.panel.offsetWidth) + this.captionMenu.classList.add(this.ClassNames.out); + this.captionMenu.style.left = captionMenuLeft + 'px'; + // As height is not in the css, it needs to be specified to animate it. + this.captionMenu.height = this.captionMenu.offsetHeight; + this.captionMenu.style.height = 0; + }, + + destroyCaptionMenu: function() + { + this.hideCaptionMenu(); + }, + + showCaptionMenu: function() + { + this.captionMenu.style.height = this.captionMenu.height + 'px'; + }, + + hideCaptionMenu: function() + { + this.captionMenu.style.height = 0; + }, + + captionMenuTransitionEnd: function(event) + { + if (this.captionMenu.offsetHeight === 0) + Controller.prototype.destroyCaptionMenu.apply(this, arguments); + }, + + handleCaptionButtonMouseOver: function(event) + { + this.handleCaptionButtonShowMenu(event); + return true; + }, + + handleCaptionButtonShowMenu: function(event) + { + if (!this.captionMenu) + this.buildCaptionMenu(); + if (!contains(this.captionMenu.captionMenuTreeElements, event.relatedTarget)) + this.showCaptionMenu(); + return true; + }, + + handleCaptionButtonMouseOut: function(event) + { + if (this.captionMenu && !contains(this.captionMenu.captionMenuTreeElements, event.relatedTarget)) + this.hideCaptionMenu(); + return true; + }, + + handleCaptionMouseOut: function(event) + { + if (event.relatedTarget != this.controls.captionButton && + !contains(this.captionMenu.captionMenuTreeElements, event.relatedTarget)) + this.hideCaptionMenu(); + return true; + }, +}; + +Object.create(Controller.prototype).extend(ControllerGtk.prototype); +Object.defineProperty(ControllerGtk.prototype, 'constructor', { enumerable:false, value:ControllerGtk }); diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.css b/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.css new file mode 100644 index 000000000..28801ef86 --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.css @@ -0,0 +1,733 @@ +/* + * Copyright (C) 2012, 2013, 2014, 2015 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, + * 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. + */ + +/* You'll see a lot of !important rules in this file. This is because + the inheritance and specificity of Shadow DOM trees is slightly + tricky. The page might have accidentally set a style and we have + to make sure it is reset. */ + +audio { + min-width: 260px; + height: 39px; +} + +body:-webkit-full-page-media { + background-color: rgb(38, 38, 38); +} + +video:-webkit-full-page-media { + margin: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +video:-webkit-full-page-media::-webkit-media-controls-panel { + bottom: 0px; +} + +::-webkit-media-controls { + width: inherit; + height: inherit; + position: relative; + display: -webkit-flex !important; + -webkit-align-items: stretch; + -webkit-justify-content: flex-end; + -webkit-flex-direction: column; + font-family: -apple-system; + overflow: hidden; +} + +video::-webkit-media-controls-panel input[type="button"], +audio::-webkit-media-controls-panel input[type="button"], +video::-webkit-media-controls-panel button, +audio::-webkit-media-controls-panel button { + padding: 0; + border: none; + -webkit-appearance: none; +} + +video::-webkit-media-controls-inline-playback-placeholder, +audio::-webkit-media-controls-inline-playback-placeholder { + display: block; + z-index: 0; + width: 100%; + height: 100%; + background-color: black; + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: 100% 50%; +} + +video::-webkit-media-controls-inline-playback-placeholder.hidden, +audio::-webkit-media-controls-inline-playback-placeholder.hidden { + display: none; +} + +video::-webkit-media-text-track-container, +audio::-webkit-media-text-track-container { + position: relative; + -webkit-flex: 1 1 auto; +} + +video::-webkit-media-controls-panel-container { + -webkit-transform: translateZ(0); + width: 100%; + direction: ltr; + height: 50px; + position: absolute; + bottom: 0; + pointer-events: none; +} + +audio::-webkit-media-controls-panel-container { + width: 100%; + direction: ltr; + height: 39px; + position: absolute; + bottom: 0; +} + +video::-webkit-media-controls-panel-background { + -webkit-transform: translateZ(0); + width: 101%; /* Due to some rounding issues we make this a little bit wider than should be necessary. */ + height: 51px; /* And taller. */ + -webkit-appearance: media-controls-light-bar-background; + transition: opacity 0.25s linear; + opacity: 0; + pointer-events: none; + bottom: 0; + position: absolute; +} + +audio::-webkit-media-controls-panel-background { + display: none; +} + +video::-webkit-media-controls-panel-background.show, +video::-webkit-media-controls-panel-background.paused { + opacity: 1; +} + +audio::-webkit-media-show-controls { + display: none !important; +} +video::-webkit-media-show-controls { + position: absolute; + left: 0; + bottom: 0; + display: block; + margin: 0; + padding: 0; + width: 100%; + height: 10px; + opacity: 0; + border: 0; + background: none; + -webkit-appearance: none; +} + +audio::-webkit-media-controls-panel, +video::-webkit-media-controls-panel { + box-sizing: border-box; + position: absolute; + bottom: 0; + width: 100%; + padding: 0; + + -webkit-user-select: none; + + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-flex-wrap: nowrap; + -webkit-justify-content: flex-start; + -webkit-align-items: center; + + transition: opacity 0.25s linear; + -webkit-transform-origin: bottom left; +} + +video::-webkit-media-controls-panel .hidden, +audio::-webkit-media-controls-panel .hidden, +video::-webkit-media-controls-panel .dropped, +audio::-webkit-media-controls-panel .dropped { + display: none; +} + +video::-webkit-media-controls-panel { + height: 50px; + opacity: 0; + pointer-events: none; + -webkit-transform: translate3d(0, 0, 0); +} + +audio::-webkit-media-controls-panel { + height: 39px; + background-color: rgba(212, 212, 212, 1); +} + +video::-webkit-media-controls-panel.show, +video::-webkit-media-controls-panel.paused { + pointer-events: auto; + opacity: 1; +} + +video::-webkit-media-controls-panel.picture-in-picture { + pointer-events: none; +} + +video::-webkit-media-controls-rewind-button, +audio::-webkit-media-controls-rewind-button, +video::-webkit-media-controls-panel .mute-box, +audio::-webkit-media-controls-panel .mute-box, +video::-webkit-media-controls-mute-button, +audio::-webkit-media-controls-mute-button, +video::-webkit-media-controls-volume-max-button, +video::-webkit-media-controls-panel .volume-box, +audio::-webkit-media-controls-panel .volume-box, +audio::-webkit-media-controls-volume-slider, +video::-webkit-media-controls-volume-slider { + display: none !important; +} + +video::-webkit-media-controls-start-playback-button { + -webkit-appearance: none; + position: absolute; + width: 72px; + height: 72px; + left: calc(50% - 36px); + top: calc(50% - 36px); + -webkit-transform: translate3d(0, 0, 0); +} + +video::-webkit-media-controls-start-playback-background { + -webkit-appearance: none; + position: absolute; + width: 72px; + height: 72px; + -webkit-appearance: media-controls-light-bar-background; + -webkit-transform: translateZ(0); + -webkit-clip-path: circle(36px); +} + +video::-webkit-media-controls-start-playback-button .webkit-media-controls-start-playback-glyph { + -webkit-appearance: none; + position: absolute; + width: 72px; + height: 72px; + opacity: 0.6; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAACMCAYAAACuwEE+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABKFJREFUeNrsnV9ojWEcx5+jhdUulF0YNS6IpDRJuNDhyi5cjBvkglGKCxtKIlxILkwi+VPsRnbDphTuLEXzJ8u/0UhstJupKRojx+/X85xIztlztrNznve8n099W9lZ3vd9Pr3P8z7P7zxvIpVKGQBfxnEJAGEAYQBhAGEAYQAQBhAGEAYQBhAGEAYAYQBhAGEAYQBhAGEAEAYQBhAGEAYQBgBhAGEAYQBhAGEAYQAQBhAGEAaCpCyKB51IJPTHRMkVyQpJr+Sa5IzkXZwasOAbQul/GLU4GvTw/8kPSZNkUpyEKWSi3CWtynDH3Cl5Kamny2UM8zezs/xuiuSC5JEkSTMjjDLV4zM1ktuSFkk1zR1fYSbo2DeHz6+VvJIcllTQ7PET5vsI/qZcss+Jsy5H4SDG8zDTJJcl9ySLUQBhfFnspGmWVKECwvig3dJGSbfrrspRAmF8qHAD4i5JHZcDYXyZIWl1j+I1XA6E8SVp7KTfWUkllwNhfK/NVslryS7JeC4JwvigC5nHJM8lKxEGfJkluekyF2HAF73LPJGcMDEqo0CY0aFlFDvc+GabiWghGsIUHn2COu3uOEmEAV90TKNzNzqHMxNhwBedJX4hOWpKtIwCYfKPztfsMXZ9quTKRBFm7NAV8HSZ6BKEAV90TequsTU41QgDPmgZhVb5abXfIRPhMgqEKSwqykET4TJRhCkO1eZPmegChAFftEz0oRscVyEM+LZBvXsM32sCL6NAmHDQib4jxk781SEM+KJLC+ky0XkIA74kJZ3GLm5WIgz4oGUTWj6hZRSNJoAyCoSJBlqodVzyTLIGYcCXOcbuunXD2C1NEAa8qJXcl0xHGPBFZ4svIgzkwlKEgVy4ijDgy2PJdoSB4dDtZXWxcpnkM8JANh4YOwO8RfKVx2rIRJ9kk7HlEHeKeSBltEXQDBo7w6tfW/kSwgEhTLi0SXZL3oZ0UAgTHrpCrdvft4d4cIxhwqHfPSYvClUW7jBhoG9gOWnspowDoR8swhSXW8Zuh9YVlQNGmOKg30tqdMJECsYwhWXAPfnMj6Is3GEKxy/JOckBN7iNLAgz9rS7x+TOUjgZhBk73rtxSlspnRTC5B+dwtepfJ3SHyy1k0OY/KFlB5eM3X2qr1RPEmHyQ4frfjpK/UR5rB4deifZYGxtbUccTpg7zMgIruwAYcKl1XU/PXE8ebokf3QeZbmxX1XtietFQJjh0ZlZfW/SQhNw2QFdUvEZkpwyESk7QJjioguD+raSbi4FXVI2tC6l1gVZECYj2uVoIVNkyw7okgrDT8l5Yzdb7kcHhMmGPvE0GPtyLKBLysgbyWpj51SQhTtMRtJlB02SbzR9/IT5JJns8TktO2iW7DclXHZAlzQ8Po2vK8i6krwZWRDmaZbffZCsNzEqO0CY4bn+n38bcmMUfctri+uOII8kUqnoXdNEIpEef6kUOiv70dhia91mvTdODVjo9oukMECXBAgDCAMIwyUAhAGEAYQBhAGEAUAYQBhAGEAYQBhAGACEAYQBhAGEAYQBhAFAGEAYQBhAGEAYAIQBhAGEAYQBhIEY8FuAAQBHs44NYWX3+AAAAABJRU5ErkJggg=='); + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: 100% 100%; + /* FIXME This is an ad-hoc fix until we fix the real compositing issue tracked by webkit.org/b/158147 */ + will-change: opacity; +} + +video::-webkit-media-controls-start-playback-button .webkit-media-controls-start-playback-glyph.active { + opacity: 1; +} + +video::-webkit-media-controls-start-playback-button .webkit-media-controls-start-playback-glyph.failed { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAACMCAYAAACuwEE+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABspJREFUeNrsnVvIVUUYhr8tZv5UZGCShdZFeBElJAQWVD9B5EVQEVIhQRR0URdZQXSAMjIoSqikqLCisIQO1kUHo8woKERMQjqoRUVlB43sfDD7m489izbimjXf3rPWnrV9XnhvF5s9z/rmm5l3rdWZmJgQhGLVARgEMAhgEMAggEEAgxDAIIBBAIMABgEMAhiEAAYBDAIYBDAIYBDAIAQwCGAQwCCAQQCDEMAggEEAgwAGAQwCGIQABgEMAhiUpxSYulyzpjq/6Py78xbnO52PYUTrHd82A7NY/4+9vNt5mfM0UGkvMIc7L3d+y/kh52MT/Rdr9wFM4W+cL3WeBDDtAmbM+eO9BvMv57sTVIGvAsAUfs95HGDaA8wFgcH8dsAq8G8EMIVXOc8GmPyBuTKyCpxu/B8ONMBSWJvjpc4HA0y+wMxx/jtiMLVaPGWsAhN9Wqeyi3RLAWDybHov9ne3pQqM1QhM4Xed5wNMnsvq2b6CxPYdX0RUgR8TQKO/5zHnmQCT5z7MfN+zWKrAvJL/YobzI857EoDzi/NNkZUNYBreuJvkV0fbIwdzjwejrAqc6LwuATTqz5zPA5g8d3p1tXKX35eJrQLXOU8puZ4O9LZE4KzzIAJMhkcDuvO72jCY2wJVQGG60cM1KDRa2R50ng4weZ4lnen8gWFAX3M+ruRaMxP2N9pcXxuobAAzxMPHyc5XOO+IHEw9bLw/UAVS9jdbnRcATJ6n1dM9CLsjB/MHD9rkkust9Ev1FOC8EqhsADPkeIMOzKuGwdQp7aySa435pXOK/kZBvkdaEKPYX/Mw1hXQaimPUaTsb3ZUVDaAGWKAaopvPmMrRBGjOKTkevP8xmCKaUor2zjA5Jm4s1aI7VIeo+j4I4hU/U2osgHMkCOaugJ6xzCYeiRxSgP9jVa2OySTGAXADFYhqmIUWr1Wii2c1U9lA5ghAdNbIW4VW4zilsBh4/yE/Y1WtpMBJi9gCqWMUaTsb/oJiAFMYoXCT6eKLUbxduCwUavQEkP1qqpsS6TBGAXA/K+quzZ1jMJavQYNiAFM6v+i5669OXDXHur3Y2JjFD9VHDam7G9CATGAqQmY2LtW90deEFuM4tzA6myRoXoNUtkApiZgYsPdC8QWowgdNupey9JE/Y3uAd0giWMUAFMNTLEqeTxw1xYxitggeRGjOKyB/iYUEAOYmoCJDXdbYxRVh41a2TZKupjo8QDTLDCx/Y1OOW8YDxvPSLQ6GyQgBjA1AROz35I6RpGyv9Gp8+p+YhQAMxgwMasSbTqvN8YoQoeN2t88nWia+sj5fIBpFpjY/iZljEI1btx9Dvll5yMApllgCn9asSo5ybhRtyGwrE/Z32hfdjTANA9MzMNrqWMUOn3dbth9LvNagBkeMDH9zUHGRva3imnP+hDf3v4DYIYLTOFdFedJWjlW9bGsl8T9zUqAyQOY2F1X60CHpr2iv/ku8lob/cEqwGQEjGWgvzdOezNKrjet4nRd+6MVfnpklZQpMDG7rlUDbX1me1/9zXrn09iHaQcwloF+yTjtnV0x7b3ufEk/gSuAGT4wvQN9TkMxCg4fRwCYmIHWs5+rjDGKeyXhM9sAkx8wMf1N6hgFwLQcmN7+5prAQM8V2/tqNsuAz2wDTN7A9OZlFjQUowCYEQAmpr+ZKrbnuf+UPl59DzDtAqa3v5kWiFE8KvF5YNMz2wDTPmB6G9nLAgNtfd4p6tX3ANNOYPQ44AGpzuYWMYqvjTGKowBmdIDp5yXR1jxwaXoQYNoDzOcy+PNFGqN41hijWAgw7QKmjg9dWGMU+sjMLIDJGxjtJ56Q+j6lo83y5RL/0utN4r9LDjD5AdPkx7osMYq5AJMXMLonskiG8znAOX5jMPT7TgCYPIDJ6YOjZTGKDUxJeQDznOT3SeMDpPsorZ5P/ezPno6so+nt1DmwnU6nbmCa1CZ/Ev2mtEipxxdgqrXTL5NX+JWQAAzA7Ev6ve3lvlfZJS0VwDQDzBrpRiu3SssFMPUC86F0nwhYIyMigKkHGJ1ybnO+z/kfGSEBTFpgFI6HpfvdgZ0yggKYdMDo8nix8/sywgKYwYH5RLofTH9e9gMBTP/A/Crdd9Etk26gWgCmzwu29Ghgp8THDmp7NXsbgOEsqavNklfsAGAyB+bJAChfSkOflwGY9gBzodg/PQww+zEw+vzyM76Z3eIb2lkgUi8wnQYGFo2QAAYBDAIYBDAIYBDAIAQwCGAQwCCAQQCDAAYhgEEAgwAGAQwCGAQwCAEMAhgEMAhgEMAgBDAIYBDAIIBBAIMABiGAQQCDGtN/AgwAK0XUGzgj/EoAAAAASUVORK5CYII='); +} + +/* ================== ALL INLINE BUTTONS ====================== */ + +video::-webkit-media-controls-play-button, +audio::-webkit-media-controls-play-button, +video::-webkit-media-controls-fullscreen-button, +audio::-webkit-media-controls-fullscreen-button, +video::-webkit-media-controls-wireless-playback-picker-button, +audio::-webkit-media-controls-wireless-playback-picker-button, +video::-webkit-media-controls-picture-in-picture-button, +audio::-webkit-media-controls-picture-in-picture-button { + -webkit-appearance: none; + display: block; + padding: 0; + border: 0; + -webkit-user-select: none; + background-repeat: no-repeat; + background-position: 50% 50%; + mix-blend-mode: plus-darker; + opacity: 0.55; + -webkit-mask-position: 50% 50%; + -webkit-mask-repeat: no-repeat; + -webkit-mask-origin: border-box; + background-color: black; + transition: background-color 250ms; +} + +video::-webkit-media-controls-play-button, +video::-webkit-media-controls-fullscreen-button, +video::-webkit-media-controls-wireless-playback-picker-button, +video::-webkit-media-controls-picture-in-picture-button { + -webkit-transform: translate3d(0, 0, 0); +} + +video::-webkit-media-controls-play-button:active, +audio::-webkit-media-controls-play-button:active, +video::-webkit-media-controls-fullscreen-button:active, +audio::-webkit-media-controls-fullscreen-button:active, +audio::-webkit-media-controls-wireless-playback-picker-button:active, +video::-webkit-media-controls-wireless-playback-picker-button:active, +video::-webkit-media-controls-picture-in-picture-button:active, +audio::-webkit-media-controls-picture-in-picture-button:active { + mix-blend-mode: normal; + opacity: 1; + background-color: white; + transition: background-color 0ms; +} + +/* ================== PLAY BUTTON ====================== */ + +audio::-webkit-media-controls-play-button, +video::-webkit-media-controls-play-button { + margin-left: 4px; + -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAsCAYAAAANUxr1AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAFVJREFUeNrs2bENgEAMA0AHsf/EIB5GcIGgOEvpIuf6TJIjXfZyv+qfe67ywJT7Vf+WnwUICAgICAgICAgICAgICAgICAgICAjoyzxvgvPlG1X/EmAASwgGTpkrSW0AAAAASUVORK5CYII='); + -webkit-mask-size: 18px 22px; + -webkit-order: 1; +} + +audio::-webkit-media-controls-play-button { + width: 42px; + height: 39px; +} + +video::-webkit-media-controls-play-button { + width: 44px; + height: 50px; +} + +audio::-webkit-media-controls-play-button.paused, +video::-webkit-media-controls-play-button.paused { + -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAsCAYAAAATmipGAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAUZJREFUeNrU2S8sRWEYgPFzD7PZbDYbQSAoBBKFwgQTmKRpJE3SaBKFRKJJJphiCslNt1AERRBsNpvN/DmeL7ybGfLzne3Xn517d873vqcsimIOtSKDq0Id0zmEhktM5hAazjGWQ2g4xUgOocknjjFkDw0fOMSAPfR78AH67KHhDXvotYeGV+yg2x4aXrCFLntoeMYGOuyh4QnraLeHhkesos0eGh6wglZ7aLjHMlrsoeEOi2i2h4ZbLKC0h4ZrzP8XXMk0MPvbeFRJXf0cjyq5NB5NlIX/qsX/1non6/afvvHXrsESeGN/PMUDv8n8Cl0yv0L1hxL9MU9/cE6jyJp5FNEPd2lc3kSneQGxbV5ApJXOLnqsK5137JuXZLF27Dcvco8waF7knmDYvBo/w6j5Y8MFxs2fb9LEN2UfnmaKDD4xfgkwANpCXZrruKGvAAAAAElFTkSuQmCC'); + -webkit-mask-size: 21px 22px; +} + +/* ================== FULLSCREEN BUTTON ====================== */ + +video::-webkit-media-controls-fullscreen-button { + width: 44px; + height: 50px; + -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAO5JREFUeNrs2F8KwjAMBvAoXsID6GEUPIKPw2sLPiu+CCIYO1nFhf1pWkw+YR9kTyv8KCNpR0TEyjqSUZiZ5vQH+d6dZ6gDDIy5BbyjISVwi4aUQEJDdgGhkH1AGOQQEAI5BnRHpgBdkalAN2QEnpsiNGQEamKKzAFK5CPUCg0YkbdQe8QdjFkifoPmjRr+wDoBJ2Bh1r9uRaW4UzNxKsQ2sxFjsUID1tmVIK0adQ7y/QfDcpJokeZALdIFqEG6AVORrsAUpDtwDAkBHELCAPuQUMAuJBxQIj/AheUVI2eRJfCifP9aP2boV7uXAAMArhj6EAChbh4AAAAASUVORK5CYII='); + -webkit-mask-size: 20px 20px; + -webkit-order: 5; +} + +/* ================== AIRPLAY BUTTON ====================== */ + +audio::-webkit-media-controls-wireless-playback-picker-button, +video::-webkit-media-controls-wireless-playback-picker-button { + -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAkCAYAAAD/yagrAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAARlJREFUeNrsmDEOgjAUhtuYMLgwuTG6Mrt7AC/gCVycHLwBN+AEXIADuDO76ubm5OJgYnw+kldDiEb6aElJ3ku+gYSWL4/yl1QrpUCNoLSIehbVgfmBiIqoiIqoiIqoiIpoUKLmAhz+7u2QpUPRj5tL0TXyQm5IGqpo3cVHY64LkoQmmlIXocURiUMRnSHnL5KGAxK5Fu1CiUxooilSdRhTNBIlpk7bPtfq5orkFMmWFmOzRqcSWsNeRE/0mk3ljK5sOqzrn6Kc2jMka57IaqjdwmQlMLkjC9+S7azkckXmviRt15TtmndSnK/UNkV6Fzf3OLnMroh2FvBM3vcArRhA0rDlimYDSrIzdjOw5N+M1QCjOMdVbwEGAJbOn8QOMeI8AAAAAElFTkSuQmCC'); + -webkit-mask-size: 21px 18px; + -webkit-order: 3; +} + +audio::-webkit-media-controls-wireless-playback-picker-button { + width: 44px; + height: 39px; +} + +video::-webkit-media-controls-wireless-playback-picker-button { + width: 44px; + height: 50px; +} + +audio::-webkit-media-controls-wireless-playback-picker-button.playing, +video::-webkit-media-controls-wireless-playback-picker-button.playing { + opacity: 1; + mix-blend-mode: normal; + background-color: -apple-wireless-playback-target-active; +} + +/* ================== PICTURE IN PICTURE BUTTON ====================== */ + +video::-webkit-media-controls-picture-in-picture-button { + width: 35px; + height: 44px; + -webkit-order: 5; + -webkit-mask-size: 23px 18px; + -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAkCAYAAAD2IghRAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANhJREFUeNrslzEOwjAMRR2UA3AQ7sLKwsTEglg6sTIgLgs7qCZBRbIsWpDayHH1v/SHJmn0ZFutHYiIqX4F+cDMtCCncgseh1JiLJ5lxH+Br2qGZ2Gpa/IjeVMZ1/ur0ndgLdaeydsvF9/7LrYEz7oo+J0X8Kyz2GuT917As04K/uAFPKtR5xov4FnHLuIy+i7AqavxVr1jAh7H/HbRZE3QZGndBvaW1k1Z6VqdHtjzIBELZrHoKIfRzbpUxqY4IOJzLpWAiAMc4AAHOMAB/m9T9Bn1veklwACtBYmnlYBaIQAAAABJRU5ErkJggg=='); +} + +video::-webkit-media-controls-picture-in-picture-button.return-from-picture-in-picture { + -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAkCAYAAAD2IghRAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOZJREFUeNpiZGBg+M8w+AEjMuf///8MTAxDFAxZh7Pgi5IBBv+HZYgPS4f/JwJ/GA1xCjMnIfAejf9poHPvfyy5GFvS6KdjyYPLXeAKiFiH/0ZiT6GT46ni8CAg/onEn0WH/EEVh4OAL5rjFwIx81BwOAh4APE3JPGlNHQ8VR0OAs5A/BVJbjUQsw0Fh4OAHRB/RpLPG0wOJwSsoTXnfBolF5o5HARUBqL2pYbDB6bGBDqchYYlAk27cqPt8YFuHVIaxYyjIT6ckwrjaIiPOnzU4aMOH3X4qMNHHU5sowjWmxhqACDAAI3lmdvpn4aTAAAAAElFTkSuQmCC'); +} + +/* ================== SPACER ====================== */ + +video::-webkit-media-controls-spacer, +audio::-webkit-media-controls-spacer { + -webkit-appearance: none !important; + -webkit-flex: 1 1 0; /* Should be the same as the timeline container. */ + -webkit-order: 2; + height: 8px; + margin: 0; + background-color: transparent !important; +} + +/* ================== TIMELINE ====================== */ + +video::-webkit-media-controls-timeline, +audio::-webkit-media-controls-timeline { + -webkit-appearance: none !important; + -webkit-flex: 1 1 0; + height: 8px; + margin: 0; + background-color: transparent !important; + background-size: 100% 100%; + border: none !important; + border-radius: 0 !important; + box-sizing: content-box !important; +} + +video::-webkit-media-controls-timeline { + -webkit-transform: translate3d(0, 0, 0); + mix-blend-mode: plus-darker; +} + +audio::-webkit-media-controls-timeline::-webkit-slider-runnable-track, +video::-webkit-media-controls-timeline::-webkit-slider-runnable-track { + -webkit-appearance: none !important; + background: none !important; + border: none !important; +} + +video::-webkit-media-controls-timeline::-webkit-slider-thumb, +audio::-webkit-media-controls-timeline::-webkit-slider-thumb { + -webkit-appearance: none !important; + width: 15px; + height: 50px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAiCAYAAABIiGl0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAxNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iUGl4ZWxtYXRvciAyLjIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTkwQ0UwODdBQTcxMTFFNEE5QTZGQTVGMjFBNkUxN0UiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTkwQ0UwODhBQTcxMTFFNEE5QTZGQTVGMjFBNkUxN0UiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1OTBDRTA4NUFBNzExMUU0QTlBNkZBNUYyMUE2RTE3RSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1OTBDRTA4NkFBNzExMUU0QTlBNkZBNUYyMUE2RTE3RSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pofoz4wAAAJGSURBVHjaxJfNTsJAEMfbLmoNaKIhIE18A+LReOXmi/gQnnwSLz6Fnjh4Id4kHElI5EDwpIkgImWdf/Nfs5ISCrR1kkmg7c5vZz/mw3WSy66oL7ojqqiQkPotOhGdaq1XGnMTvC+KlprN5kW9Xr8sFotnhUKhCsUHs9lsCB2NRu1Op3PfaDRa8vhDdCSqnQ0E3lX7/f6NGH7RCQXfYgzG0sZacthut6/G4/Gj3lAwFjZgKyn0qNfrXYdh+Ka3FNiALdhc6Sk+1CkL4Us997E0aXga5zmX3Y87vdVt9jTJnvPA/blJJZxEnbHwtJds8Mk6V2ZTAQMsAD1EJAQHpdSpk7GAARaYAPuISE5OQpYfeYwwmBeYrMhjZeJuHkKWAtj7B7DnJchQaQsylgvwHGktL6qwXsH0TE7NETw091gjiecFFtYzmNFSo3LICyysBzBxsI5xoWUJWllHL8lSfTnViFyTyGO4PhgMbrP2lgxNpnPAdBXkkBYDsg48lqbRDLrd7t18Pn9P21PYhG3zl8yoXsY+1zCjDEufgIxjMqPCHMm5wpdBBsVeQK2QpUzZs8dKsGY+SrG8DSxvj8j6DdOKHUPZhkO3KOiDBWiZDGW3MC4f7FPVYvKQyuE8YQvzFJMUcJg+qfit3YVKc4clqB8H3zAThWzmJmzstGN1fI511LVJXYzl7hZQ00F+2dA4sH3P7Am4a0zAeDklEODZYueoVgz+DS4rWk5tTdj2cmqt4tr9sccJFqyG3N4CGxrSu3AZ0MiPAAMAZrLkuVVmRJsAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-size: 15px 17px; + background-position: 0px 18px; + background-color: transparent !important; + border: none !important; + -webkit-transform: rotateZ(0deg); +} + +video::-webkit-media-controls-timeline::-webkit-slider-thumb:active, +audio::-webkit-media-controls-timeline::-webkit-slider-thumb:active { + background-color: transparent !important; +} + +audio::-webkit-media-controls-timeline::-webkit-slider-thumb { + height: 39px; + background-position: 0px 12px; +} + +video::-webkit-media-controls-current-time-display, +video::-webkit-media-controls-time-remaining-display, +audio::-webkit-media-controls-current-time-display, +audio::-webkit-media-controls-time-remaining-display { + -webkit-user-select: none; + -webkit-flex: 0 0 0; + display: -webkit-flex; + -webkit-align-items: center; + overflow-y: hidden; + overflow-x: hidden; + letter-spacing: normal; + word-spacing: normal; + line-height: normal; + text-transform: none; + text-indent: 0px; + text-decoration: none; + color: black; + mix-blend-mode: plus-darker; + opacity: 0.55; + height: 50px; + font-size: 13px; + font-family: -apple-system-monospaced-numbers; + -webkit-text-zoom: reset; +} + +audio::-webkit-media-controls-current-time-display, +video::-webkit-media-controls-current-time-display { + min-width: 32px; + -webkit-justify-content: flex-end; + padding-right: 6px; +} + +audio::-webkit-media-controls-time-remaining-display, +video::-webkit-media-controls-time-remaining-display { + min-width: 38px; + -webkit-justify-content: flex-start; + padding-left: 6px; + margin-right: 6px; +} + +video::-webkit-media-controls-timeline-container, +audio::-webkit-media-controls-timeline-container { + display: -webkit-flex; + -webkit-flex-direction: row; + -webkit-align-items: center; + -webkit-user-select: none; + -webkit-flex: 1 1 0; /* Any changes here should also be made on the spacer. */ + position: relative; + padding: 0; + -webkit-order: 2; + -webkit-text-size-adjust: auto; +} + +audio::-webkit-media-controls-timeline-container { + padding-right: 10px; +} + +audio::-webkit-media-controls-current-time-display.three-digit-time, +video::-webkit-media-controls-current-time-display.three-digit-time { + min-width: 31px; +} + +audio::-webkit-media-controls-time-remaining-display.three-digit-time, +video::-webkit-media-controls-time-remaining-display.three-digit-time { + min-width: 40px; +} + +audio::-webkit-media-controls-current-time-display.four-digit-time, +video::-webkit-media-controls-current-time-display.four-digit-time { + min-width: 40px; +} + +audio::-webkit-media-controls-time-remaining-display.four-digit-time, +video::-webkit-media-controls-time-remaining-display.four-digit-time { + min-width: 49px; +} + +audio::-webkit-media-controls-current-time-display.five-digit-time, +video::-webkit-media-controls-current-time-display.five-digit-time { + min-width: 51px; +} + +audio::-webkit-media-controls-time-remaining-display.five-digit-time, +video::-webkit-media-controls-time-remaining-display.five-digit-time { + min-width: 60px; +} + +audio::-webkit-media-controls-current-time-display.six-digit-time, +video::-webkit-media-controls-current-time-display.six-digit-time { + min-width: 60px; +} + +audio::-webkit-media-controls-time-remaining-display.six-digit-time, +video::-webkit-media-controls-time-remaining-display.six-digit-time { + min-width: 69px; +} + +/* ================== STATUS DISPLAY ====================== */ + +video::-webkit-media-controls-status-display, +audio::-webkit-media-controls-status-display { + + overflow: hidden; + font-family: -apple-system; + letter-spacing: normal; + word-spacing: normal; + line-height: normal; + text-transform: none; + text-indent: 0px; + text-decoration: none; + color: black; + mix-blend-mode: plus-darker; + opacity: 0.55; + -webkit-order: 2; +} + +/* ================== CAPTIONS ====================== */ + +video::-webkit-media-text-track-container { + position: relative; + width: 100%; + overflow: hidden; + padding-bottom: 5px; + z-index: 0; + + text-align: center; + color: rgba(255, 255, 255, 1); + + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0; + text-decoration: none; + pointer-events: none; + -webkit-user-select: none; + word-break: break-word; + + -webkit-flex: 1 1; + + -webkit-line-box-contain: block inline-box replaced; +} + +video::cue { + background-color: rgba(0, 0, 0, 0.8); +} + +video::-webkit-media-text-track-display { + position: absolute; + overflow: hidden; + white-space: pre-wrap; + -webkit-box-sizing: border-box; + font: 22px sans-serif; +} + +video::-webkit-media-text-track-display-backdrop { + display: inline-block; +} + +video::cue(:future) { + color: gray; +} + +video::-webkit-media-text-track-container b { + font-weight: bold; +} + +video::-webkit-media-text-track-container u { + text-decoration: underline; +} + +video::-webkit-media-text-track-container i { + font-style: italic; +} + +/* ==================== PLAYBACK PLACARD ==================== */ + +video::-webkit-media-controls-wireless-playback-status, +audio::-webkit-media-controls-wireless-playback-status { + display: block; + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 131 90"><g fill="none" stroke="-apple-system-gray" stroke-width="4"><rect x="2" y="2" width="127" height="76"/><line x1="30" y1="88" x2="101" y2="88"/></g></svg>'); + background-color: rgb(51, 51, 53); + background-repeat: no-repeat; + background-position: 50% calc(.5 * (100% - 25px) - 21pt); + background-size: 131px auto; + color: -apple-system-gray; + font: -apple-system; + font-weight: 400; + vertical-align: text-bottom; +} + +video::-webkit-media-controls-wireless-playback-text, +audio::-webkit-media-controls-wireless-playback-text { + cursor: default; + position: absolute; + width: 100%; + top: calc(.5 * (100% - 25px) + (.5 * (90px + 42pt) - 42pt)); + -webkit-user-select: none; + margin: 0px; + height: 42pt; +} + +video::-webkit-media-controls-wireless-playback-text-top, +audio::-webkit-media-controls-wireless-playback-text-top { + position: absolute; + top: 15pt; + width: 100%; + line-height: 12pt; + height: 12pt; + font-size: 12pt; + text-align: center; + margin: 0px; + color: -apple-system-gray; +} + +video::-webkit-media-controls-wireless-playback-text-bottom, +audio::-webkit-media-controls-wireless-playback-text-bottom { + position: absolute; + bottom: 0; + left: 5%; + width: 90%; + line-height: 10pt; + height: 10pt; + font-size: 10pt; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; +} + +video::-webkit-media-controls-wireless-playback-status.small, +audio::-webkit-media-controls-wireless-playback-status.small { + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62 43"><g fill="none" stroke="rgb(146,146,146)" stroke-width="2"><rect x="1" y="1" width="60" height="36"/><line x1="14" y1="42" x2="48" y2="42"/></g></svg>'); + background-position: 50% calc(.5 * (100% - 25px) - 5pt); + background-size: 62px auto; +} + +video::-webkit-media-controls-wireless-playback-text-top.small, +audio::-webkit-media-controls-wireless-playback-text-top.small { + top: 4pt; +} + +video::-webkit-media-controls-wireless-playback-text-bottom.small, +audio::-webkit-media-controls-wireless-playback-text-bottom.small { + display: none; +} + +video::-webkit-media-controls-wireless-playback-status.picture-in-picture, +audio::-webkit-media-controls-wireless-playback-status.picture-in-picture +{ + background-size: 304px auto; + background-position: 50% calc(.5 * (100% - 25px)); + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 304 150"><g fill="none" stroke="-apple-system-gray" stroke-width="3"><path d="m 172,106 -81,0 c -3.5,0 -6,-2.5 -6,-6 l 0,-89 c 0,-3.5 2.5,-6 6,-6 l 122,0 c 3.5,0 6,2.5 6,6 l 0,57" /><path d="m 233,119 -53,0 c -3,0 -3,-0 -3,-3 l 0,-40 c 0,-3 0,-3 3,-3 l 53,0 c 3,0 3,0 3,3 l 0,40 c 0,3 0,3 -3,3 z" /></g></svg>'); +} + +video::-webkit-media-controls-wireless-playback-text-top.picture-in-picture, +audio::-webkit-media-controls-wireless-playback-text-top.picture-in-picture { + top: initial; + bottom: 0; +} + +video::-webkit-media-controls-wireless-playback-text-bottom.picture-in-picture, +audio::-webkit-media-controls-wireless-playback-text-bottom.picture-in-picture { + display: none; +} + +video::-webkit-media-controls-wireless-playback-status.hidden, +audio::-webkit-media-controls-wireless-playback-status.hidden { + display: none; +} + +/* When PiP is active, we don't want the controls container showing. */ +video::-webkit-media-controls-panel-container.picture-in-picture { + opacity: 0; + pointer-events: none; +} + +/* Time display clones that we use in updateLayoutForDisplayedWidth(). */ +::-webkit-media-controls-current-time-display.clone, +::-webkit-media-controls-time-remaining-display.clone { + position: absolute; + display: inline; + top: 100%; + mix-blend-mode: normal; +} diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js b/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js new file mode 100644 index 000000000..9d6d28abb --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js @@ -0,0 +1,628 @@ +function createControls(root, video, host) +{ + return new ControllerIOS(root, video, host); +}; + +function ControllerIOS(root, video, host) +{ + this.doingSetup = true; + this._pageScaleFactor = 1; + + this.timelineContextName = "_webkit-media-controls-timeline-" + host.generateUUID(); + + Controller.call(this, root, video, host); + + this.setNeedsTimelineMetricsUpdate(); + + this._timelineIsHidden = false; + this._currentDisplayWidth = 0; + this.scheduleUpdateLayoutForDisplayedWidth(); + + host.controlsDependOnPageScaleFactor = true; + this.doingSetup = false; +}; + +/* Enums */ +ControllerIOS.StartPlaybackControls = 2; + +ControllerIOS.prototype = { + /* Constants */ + MinimumTimelineWidth: 150, + ButtonWidth: 42, + + get idiom() + { + return "ios"; + }, + + createBase: function() { + Controller.prototype.createBase.call(this); + + var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('div'); + startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button'); + startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback')); + startPlaybackButton.setAttribute('role', 'button'); + + var startPlaybackBackground = document.createElement('div'); + startPlaybackBackground.setAttribute('pseudo', '-webkit-media-controls-start-playback-background'); + startPlaybackBackground.classList.add('webkit-media-controls-start-playback-background'); + startPlaybackButton.appendChild(startPlaybackBackground); + + var startPlaybackGlyph = document.createElement('div'); + startPlaybackGlyph.setAttribute('pseudo', '-webkit-media-controls-start-playback-glyph'); + startPlaybackGlyph.classList.add('webkit-media-controls-start-playback-glyph'); + startPlaybackButton.appendChild(startPlaybackGlyph); + + this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart); + this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange); + this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd); + this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart); + this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove); + this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut); + + this.listenFor(document, 'visibilitychange', this.handleVisibilityChange); + }, + + shouldHaveStartPlaybackButton: function() { + var allowsInline = this.host.allowsInlineMediaPlayback; + + if (this.isPlaying || (this.hasPlayed && allowsInline)) + return false; + + if (this.isAudio() && allowsInline) + return false; + + if (this.doingSetup) + return true; + + if (this.isFullScreen()) + return false; + + if (!this.video.currentSrc && this.video.error) + return false; + + if (!this.video.controls && allowsInline) + return false; + + if (this.video.currentSrc && this.video.error) + return true; + + return true; + }, + + shouldHaveControls: function() { + if (this.shouldHaveStartPlaybackButton()) + return false; + + return Controller.prototype.shouldHaveControls.call(this); + }, + + shouldHaveAnyUI: function() { + return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless(); + }, + + createControls: function() { + Controller.prototype.createControls.call(this); + + var panelContainer = this.controls.panelContainer = document.createElement('div'); + panelContainer.setAttribute('pseudo', '-webkit-media-controls-panel-container'); + + var wirelessTargetPicker = this.controls.wirelessTargetPicker; + this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart); + this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd); + this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel); + + this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart); + this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd); + this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel); + + this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart); + this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd); + this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel); + this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart); + this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd); + this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel); + this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart); + this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd); + this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel); + this.listenFor(this.controls.pictureInPictureButton, 'touchstart', this.handlePictureInPictureTouchStart); + this.listenFor(this.controls.pictureInPictureButton, 'touchend', this.handlePictureInPictureTouchEnd); + this.listenFor(this.controls.pictureInPictureButton, 'touchcancel', this.handlePictureInPictureTouchCancel); + this.listenFor(this.controls.timeline, 'touchstart', this.handleTimelineTouchStart); + this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked); + + this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')'; + }, + + setControlsType: function(type) { + if (type === this.controlsType) + return; + Controller.prototype.setControlsType.call(this, type); + + if (type === ControllerIOS.StartPlaybackControls) + this.addStartPlaybackControls(); + else + this.removeStartPlaybackControls(); + }, + + addStartPlaybackControls: function() { + this.base.appendChild(this.controls.startPlaybackButton); + this.showShowControlsButton(false); + }, + + removeStartPlaybackControls: function() { + if (this.controls.startPlaybackButton.parentNode) + this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton); + }, + + reconnectControls: function() + { + Controller.prototype.reconnectControls.call(this); + + if (this.controlsType === ControllerIOS.StartPlaybackControls) + this.addStartPlaybackControls(); + }, + + configureInlineControls: function() { + this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText); + this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop); + this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom); + this.controls.panel.appendChild(this.controls.playButton); + this.controls.panel.appendChild(this.controls.statusDisplay); + this.controls.panel.appendChild(this.controls.timelineBox); + this.controls.panel.appendChild(this.controls.wirelessTargetPicker); + if (!this.isLive) { + this.controls.timelineBox.appendChild(this.controls.currentTime); + this.controls.timelineBox.appendChild(this.controls.timeline); + this.controls.timelineBox.appendChild(this.controls.remainingTime); + } + if (this.isAudio()) { + // Hide the scrubber on audio until the user starts playing. + this.controls.timelineBox.classList.add(this.ClassNames.hidden); + } else { + this.updatePictureInPictureButton(); + this.controls.panel.appendChild(this.controls.fullscreenButton); + } + }, + + configureFullScreenControls: function() { + // Explicitly do nothing to override base-class behavior. + }, + + controlsAreHidden: function() + { + // Controls are only ever actually hidden when they are removed from the tree + return !this.controls.panelContainer.parentElement; + }, + + addControls: function() { + this.base.appendChild(this.controls.inlinePlaybackPlaceholder); + this.base.appendChild(this.controls.panelContainer); + this.controls.panelContainer.appendChild(this.controls.panelBackground); + this.controls.panelContainer.appendChild(this.controls.panel); + this.setNeedsTimelineMetricsUpdate(); + }, + + updateControls: function() { + if (this.shouldHaveStartPlaybackButton()) + this.setControlsType(ControllerIOS.StartPlaybackControls); + else if (this.presentationMode() === "fullscreen") + this.setControlsType(Controller.FullScreenControls); + else + this.setControlsType(Controller.InlineControls); + + this.updateLayoutForDisplayedWidth(); + this.setNeedsTimelineMetricsUpdate(); + }, + + drawTimelineBackground: function() { + var width = this.timelineWidth * window.devicePixelRatio; + var height = this.timelineHeight * window.devicePixelRatio; + + if (!width || !height) + return; + + var played = this.video.currentTime / this.video.duration; + var buffered = 0; + var bufferedRanges = this.video.buffered; + if (bufferedRanges && bufferedRanges.length) + buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered); + + buffered /= this.video.duration; + buffered = Math.max(buffered, played); + + var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height); + + ctx.clearRect(0, 0, width, height); + + var midY = height / 2; + + // 1. Draw the buffered part and played parts, using + // solid rectangles that are clipped to the outside of + // the lozenge. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3); + ctx.closePath(); + ctx.clip(); + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, Math.round(width * played) + 2, height); + ctx.fillStyle = "rgba(0, 0, 0, 0.55)"; + ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height); + ctx.restore(); + + // 2. Draw the outline with a clip path that subtracts the + // middle of a lozenge. This produces a better result than + // stroking. + ctx.save(); + ctx.beginPath(); + this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3); + this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2); + ctx.closePath(); + ctx.clip("evenodd"); + ctx.fillStyle = "rgba(0, 0, 0, 0.55)"; + ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height); + ctx.restore(); + }, + + formatTime: function(time) { + if (isNaN(time)) + time = 0; + var absTime = Math.abs(time); + var intSeconds = Math.floor(absTime % 60).toFixed(0); + var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); + var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); + var sign = time < 0 ? '-' : String(); + + if (intHours > 0) + return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2); + + return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2); + }, + + handlePlayButtonTouchStart: function() { + this.controls.playButton.classList.add('active'); + }, + + handlePlayButtonTouchEnd: function(event) { + this.controls.playButton.classList.remove('active'); + + if (this.canPlay()) { + this.video.play(); + this.showControls(); + } else + this.video.pause(); + + return true; + }, + + handlePlayButtonTouchCancel: function(event) { + this.controls.playButton.classList.remove('active'); + return true; + }, + + handleBaseGestureStart: function(event) { + this.gestureStartTime = new Date(); + // If this gesture started with two fingers inside the video, then + // don't treat it as a potential zoom, unless we're still waiting + // to play. + if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls) + event.preventDefault(); + }, + + handleBaseGestureChange: function(event) { + if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls) + return; + + var scaleDetectionThreshold = 0.2; + if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold) + delete this.lastDoubleTouchTime; + + if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0) + event.preventDefault(); + + var currentGestureTime = new Date(); + var duration = (currentGestureTime - this.gestureStartTime) / 1000; + if (!duration) + return; + + var velocity = Math.abs(event.scale - 1) / duration; + + var pinchOutVelocityThreshold = 2; + var pinchOutGestureScaleThreshold = 1.25; + if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold) + return; + + delete this.gestureStartTime; + this.video.webkitEnterFullscreen(); + }, + + handleBaseGestureEnd: function(event) { + delete this.gestureStartTime; + }, + + handleWrapperTouchStart: function(event) { + if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder) + return; + + this.mostRecentNumberOfTargettedTouches = event.targetTouches.length; + + if (this.controlsAreHidden() || !this.controls.panel.classList.contains(this.ClassNames.show)) { + this.showControls(); + this.resetHideControlsTimer(); + } else if (!this.canPlay()) + this.hideControls(); + }, + + handlePanelTouchStart: function(event) { + this.video.style.webkitUserSelect = 'none'; + }, + + handlePanelTouchEnd: function(event) { + this.video.style.removeProperty('-webkit-user-select'); + }, + + handlePanelTouchCancel: function(event) { + this.video.style.removeProperty('-webkit-user-select'); + }, + + handleVisibilityChange: function(event) { + this.updateShouldListenForPlaybackTargetAvailabilityEvent(); + }, + + handlePanelTransitionEnd: function(event) + { + var opacity = window.getComputedStyle(this.controls.panel).opacity; + if (!parseInt(opacity) && !this.controlsAlwaysVisible()) { + this.base.removeChild(this.controls.inlinePlaybackPlaceholder); + this.base.removeChild(this.controls.panelContainer); + } + }, + + handleFullscreenButtonClicked: function(event) { + if ('webkitSetPresentationMode' in this.video) { + if (this.presentationMode() === 'fullscreen') + this.video.webkitSetPresentationMode('inline'); + else + this.video.webkitSetPresentationMode('fullscreen'); + + return; + } + + if (this.isFullScreen()) + this.video.webkitExitFullscreen(); + else + this.video.webkitEnterFullscreen(); + }, + + handleFullscreenTouchStart: function() { + this.controls.fullscreenButton.classList.add('active'); + }, + + handleFullscreenTouchEnd: function(event) { + this.controls.fullscreenButton.classList.remove('active'); + + this.handleFullscreenButtonClicked(); + + return true; + }, + + handleFullscreenTouchCancel: function(event) { + this.controls.fullscreenButton.classList.remove('active'); + return true; + }, + + handlePictureInPictureTouchStart: function() { + this.controls.pictureInPictureButton.classList.add('active'); + }, + + handlePictureInPictureTouchEnd: function(event) { + this.controls.pictureInPictureButton.classList.remove('active'); + + this.handlePictureInPictureButtonClicked(); + + return true; + }, + + handlePictureInPictureTouchCancel: function(event) { + this.controls.pictureInPictureButton.classList.remove('active'); + return true; + }, + + handleStartPlaybackButtonTouchStart: function(event) { + this.controls.startPlaybackButton.classList.add('active'); + this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-glyph').classList.add('active'); + }, + + handleStartPlaybackButtonTouchEnd: function(event) { + this.controls.startPlaybackButton.classList.remove('active'); + this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-glyph').classList.remove('active'); + + if (this.video.error) + return true; + + this.video.play(); + this.canToggleShowControlsButton = true; + this.updateControls(); + + return true; + }, + + handleStartPlaybackButtonTouchCancel: function(event) { + this.controls.startPlaybackButton.classList.remove('active'); + return true; + }, + + handleTimelineTouchStart: function(event) { + this.scrubbing = true; + this.listenFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd); + this.listenFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd); + }, + + handleTimelineTouchEnd: function(event) { + this.stopListeningFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd); + this.stopListeningFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd); + this.scrubbing = false; + }, + + handleWirelessPickerButtonTouchStart: function() { + if (!this.video.error) + this.controls.wirelessTargetPicker.classList.add('active'); + }, + + handleWirelessPickerButtonTouchEnd: function(event) { + this.controls.wirelessTargetPicker.classList.remove('active'); + return this.handleWirelessPickerButtonClicked(); + }, + + handleWirelessPickerButtonTouchCancel: function(event) { + this.controls.wirelessTargetPicker.classList.remove('active'); + return true; + }, + + updateShouldListenForPlaybackTargetAvailabilityEvent: function() { + if (this.controlsType === ControllerIOS.StartPlaybackControls) { + this.setShouldListenForPlaybackTargetAvailabilityEvent(false); + return; + } + + Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this); + }, + + updateWirelessTargetPickerButton: function() { + }, + + updateStatusDisplay: function(event) + { + this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null); + this.controls.startPlaybackButton.querySelector(".webkit-media-controls-start-playback-glyph").classList.toggle(this.ClassNames.failed, this.video.error !== null); + Controller.prototype.updateStatusDisplay.call(this, event); + }, + + setPlaying: function(isPlaying) + { + Controller.prototype.setPlaying.call(this, isPlaying); + + this.updateControls(); + + if (isPlaying && this.isAudio()) + this.controls.timelineBox.classList.remove(this.ClassNames.hidden); + + if (isPlaying) + this.hasPlayed = true; + else + this.showControls(); + }, + + showControls: function() + { + this.updateShouldListenForPlaybackTargetAvailabilityEvent(); + if (!this.video.controls) + return; + + this.updateForShowingControls(); + if (this.shouldHaveControls() && !this.controls.panelContainer.parentElement) { + this.base.appendChild(this.controls.inlinePlaybackPlaceholder); + this.base.appendChild(this.controls.panelContainer); + this.showShowControlsButton(false); + } + }, + + setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) + { + if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error)) + return; + + Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen); + }, + + shouldReturnVideoLayerToInline: function() + { + return this.presentationMode() === 'inline'; + }, + + updatePictureInPicturePlaceholder: function(event) + { + var presentationMode = this.presentationMode(); + + switch (presentationMode) { + case 'inline': + this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture); + break; + case 'picture-in-picture': + this.controls.panelContainer.classList.add(this.ClassNames.pictureInPicture); + break; + default: + this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture); + break; + } + + Controller.prototype.updatePictureInPicturePlaceholder.call(this, event); + }, + + // Due to the bad way we are faking inheritance here, in particular the extends method + // on Controller.prototype, we don't copy getters and setters from the prototype. This + // means we have to implement them again, here in the subclass. + // FIXME: Use ES6 classes! + + get scrubbing() + { + return Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").get.call(this); + }, + + set scrubbing(flag) + { + Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").set.call(this, flag); + }, + + get pageScaleFactor() + { + return this._pageScaleFactor; + }, + + set pageScaleFactor(newScaleFactor) + { + if (!newScaleFactor || this._pageScaleFactor === newScaleFactor) + return; + + this._pageScaleFactor = newScaleFactor; + + var scaleValue = 1 / newScaleFactor; + var scaleTransform = "scale(" + scaleValue + ")"; + + function applyScaleFactorToElement(element) { + if (scaleValue > 1) { + element.style.zoom = scaleValue; + element.style.webkitTransform = "scale(1)"; + } else { + element.style.zoom = 1; + element.style.webkitTransform = scaleTransform; + } + } + + if (this.controls.startPlaybackButton) + applyScaleFactorToElement(this.controls.startPlaybackButton); + if (this.controls.panel) { + applyScaleFactorToElement(this.controls.panel); + if (scaleValue > 1) { + this.controls.panel.style.width = "100%"; + this.controls.timelineBox.style.webkitTextSizeAdjust = (100 * scaleValue) + "%"; + } else { + var bottomAligment = -2 * scaleValue; + this.controls.panel.style.bottom = bottomAligment + "px"; + this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px"; + this.controls.panel.style.width = Math.round(newScaleFactor * 100) + "%"; + this.controls.timelineBox.style.webkitTextSizeAdjust = "auto"; + } + this.controls.panelBackground.style.height = (50 * scaleValue) + "px"; + + this.setNeedsTimelineMetricsUpdate(); + this.updateProgress(); + this.scheduleUpdateLayoutForDisplayedWidth(); + } + }, + +}; + +Object.create(Controller.prototype).extend(ControllerIOS.prototype); +Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS }); |