[Modern Media Controls] Improve media documents across macOS, iPhone and iPad
[WebKit-https.git] / Source / WebCore / Modules / modern-media-controls / media / media-controller.js
1 /*
2  * Copyright (C) 2016 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 const AudioTightPaddingMaxWidth = 400;
27
28 class MediaController
29 {
30
31     constructor(shadowRoot, media, host)
32     {
33         this.shadowRoot = shadowRoot;
34         this.media = media;
35         this.host = host;
36
37         this.container = shadowRoot.appendChild(document.createElement("div"));
38         this.container.className = "media-controls-container";
39
40         if (host) {
41             host.controlsDependOnPageScaleFactor = this.layoutTraits & LayoutTraits.iOS;
42             this.container.appendChild(host.textTrackContainer);
43             if (host.isInMediaDocument)
44                 this.mediaDocumentController = new MediaDocumentController(this);
45         }
46
47         this._updateControlsIfNeeded();
48         scheduler.flushScheduledLayoutCallbacks();
49
50         shadowRoot.addEventListener("resize", this);
51
52         media.videoTracks.addEventListener("addtrack", this);
53         media.videoTracks.addEventListener("removetrack", this);
54
55         if (media.webkitSupportsPresentationMode)
56             media.addEventListener("webkitpresentationmodechanged", this);
57         else
58             media.addEventListener("webkitfullscreenchange", this);
59     }
60
61     // Public
62
63     get isAudio()
64     {
65         if (this.media instanceof HTMLAudioElement)
66             return true;
67
68         if (this.media.readyState < HTMLMediaElement.HAVE_METADATA)
69             return false;
70
71         const isLiveBroadcast = this.media.duration === Number.POSITIVE_INFINITY;
72         const hasVideoTracks = this.media.videoWidth != 0;
73         return !isLiveBroadcast && !hasVideoTracks;
74     }
75
76     get layoutTraits()
77     {
78         let traits = window.navigator.platform === "MacIntel" ? LayoutTraits.macOS : LayoutTraits.iOS;
79         if (this.media.webkitSupportsPresentationMode) {
80             if (this.media.webkitPresentationMode === "fullscreen")
81                 return traits | LayoutTraits.Fullscreen;
82         } else if (this.media.webkitDisplayingFullscreen)
83             return traits | LayoutTraits.Fullscreen;
84
85         if (traits & LayoutTraits.macOS)
86             return traits | LayoutTraits.Compact;
87
88         if (this.isAudio && this._controlsWidth() <= AudioTightPaddingMaxWidth)
89             return traits | LayoutTraits.TightPadding;
90
91         return traits;
92     }
93
94     togglePlayback()
95     {
96         if (this.media.paused)
97             this.media.play();
98         else
99             this.media.pause();
100     }
101
102     // Protected
103
104     set pageScaleFactor(pageScaleFactor)
105     {
106         this.controls.scaleFactor = pageScaleFactor;
107         this._updateControlsSize();
108     }
109
110     set usesLTRUserInterfaceLayoutDirection(flag)
111     {
112         this.controls.usesLTRUserInterfaceLayoutDirection = flag;
113     }
114
115     controlsBarFadedStateDidChange()
116     {
117         this._updateTextTracksClassList();
118     }
119
120     macOSControlsBackgroundWasClicked()
121     {
122         // Toggle playback when clicking on the video but not on any controls on macOS.
123         if (this.media.controls)
124             this.togglePlayback();
125     }
126
127     handleEvent(event)
128     {
129         if (event instanceof TrackEvent && event.currentTarget === this.media.videoTracks)
130             this._updateControlsIfNeeded();
131         else if (event.type === "resize" && event.currentTarget === this.shadowRoot) {
132             this._updateControlsIfNeeded();
133             // We must immediately perform layouts so that we don't lag behind the media layout size.
134             scheduler.flushScheduledLayoutCallbacks();
135         } else if (event.currentTarget === this.media) {
136             this._updateControlsIfNeeded();
137             if (event.type === "webkitpresentationmodechanged")
138                 this._returnMediaLayerToInlineIfNeeded();
139         }
140     }
141
142     // Private
143
144     _updateControlsIfNeeded()
145     {
146         const layoutTraits = this.layoutTraits;
147         const previousControls = this.controls;
148         const ControlsClass = this._controlsClassForLayoutTraits(layoutTraits);
149         if (previousControls && previousControls.constructor === ControlsClass) {
150             this.controls.layoutTraits = layoutTraits;
151             this._updateTextTracksClassList();
152             this._updateControlsSize();
153             return;
154         }
155
156         // Before we reset the .controls property, we need to destroy the previous
157         // supporting objects so we don't leak.
158         if (this._supportingObjects) {
159             for (let supportingObject of this._supportingObjects)
160                 supportingObject.destroy();
161         }
162
163         this.controls = new ControlsClass;
164         this.controls.delegate = this;
165
166         if (this.shadowRoot.host && this.shadowRoot.host.dataset.autoHideDelay)
167             this.controls.controlsBar.autoHideDelay = this.shadowRoot.host.dataset.autoHideDelay;
168
169         if (previousControls) {
170             this.controls.fadeIn();
171             this.container.replaceChild(this.controls.element, previousControls.element);
172             this.controls.usesLTRUserInterfaceLayoutDirection = previousControls.usesLTRUserInterfaceLayoutDirection;
173         } else
174             this.container.appendChild(this.controls.element);
175
176         this.controls.layoutTraits = layoutTraits;
177         this._updateTextTracksClassList();
178         this._updateControlsSize();
179
180         this._supportingObjects = [AirplaySupport, ControlsVisibilitySupport, FullscreenSupport, MuteSupport, PiPSupport, PlacardSupport, PlaybackSupport, ScrubbingSupport, SeekBackwardSupport, SeekForwardSupport, SkipBackSupport, StartSupport, StatusSupport, TimeLabelsSupport, TracksSupport, VolumeSupport, VolumeDownSupport, VolumeUpSupport].map(SupportClass => {
181             return new SupportClass(this);
182         }, this);
183     }
184
185     _updateControlsSize()
186     {
187         this.controls.width = this._controlsWidth();
188         this.controls.height = Math.round(this.container.getBoundingClientRect().height * this.controls.scaleFactor);
189         this.controls.shouldCenterControlsVertically = this.isAudio;
190     }
191
192     _controlsWidth()
193     {
194         return Math.round(this.container.getBoundingClientRect().width * (this.controls ? this.controls.scaleFactor : 1));
195     }
196
197     _returnMediaLayerToInlineIfNeeded()
198     {
199         if (this.host)
200             window.requestAnimationFrame(() => this.host.setPreparedToReturnVideoLayerToInline(this.media.webkitPresentationMode !== PiPMode));
201     }
202
203     _controlsClassForLayoutTraits(layoutTraits)
204     {
205         if (layoutTraits & LayoutTraits.iOS)
206             return IOSInlineMediaControls;
207         if (layoutTraits & LayoutTraits.Fullscreen)
208             return MacOSFullscreenMediaControls;
209         return MacOSInlineMediaControls;
210     }
211
212     _updateTextTracksClassList()
213     {
214         if (!this.host)
215             return;
216
217         const layoutTraits = this.layoutTraits;
218         if (layoutTraits & LayoutTraits.Fullscreen)
219             return;
220
221         this.host.textTrackContainer.classList.toggle("visible-controls-bar", !this.controls.controlsBar.faded);
222         this.host.textTrackContainer.classList.toggle("compact-controls-bar", !!(layoutTraits & LayoutTraits.Compact));
223     }
224
225 }