e436d4c5e03d03e8bd84d519ba6977975c9346e0
[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         }
44
45         this._updateControlsIfNeeded();
46         scheduler.flushScheduledLayoutCallbacks();
47
48         shadowRoot.addEventListener("resize", this);
49
50         media.videoTracks.addEventListener("addtrack", this);
51         media.videoTracks.addEventListener("removetrack", this);
52
53         if (media.webkitSupportsPresentationMode)
54             media.addEventListener("webkitpresentationmodechanged", this);
55         else
56             media.addEventListener("webkitfullscreenchange", this);
57     }
58
59     // Public
60
61     get isAudio()
62     {
63         return this.media instanceof HTMLAudioElement || (this.media.readyState >= HTMLMediaElement.HAVE_METADATA && this.media.videoWidth === 0);
64     }
65
66     get layoutTraits()
67     {
68         let traits = window.navigator.platform === "MacIntel" ? LayoutTraits.macOS : LayoutTraits.iOS;
69         if (this.media.webkitSupportsPresentationMode) {
70             if (this.media.webkitPresentationMode === "fullscreen")
71                 return traits | LayoutTraits.Fullscreen;
72         } else if (this.media.webkitDisplayingFullscreen)
73             return traits | LayoutTraits.Fullscreen;
74
75         if (traits & LayoutTraits.macOS)
76             return traits | LayoutTraits.Compact;
77
78         if (this.isAudio && this._controlsWidth() <= AudioTightPaddingMaxWidth)
79             return traits | LayoutTraits.TightPadding;
80
81         return traits;
82     }
83
84     togglePlayback()
85     {
86         if (this.media.paused)
87             this.media.play();
88         else
89             this.media.pause();
90     }
91
92     // Protected
93
94     set pageScaleFactor(pageScaleFactor)
95     {
96         this.controls.scaleFactor = pageScaleFactor;
97         this._updateControlsSize();
98     }
99
100     set usesLTRUserInterfaceLayoutDirection(flag)
101     {
102         this.controls.usesLTRUserInterfaceLayoutDirection = flag;
103     }
104
105     controlsBarFadedStateDidChange()
106     {
107         this._updateTextTracksClassList();
108     }
109
110     macOSControlsBackgroundWasClicked()
111     {
112         // Toggle playback when clicking on the video but not on any controls on macOS.
113         if (this.media.controls)
114             this.togglePlayback();
115     }
116
117     handleEvent(event)
118     {
119         if (event instanceof TrackEvent && event.currentTarget === this.media.videoTracks)
120             this._updateControlsIfNeeded();
121         else if (event.type === "resize" && event.currentTarget === this.shadowRoot) {
122             this._updateControlsIfNeeded();
123             // We must immediately perform layouts so that we don't lag behind the media layout size.
124             scheduler.flushScheduledLayoutCallbacks();
125         } else if (event.currentTarget === this.media) {
126             this._updateControlsIfNeeded();
127             if (event.type === "webkitpresentationmodechanged")
128                 this._returnMediaLayerToInlineIfNeeded();
129         }
130     }
131
132     // Private
133
134     _updateControlsIfNeeded()
135     {
136         const layoutTraits = this.layoutTraits;
137         const previousControls = this.controls;
138         const ControlsClass = this._controlsClassForLayoutTraits(layoutTraits);
139         if (previousControls && previousControls.constructor === ControlsClass) {
140             this.controls.layoutTraits = layoutTraits;
141             this._updateTextTracksClassList();
142             this._updateControlsSize();
143             return;
144         }
145
146         // Before we reset the .controls property, we need to destroy the previous
147         // supporting objects so we don't leak.
148         if (this._supportingObjects) {
149             for (let supportingObject of this._supportingObjects)
150                 supportingObject.destroy();
151         }
152
153         this.controls = new ControlsClass;
154         this.controls.delegate = this;
155
156         if (this.shadowRoot.host && this.shadowRoot.host.dataset.autoHideDelay)
157             this.controls.controlsBar.autoHideDelay = this.shadowRoot.host.dataset.autoHideDelay;
158
159         if (previousControls) {
160             this.controls.fadeIn();
161             this.container.replaceChild(this.controls.element, previousControls.element);
162             this.controls.usesLTRUserInterfaceLayoutDirection = previousControls.usesLTRUserInterfaceLayoutDirection;
163         } else
164             this.container.appendChild(this.controls.element);
165
166         this.controls.layoutTraits = layoutTraits;
167         this._updateTextTracksClassList();
168         this._updateControlsSize();
169
170         this._supportingObjects = [AirplaySupport, ControlsVisibilitySupport, FullscreenSupport, MuteSupport, PiPSupport, PlacardSupport, PlaybackSupport, ScrubbingSupport, SeekBackwardSupport, SeekForwardSupport, SkipBackSupport, StartSupport, StatusSupport, TimeLabelsSupport, TracksSupport, VolumeSupport, VolumeDownSupport, VolumeUpSupport].map(SupportClass => {
171             return new SupportClass(this);
172         }, this);
173     }
174
175     _updateControlsSize()
176     {
177         this.controls.width = this._controlsWidth();
178         this.controls.height = Math.round(this.container.getBoundingClientRect().height * this.controls.scaleFactor);
179         this.controls.shouldCenterControlsVertically = this.isAudio;
180     }
181
182     _controlsWidth()
183     {
184         return Math.round(this.container.getBoundingClientRect().width * (this.controls ? this.controls.scaleFactor : 1));
185     }
186
187     _returnMediaLayerToInlineIfNeeded()
188     {
189         if (this.host)
190             window.requestAnimationFrame(() => this.host.setPreparedToReturnVideoLayerToInline(this.media.webkitPresentationMode !== PiPMode));
191     }
192
193     _controlsClassForLayoutTraits(layoutTraits)
194     {
195         if (layoutTraits & LayoutTraits.iOS)
196             return IOSInlineMediaControls;
197         if (layoutTraits & LayoutTraits.Fullscreen)
198             return MacOSFullscreenMediaControls;
199         return MacOSInlineMediaControls;
200     }
201
202     _updateTextTracksClassList()
203     {
204         if (!this.host)
205             return;
206
207         const layoutTraits = this.layoutTraits;
208         if (layoutTraits & LayoutTraits.Fullscreen)
209             return;
210
211         this.host.textTrackContainer.classList.toggle("visible-controls-bar", !this.controls.controlsBar.faded);
212         this.host.textTrackContainer.classList.toggle("compact-controls-bar", !!(layoutTraits & LayoutTraits.Compact));
213     }
214
215 }