Media elements allowed to play without a user gesture, but requiring fullscreen playb...
[WebKit-https.git] / Source / WebCore / html / MediaElementSession.cpp
1 /*
2  * Copyright (C) 2014 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. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27
28 #if ENABLE(VIDEO)
29
30 #include "MediaElementSession.h"
31
32 #include "Chrome.h"
33 #include "ChromeClient.h"
34 #include "Document.h"
35 #include "Frame.h"
36 #include "FrameView.h"
37 #include "HTMLMediaElement.h"
38 #include "HTMLMediaElementEnums.h"
39 #include "HTMLNames.h"
40 #include "HTMLVideoElement.h"
41 #include "HitTestResult.h"
42 #include "Logging.h"
43 #include "MainFrame.h"
44 #include "Page.h"
45 #include "PlatformMediaSessionManager.h"
46 #include "RenderView.h"
47 #include "ScriptController.h"
48 #include "SourceBuffer.h"
49
50 #if PLATFORM(IOS)
51 #include "AudioSession.h"
52 #include "RuntimeApplicationChecks.h"
53 #endif
54
55 namespace WebCore {
56
57 static const int elementMainContentMinimumWidth = 400;
58 static const int elementMainContentMinimumHeight = 300;
59 static const double elementMainContentCheckInterval = .250;
60
61 static bool isMainContent(const HTMLMediaElement&);
62
63 #if !LOG_DISABLED
64 static String restrictionName(MediaElementSession::BehaviorRestrictions restriction)
65 {
66     StringBuilder restrictionBuilder;
67 #define CASE(restrictionType) \
68     if (restriction & MediaElementSession::restrictionType) { \
69         if (!restrictionBuilder.isEmpty()) \
70             restrictionBuilder.append(", "); \
71         restrictionBuilder.append(#restrictionType); \
72     } \
73
74     CASE(NoRestrictions);
75     CASE(RequireUserGestureForLoad);
76     CASE(RequireUserGestureForVideoRateChange);
77     CASE(RequireUserGestureForAudioRateChange);
78     CASE(RequireUserGestureForFullscreen);
79     CASE(RequirePageConsentToLoadMedia);
80     CASE(RequirePageConsentToResumeMedia);
81 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
82     CASE(RequireUserGestureToShowPlaybackTargetPicker);
83     CASE(WirelessVideoPlaybackDisabled);
84 #endif
85     CASE(RequireUserGestureForAudioRateChange);
86     CASE(InvisibleAutoplayNotPermitted);
87     CASE(OverrideUserGestureRequirementForMainContent);
88
89     return restrictionBuilder.toString();
90 }
91 #endif
92
93 static bool pageExplicitlyAllowsElementToAutoplayInline(const HTMLMediaElement& element)
94 {
95     Document& document = element.document();
96     Page* page = document.page();
97     return document.isMediaDocument() && !document.ownerElement() && page && page->allowsMediaDocumentInlinePlayback();
98 }
99
100 MediaElementSession::MediaElementSession(HTMLMediaElement& element)
101     : PlatformMediaSession(element)
102     , m_element(element)
103     , m_restrictions(NoRestrictions)
104 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
105     , m_targetAvailabilityChangedTimer(*this, &MediaElementSession::targetAvailabilityChangedTimerFired)
106 #endif
107     , m_mainContentCheckTimer(*this, &MediaElementSession::mainContentCheckTimerFired)
108 {
109 }
110
111 void MediaElementSession::registerWithDocument(Document& document)
112 {
113 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
114     document.addPlaybackTargetPickerClient(*this);
115 #else
116     UNUSED_PARAM(document);
117 #endif
118 }
119
120 void MediaElementSession::unregisterWithDocument(Document& document)
121 {
122 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
123     document.removePlaybackTargetPickerClient(*this);
124 #else
125     UNUSED_PARAM(document);
126 #endif
127 }
128
129 void MediaElementSession::addBehaviorRestriction(BehaviorRestrictions restriction)
130 {
131     LOG(Media, "MediaElementSession::addBehaviorRestriction - adding %s", restrictionName(restriction).utf8().data());
132     m_restrictions |= restriction;
133
134     if (restriction & OverrideUserGestureRequirementForMainContent)
135         m_mainContentCheckTimer.startRepeating(elementMainContentCheckInterval);
136 }
137
138 void MediaElementSession::removeBehaviorRestriction(BehaviorRestrictions restriction)
139 {
140     LOG(Media, "MediaElementSession::removeBehaviorRestriction - removing %s", restrictionName(restriction).utf8().data());
141     m_restrictions &= ~restriction;
142 }
143
144 bool MediaElementSession::playbackPermitted(const HTMLMediaElement& element) const
145 {
146     if (pageExplicitlyAllowsElementToAutoplayInline(element))
147         return true;
148
149     if (requiresFullscreenForVideoPlayback(element) && !ScriptController::processingUserGestureForMedia()) {
150         LOG(Media, "MediaElementSession::playbackPermitted - returning FALSE");
151         return false;
152     }
153
154     if (m_restrictions & OverrideUserGestureRequirementForMainContent && updateIsMainContent())
155         return true;
156
157     if (m_restrictions & RequireUserGestureForVideoRateChange && element.isVideo() && !ScriptController::processingUserGestureForMedia()) {
158         LOG(Media, "MediaElementSession::playbackPermitted - returning FALSE");
159         return false;
160     }
161
162     if (m_restrictions & RequireUserGestureForAudioRateChange && (!element.isVideo() || element.hasAudio()) && !ScriptController::processingUserGestureForMedia()) {
163         LOG(Media, "MediaElementSession::playbackPermitted - returning FALSE");
164         return false;
165     }
166
167     return true;
168 }
169
170 bool MediaElementSession::dataLoadingPermitted(const HTMLMediaElement&) const
171 {
172     if (m_restrictions & OverrideUserGestureRequirementForMainContent && updateIsMainContent())
173         return true;
174
175     if (m_restrictions & RequireUserGestureForLoad && !ScriptController::processingUserGestureForMedia()) {
176         LOG(Media, "MediaElementSession::dataLoadingPermitted - returning FALSE");
177         return false;
178     }
179
180     return true;
181 }
182
183 bool MediaElementSession::fullscreenPermitted(const HTMLMediaElement&) const
184 {
185     if (m_restrictions & RequireUserGestureForFullscreen && !ScriptController::processingUserGestureForMedia()) {
186         LOG(Media, "MediaElementSession::fullscreenPermitted - returning FALSE");
187         return false;
188     }
189
190     return true;
191 }
192
193 bool MediaElementSession::pageAllowsDataLoading(const HTMLMediaElement& element) const
194 {
195     Page* page = element.document().page();
196     if (m_restrictions & RequirePageConsentToLoadMedia && page && !page->canStartMedia()) {
197         LOG(Media, "MediaElementSession::pageAllowsDataLoading - returning FALSE");
198         return false;
199     }
200
201     return true;
202 }
203
204 bool MediaElementSession::pageAllowsPlaybackAfterResuming(const HTMLMediaElement& element) const
205 {
206     Page* page = element.document().page();
207     if (m_restrictions & RequirePageConsentToResumeMedia && page && !page->canStartMedia()) {
208         LOG(Media, "MediaElementSession::pageAllowsPlaybackAfterResuming - returning FALSE");
209         return false;
210     }
211
212     return true;
213 }
214
215 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
216 void MediaElementSession::showPlaybackTargetPicker(const HTMLMediaElement& element)
217 {
218     LOG(Media, "MediaElementSession::showPlaybackTargetPicker");
219
220     if (m_restrictions & RequireUserGestureToShowPlaybackTargetPicker && !ScriptController::processingUserGestureForMedia()) {
221         LOG(Media, "MediaElementSession::showPlaybackTargetPicker - returning early because of permissions");
222         return;
223     }
224
225     if (!element.document().page()) {
226         LOG(Media, "MediaElementSession::showingPlaybackTargetPickerPermitted - returning early because page is NULL");
227         return;
228     }
229
230 #if !PLATFORM(IOS)
231     if (element.readyState() < HTMLMediaElementEnums::HAVE_METADATA) {
232         LOG(Media, "MediaElementSession::showPlaybackTargetPicker - returning early because element is not playable");
233         return;
234     }
235 #endif
236
237     String customMenuItemTitle = element.playbackTargetPickerCustomActionName();
238     element.document().showPlaybackTargetPicker(*this, is<HTMLVideoElement>(element), customMenuItemTitle);
239 }
240
241 bool MediaElementSession::hasWirelessPlaybackTargets(const HTMLMediaElement&) const
242 {
243 #if PLATFORM(IOS)
244     // FIXME: consolidate Mac and iOS implementations
245     m_hasPlaybackTargets = PlatformMediaSessionManager::sharedManager().hasWirelessTargetsAvailable();
246 #endif
247
248     LOG(Media, "MediaElementSession::hasWirelessPlaybackTargets - returning %s", m_hasPlaybackTargets ? "TRUE" : "FALSE");
249
250     return m_hasPlaybackTargets;
251 }
252
253 bool MediaElementSession::wirelessVideoPlaybackDisabled(const HTMLMediaElement& element) const
254 {
255     Settings* settings = element.document().settings();
256     if (!settings || !settings->allowsAirPlayForMediaPlayback()) {
257         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning TRUE because of settings");
258         return true;
259     }
260
261     if (element.fastHasAttribute(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)) {
262         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning TRUE because of attribute");
263         return true;
264     }
265
266 #if PLATFORM(IOS)
267     String legacyAirplayAttributeValue = element.fastGetAttribute(HTMLNames::webkitairplayAttr);
268     if (equalLettersIgnoringASCIICase(legacyAirplayAttributeValue, "deny")) {
269         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning TRUE because of legacy attribute");
270         return true;
271     }
272     if (equalLettersIgnoringASCIICase(legacyAirplayAttributeValue, "allow")) {
273         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning FALSE because of legacy attribute");
274         return false;
275     }
276 #endif
277
278     MediaPlayer* player = element.player();
279     if (!player)
280         return true;
281
282     bool disabled = player->wirelessVideoPlaybackDisabled();
283     LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning %s because media engine says so", disabled ? "TRUE" : "FALSE");
284     
285     return disabled;
286 }
287
288 void MediaElementSession::setWirelessVideoPlaybackDisabled(const HTMLMediaElement& element, bool disabled)
289 {
290     if (disabled)
291         addBehaviorRestriction(WirelessVideoPlaybackDisabled);
292     else
293         removeBehaviorRestriction(WirelessVideoPlaybackDisabled);
294
295     MediaPlayer* player = element.player();
296     if (!player)
297         return;
298
299     LOG(Media, "MediaElementSession::setWirelessVideoPlaybackDisabled - disabled %s", disabled ? "TRUE" : "FALSE");
300     player->setWirelessVideoPlaybackDisabled(disabled);
301 }
302
303 void MediaElementSession::setHasPlaybackTargetAvailabilityListeners(const HTMLMediaElement& element, bool hasListeners)
304 {
305     LOG(Media, "MediaElementSession::setHasPlaybackTargetAvailabilityListeners - hasListeners %s", hasListeners ? "TRUE" : "FALSE");
306
307 #if PLATFORM(IOS)
308     UNUSED_PARAM(element);
309     m_hasPlaybackTargetAvailabilityListeners = hasListeners;
310     PlatformMediaSessionManager::sharedManager().configureWireLessTargetMonitoring();
311 #else
312     UNUSED_PARAM(hasListeners);
313     element.document().playbackTargetPickerClientStateDidChange(*this, element.mediaState());
314 #endif
315 }
316
317 void MediaElementSession::setPlaybackTarget(Ref<MediaPlaybackTarget>&& device)
318 {
319     m_playbackTarget = WTFMove(device);
320     client().setWirelessPlaybackTarget(*m_playbackTarget.copyRef());
321 }
322
323 void MediaElementSession::targetAvailabilityChangedTimerFired()
324 {
325     client().wirelessRoutesAvailableDidChange();
326 }
327
328 void MediaElementSession::externalOutputDeviceAvailableDidChange(bool hasTargets)
329 {
330     if (m_hasPlaybackTargets == hasTargets)
331         return;
332
333     LOG(Media, "MediaElementSession::externalOutputDeviceAvailableDidChange(%p) - hasTargets %s", this, hasTargets ? "TRUE" : "FALSE");
334
335     m_hasPlaybackTargets = hasTargets;
336     m_targetAvailabilityChangedTimer.startOneShot(0);
337 }
338
339 bool MediaElementSession::canPlayToWirelessPlaybackTarget() const
340 {
341     if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute())
342         return false;
343
344     return client().canPlayToWirelessPlaybackTarget();
345 }
346
347 bool MediaElementSession::isPlayingToWirelessPlaybackTarget() const
348 {
349     if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute())
350         return false;
351
352     return client().isPlayingToWirelessPlaybackTarget();
353 }
354
355 void MediaElementSession::setShouldPlayToPlaybackTarget(bool shouldPlay)
356 {
357     LOG(Media, "MediaElementSession::setShouldPlayToPlaybackTarget - shouldPlay %s", shouldPlay ? "TRUE" : "FALSE");
358     m_shouldPlayToPlaybackTarget = shouldPlay;
359     client().setShouldPlayToPlaybackTarget(shouldPlay);
360 }
361
362 void MediaElementSession::customPlaybackActionSelected()
363 {
364     client().customPlaybackActionSelected();
365 }
366
367 void MediaElementSession::mediaStateDidChange(const HTMLMediaElement& element, MediaProducer::MediaStateFlags state)
368 {
369     element.document().playbackTargetPickerClientStateDidChange(*this, state);
370 }
371 #endif
372
373 MediaPlayer::Preload MediaElementSession::effectivePreloadForElement(const HTMLMediaElement& element) const
374 {
375     MediaPlayer::Preload preload = element.preloadValue();
376
377     if (pageExplicitlyAllowsElementToAutoplayInline(element))
378         return preload;
379
380     if (m_restrictions & MetadataPreloadingNotPermitted)
381         return MediaPlayer::None;
382
383     if (m_restrictions & AutoPreloadingNotPermitted) {
384         if (preload > MediaPlayer::MetaData)
385             return MediaPlayer::MetaData;
386     }
387
388     return preload;
389 }
390
391 bool MediaElementSession::requiresFullscreenForVideoPlayback(const HTMLMediaElement& element) const
392 {
393     if (pageExplicitlyAllowsElementToAutoplayInline(element))
394         return false;
395
396     Settings* settings = element.document().settings();
397     if (!settings || !settings->allowsInlineMediaPlayback())
398         return true;
399
400     return settings->inlineMediaPlaybackRequiresPlaysInlineAttribute() && !element.fastHasAttribute(HTMLNames::webkit_playsinlineAttr);
401 }
402
403 bool MediaElementSession::allowsAutomaticMediaDataLoading(const HTMLMediaElement& element) const
404 {
405     if (pageExplicitlyAllowsElementToAutoplayInline(element))
406         return true;
407
408     Settings* settings = element.document().settings();
409     if (settings && settings->mediaDataLoadsAutomatically())
410         return true;
411
412     return false;
413 }
414
415 void MediaElementSession::mediaEngineUpdated(const HTMLMediaElement& element)
416 {
417     LOG(Media, "MediaElementSession::mediaEngineUpdated");
418
419 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
420     if (m_restrictions & WirelessVideoPlaybackDisabled)
421         setWirelessVideoPlaybackDisabled(element, true);
422     if (m_playbackTarget)
423         client().setWirelessPlaybackTarget(*m_playbackTarget.copyRef());
424     if (m_shouldPlayToPlaybackTarget)
425         client().setShouldPlayToPlaybackTarget(true);
426 #else
427     UNUSED_PARAM(element);
428 #endif
429     
430 }
431
432 bool MediaElementSession::allowsPictureInPicture(const HTMLMediaElement& element) const
433 {
434     Settings* settings = element.document().settings();
435     return settings && settings->allowsPictureInPictureMediaPlayback() && !element.webkitCurrentPlaybackTargetIsWireless();
436 }
437
438 #if PLATFORM(IOS)
439 bool MediaElementSession::requiresPlaybackTargetRouteMonitoring() const
440 {
441     return m_hasPlaybackTargetAvailabilityListeners && !client().elementIsHidden();
442 }
443 #endif
444
445 #if ENABLE(MEDIA_SOURCE)
446 const unsigned fiveMinutesOf1080PVideo = 290 * 1024 * 1024; // 290 MB is approximately 5 minutes of 8Mbps (1080p) content.
447 const unsigned fiveMinutesStereoAudio = 14 * 1024 * 1024; // 14 MB is approximately 5 minutes of 384kbps content.
448
449 size_t MediaElementSession::maximumMediaSourceBufferSize(const SourceBuffer& buffer) const
450 {
451     // A good quality 1080p video uses 8,000 kbps and stereo audio uses 384 kbps, so assume 95% for video and 5% for audio.
452     const float bufferBudgetPercentageForVideo = .95;
453     const float bufferBudgetPercentageForAudio = .05;
454
455     size_t maximum;
456     Settings* settings = buffer.document().settings();
457     if (settings)
458         maximum = settings->maximumSourceBufferSize();
459     else
460         maximum = fiveMinutesOf1080PVideo + fiveMinutesStereoAudio;
461
462     // Allow a SourceBuffer to buffer as though it is audio-only even if it doesn't have any active tracks (yet).
463     size_t bufferSize = static_cast<size_t>(maximum * bufferBudgetPercentageForAudio);
464     if (buffer.hasVideo())
465         bufferSize += static_cast<size_t>(maximum * bufferBudgetPercentageForVideo);
466
467     // FIXME: we might want to modify this algorithm to:
468     // - decrease the maximum size for background tabs
469     // - decrease the maximum size allowed for inactive elements when a process has more than one
470     //   element, eg. so a page with many elements which are played one at a time doesn't keep
471     //   everything buffered after an element has finished playing.
472
473     return bufferSize;
474 }
475 #endif
476
477 static bool isMainContent(const HTMLMediaElement& element)
478 {
479     if (!element.hasAudio() || !element.hasVideo())
480         return false;
481
482     // Elements which have not yet been laid out, or which are not yet in the DOM, cannot be main content.
483     RenderBox* renderer = downcast<RenderBox>(element.renderer());
484     if (!renderer)
485         return false;
486
487     if (renderer->clientWidth() < elementMainContentMinimumWidth
488         || renderer->clientHeight() < elementMainContentMinimumHeight)
489         return false;
490
491     // Elements which are hidden by style, or have been scrolled out of view, cannot be main content.
492     if (renderer->style().visibility() != VISIBLE
493         || renderer->visibleInViewportState() != RenderElement::VisibleInViewport)
494         return false;
495
496     // Main content elements must be in the main frame.
497     Document& document = element.document();
498     if (!document.frame() || !document.frame()->isMainFrame())
499         return false;
500
501     MainFrame& mainFrame = document.frame()->mainFrame();
502     if (!mainFrame.view() || !mainFrame.view()->renderView())
503         return false;
504
505     RenderView& mainRenderView = *mainFrame.view()->renderView();
506
507     // Hit test the area of the main frame where the element appears, to determine if the element is being obscured.
508     IntRect rectRelativeToView = element.clientRect();
509     ScrollPosition scrollPosition = mainFrame.view()->documentScrollPositionRelativeToViewOrigin();
510     IntRect rectRelativeToTopDocument(rectRelativeToView.location() + scrollPosition, rectRelativeToView.size());
511     HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent | HitTestRequest::IgnoreClipping | HitTestRequest::DisallowShadowContent);
512     HitTestResult result(rectRelativeToTopDocument.center());
513
514     // Elements which are obscured by other elements cannot be main content.
515     mainRenderView.hitTest(request, result);
516     Element* hitElement = result.innerElement();
517     if (hitElement != &element)
518         return false;
519
520     return true;
521 }
522
523 void MediaElementSession::mainContentCheckTimerFired()
524 {
525     if (!hasBehaviorRestriction(OverrideUserGestureRequirementForMainContent))
526         return;
527
528     bool wasMainContent = m_isMainContent;
529     m_isMainContent = isMainContent(m_element);
530
531     if (m_isMainContent != wasMainContent)
532         m_element.updateShouldPlay();
533 }
534
535 bool MediaElementSession::updateIsMainContent() const
536 {
537     return m_isMainContent = isMainContent(m_element);
538 }
539
540 }
541
542 #endif // ENABLE(VIDEO)