Add heuristic for "main content" videos which override user gesture requirements
[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(RequireUserGestureForRateChange);
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 (m_restrictions & OverrideUserGestureRequirementForMainContent && updateIsMainContent())
150         return true;
151
152     if (m_restrictions & RequireUserGestureForRateChange && !ScriptController::processingUserGestureForMedia()) {
153         LOG(Media, "MediaElementSession::playbackPermitted - returning FALSE");
154         return false;
155     }
156
157     if (m_restrictions & RequireUserGestureForAudioRateChange && element.hasAudio() && !ScriptController::processingUserGestureForMedia()) {
158         LOG(Media, "MediaElementSession::playbackPermitted - returning FALSE");
159         return false;
160     }
161
162     return true;
163 }
164
165 bool MediaElementSession::dataLoadingPermitted(const HTMLMediaElement&) const
166 {
167     if (m_restrictions & OverrideUserGestureRequirementForMainContent && updateIsMainContent())
168         return true;
169
170     if (m_restrictions & RequireUserGestureForLoad && !ScriptController::processingUserGestureForMedia()) {
171         LOG(Media, "MediaElementSession::dataLoadingPermitted - returning FALSE");
172         return false;
173     }
174
175     return true;
176 }
177
178 bool MediaElementSession::fullscreenPermitted(const HTMLMediaElement&) const
179 {
180     if (m_restrictions & RequireUserGestureForFullscreen && !ScriptController::processingUserGestureForMedia()) {
181         LOG(Media, "MediaElementSession::fullscreenPermitted - returning FALSE");
182         return false;
183     }
184
185     return true;
186 }
187
188 bool MediaElementSession::pageAllowsDataLoading(const HTMLMediaElement& element) const
189 {
190     Page* page = element.document().page();
191     if (m_restrictions & RequirePageConsentToLoadMedia && page && !page->canStartMedia()) {
192         LOG(Media, "MediaElementSession::pageAllowsDataLoading - returning FALSE");
193         return false;
194     }
195
196     return true;
197 }
198
199 bool MediaElementSession::pageAllowsPlaybackAfterResuming(const HTMLMediaElement& element) const
200 {
201     Page* page = element.document().page();
202     if (m_restrictions & RequirePageConsentToResumeMedia && page && !page->canStartMedia()) {
203         LOG(Media, "MediaElementSession::pageAllowsPlaybackAfterResuming - returning FALSE");
204         return false;
205     }
206
207     return true;
208 }
209
210 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
211 void MediaElementSession::showPlaybackTargetPicker(const HTMLMediaElement& element)
212 {
213     LOG(Media, "MediaElementSession::showPlaybackTargetPicker");
214
215     if (m_restrictions & RequireUserGestureToShowPlaybackTargetPicker && !ScriptController::processingUserGestureForMedia()) {
216         LOG(Media, "MediaElementSession::showPlaybackTargetPicker - returning early because of permissions");
217         return;
218     }
219
220     if (!element.document().page()) {
221         LOG(Media, "MediaElementSession::showingPlaybackTargetPickerPermitted - returning early because page is NULL");
222         return;
223     }
224
225 #if !PLATFORM(IOS)
226     if (element.readyState() < HTMLMediaElementEnums::HAVE_METADATA) {
227         LOG(Media, "MediaElementSession::showPlaybackTargetPicker - returning early because element is not playable");
228         return;
229     }
230 #endif
231
232     String customMenuItemTitle = element.playbackTargetPickerCustomActionName();
233     element.document().showPlaybackTargetPicker(*this, is<HTMLVideoElement>(element), customMenuItemTitle);
234 }
235
236 bool MediaElementSession::hasWirelessPlaybackTargets(const HTMLMediaElement&) const
237 {
238 #if PLATFORM(IOS)
239     // FIXME: consolidate Mac and iOS implementations
240     m_hasPlaybackTargets = PlatformMediaSessionManager::sharedManager().hasWirelessTargetsAvailable();
241 #endif
242
243     LOG(Media, "MediaElementSession::hasWirelessPlaybackTargets - returning %s", m_hasPlaybackTargets ? "TRUE" : "FALSE");
244
245     return m_hasPlaybackTargets;
246 }
247
248 bool MediaElementSession::wirelessVideoPlaybackDisabled(const HTMLMediaElement& element) const
249 {
250     Settings* settings = element.document().settings();
251     if (!settings || !settings->allowsAirPlayForMediaPlayback()) {
252         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning TRUE because of settings");
253         return true;
254     }
255
256     if (element.fastHasAttribute(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)) {
257         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning TRUE because of attribute");
258         return true;
259     }
260
261 #if PLATFORM(IOS)
262     String legacyAirplayAttributeValue = element.fastGetAttribute(HTMLNames::webkitairplayAttr);
263     if (equalLettersIgnoringASCIICase(legacyAirplayAttributeValue, "deny")) {
264         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning TRUE because of legacy attribute");
265         return true;
266     }
267     if (equalLettersIgnoringASCIICase(legacyAirplayAttributeValue, "allow")) {
268         LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning FALSE because of legacy attribute");
269         return false;
270     }
271 #endif
272
273     MediaPlayer* player = element.player();
274     if (!player)
275         return true;
276
277     bool disabled = player->wirelessVideoPlaybackDisabled();
278     LOG(Media, "MediaElementSession::wirelessVideoPlaybackDisabled - returning %s because media engine says so", disabled ? "TRUE" : "FALSE");
279     
280     return disabled;
281 }
282
283 void MediaElementSession::setWirelessVideoPlaybackDisabled(const HTMLMediaElement& element, bool disabled)
284 {
285     if (disabled)
286         addBehaviorRestriction(WirelessVideoPlaybackDisabled);
287     else
288         removeBehaviorRestriction(WirelessVideoPlaybackDisabled);
289
290     MediaPlayer* player = element.player();
291     if (!player)
292         return;
293
294     LOG(Media, "MediaElementSession::setWirelessVideoPlaybackDisabled - disabled %s", disabled ? "TRUE" : "FALSE");
295     player->setWirelessVideoPlaybackDisabled(disabled);
296 }
297
298 void MediaElementSession::setHasPlaybackTargetAvailabilityListeners(const HTMLMediaElement& element, bool hasListeners)
299 {
300     LOG(Media, "MediaElementSession::setHasPlaybackTargetAvailabilityListeners - hasListeners %s", hasListeners ? "TRUE" : "FALSE");
301
302 #if PLATFORM(IOS)
303     UNUSED_PARAM(element);
304     m_hasPlaybackTargetAvailabilityListeners = hasListeners;
305     PlatformMediaSessionManager::sharedManager().configureWireLessTargetMonitoring();
306 #else
307     UNUSED_PARAM(hasListeners);
308     element.document().playbackTargetPickerClientStateDidChange(*this, element.mediaState());
309 #endif
310 }
311
312 void MediaElementSession::setPlaybackTarget(Ref<MediaPlaybackTarget>&& device)
313 {
314     m_playbackTarget = WTFMove(device);
315     client().setWirelessPlaybackTarget(*m_playbackTarget.copyRef());
316 }
317
318 void MediaElementSession::targetAvailabilityChangedTimerFired()
319 {
320     client().wirelessRoutesAvailableDidChange();
321 }
322
323 void MediaElementSession::externalOutputDeviceAvailableDidChange(bool hasTargets)
324 {
325     if (m_hasPlaybackTargets == hasTargets)
326         return;
327
328     LOG(Media, "MediaElementSession::externalOutputDeviceAvailableDidChange(%p) - hasTargets %s", this, hasTargets ? "TRUE" : "FALSE");
329
330     m_hasPlaybackTargets = hasTargets;
331     m_targetAvailabilityChangedTimer.startOneShot(0);
332 }
333
334 bool MediaElementSession::canPlayToWirelessPlaybackTarget() const
335 {
336     if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute())
337         return false;
338
339     return client().canPlayToWirelessPlaybackTarget();
340 }
341
342 bool MediaElementSession::isPlayingToWirelessPlaybackTarget() const
343 {
344     if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute())
345         return false;
346
347     return client().isPlayingToWirelessPlaybackTarget();
348 }
349
350 void MediaElementSession::setShouldPlayToPlaybackTarget(bool shouldPlay)
351 {
352     LOG(Media, "MediaElementSession::setShouldPlayToPlaybackTarget - shouldPlay %s", shouldPlay ? "TRUE" : "FALSE");
353     m_shouldPlayToPlaybackTarget = shouldPlay;
354     client().setShouldPlayToPlaybackTarget(shouldPlay);
355 }
356
357 void MediaElementSession::customPlaybackActionSelected()
358 {
359     client().customPlaybackActionSelected();
360 }
361
362 void MediaElementSession::mediaStateDidChange(const HTMLMediaElement& element, MediaProducer::MediaStateFlags state)
363 {
364     element.document().playbackTargetPickerClientStateDidChange(*this, state);
365 }
366 #endif
367
368 MediaPlayer::Preload MediaElementSession::effectivePreloadForElement(const HTMLMediaElement& element) const
369 {
370     MediaPlayer::Preload preload = element.preloadValue();
371
372     if (pageExplicitlyAllowsElementToAutoplayInline(element))
373         return preload;
374
375     if (m_restrictions & MetadataPreloadingNotPermitted)
376         return MediaPlayer::None;
377
378     if (m_restrictions & AutoPreloadingNotPermitted) {
379         if (preload > MediaPlayer::MetaData)
380             return MediaPlayer::MetaData;
381     }
382
383     return preload;
384 }
385
386 bool MediaElementSession::requiresFullscreenForVideoPlayback(const HTMLMediaElement& element) const
387 {
388     if (pageExplicitlyAllowsElementToAutoplayInline(element))
389         return false;
390
391     Settings* settings = element.document().settings();
392     if (!settings || !settings->allowsInlineMediaPlayback())
393         return true;
394
395     return settings->inlineMediaPlaybackRequiresPlaysInlineAttribute() && !element.fastHasAttribute(HTMLNames::webkit_playsinlineAttr);
396 }
397
398 bool MediaElementSession::allowsAutomaticMediaDataLoading(const HTMLMediaElement& element) const
399 {
400     if (pageExplicitlyAllowsElementToAutoplayInline(element))
401         return true;
402
403     Settings* settings = element.document().settings();
404     if (settings && settings->mediaDataLoadsAutomatically())
405         return true;
406
407     return false;
408 }
409
410 void MediaElementSession::mediaEngineUpdated(const HTMLMediaElement& element)
411 {
412     LOG(Media, "MediaElementSession::mediaEngineUpdated");
413
414 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
415     if (m_restrictions & WirelessVideoPlaybackDisabled)
416         setWirelessVideoPlaybackDisabled(element, true);
417     if (m_playbackTarget)
418         client().setWirelessPlaybackTarget(*m_playbackTarget.copyRef());
419     if (m_shouldPlayToPlaybackTarget)
420         client().setShouldPlayToPlaybackTarget(true);
421 #else
422     UNUSED_PARAM(element);
423 #endif
424     
425 }
426
427 bool MediaElementSession::allowsPictureInPicture(const HTMLMediaElement& element) const
428 {
429     Settings* settings = element.document().settings();
430     return settings && settings->allowsPictureInPictureMediaPlayback() && !element.webkitCurrentPlaybackTargetIsWireless();
431 }
432
433 #if PLATFORM(IOS)
434 bool MediaElementSession::requiresPlaybackTargetRouteMonitoring() const
435 {
436     return m_hasPlaybackTargetAvailabilityListeners && !client().elementIsHidden();
437 }
438 #endif
439
440 #if ENABLE(MEDIA_SOURCE)
441 const unsigned fiveMinutesOf1080PVideo = 290 * 1024 * 1024; // 290 MB is approximately 5 minutes of 8Mbps (1080p) content.
442 const unsigned fiveMinutesStereoAudio = 14 * 1024 * 1024; // 14 MB is approximately 5 minutes of 384kbps content.
443
444 size_t MediaElementSession::maximumMediaSourceBufferSize(const SourceBuffer& buffer) const
445 {
446     // A good quality 1080p video uses 8,000 kbps and stereo audio uses 384 kbps, so assume 95% for video and 5% for audio.
447     const float bufferBudgetPercentageForVideo = .95;
448     const float bufferBudgetPercentageForAudio = .05;
449
450     size_t maximum;
451     Settings* settings = buffer.document().settings();
452     if (settings)
453         maximum = settings->maximumSourceBufferSize();
454     else
455         maximum = fiveMinutesOf1080PVideo + fiveMinutesStereoAudio;
456
457     // Allow a SourceBuffer to buffer as though it is audio-only even if it doesn't have any active tracks (yet).
458     size_t bufferSize = static_cast<size_t>(maximum * bufferBudgetPercentageForAudio);
459     if (buffer.hasVideo())
460         bufferSize += static_cast<size_t>(maximum * bufferBudgetPercentageForVideo);
461
462     // FIXME: we might want to modify this algorithm to:
463     // - decrease the maximum size for background tabs
464     // - decrease the maximum size allowed for inactive elements when a process has more than one
465     //   element, eg. so a page with many elements which are played one at a time doesn't keep
466     //   everything buffered after an element has finished playing.
467
468     return bufferSize;
469 }
470 #endif
471
472 static bool isMainContent(const HTMLMediaElement& element)
473 {
474     if (!element.hasAudio() || !element.hasVideo())
475         return false;
476
477     // Elements which have not yet been laid out, or which are not yet in the DOM, cannot be main content.
478     RenderBox* renderer = downcast<RenderBox>(element.renderer());
479     if (!renderer)
480         return false;
481
482     if (renderer->clientWidth() < elementMainContentMinimumWidth
483         || renderer->clientHeight() < elementMainContentMinimumHeight)
484         return false;
485
486     // Elements which are hidden by style, or have been scrolled out of view, cannot be main content.
487     if (renderer->style().visibility() != VISIBLE
488         || renderer->visibleInViewportState() != RenderElement::VisibleInViewport)
489         return false;
490
491     // Main content elements must be in the main frame.
492     Document& document = element.document();
493     if (!document.frame() || !document.frame()->isMainFrame())
494         return false;
495
496     MainFrame& mainFrame = document.frame()->mainFrame();
497     if (!mainFrame.view() || !mainFrame.view()->renderView())
498         return false;
499
500     RenderView& mainRenderView = *mainFrame.view()->renderView();
501
502     // Hit test the area of the main frame where the element appears, to determine if the element is being obscured.
503     IntRect rectRelativeToView = element.clientRect();
504     ScrollPosition scrollPosition = mainFrame.view()->documentScrollPositionRelativeToViewOrigin();
505     IntRect rectRelativeToTopDocument(rectRelativeToView.location() + scrollPosition, rectRelativeToView.size());
506     HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent | HitTestRequest::IgnoreClipping | HitTestRequest::DisallowShadowContent);
507     HitTestResult result(rectRelativeToTopDocument.center());
508
509     // Elements which are obscured by other elements cannot be main content.
510     mainRenderView.hitTest(request, result);
511     Element* hitElement = result.innerElement();
512     if (hitElement != &element)
513         return false;
514
515     return true;
516 }
517
518 void MediaElementSession::mainContentCheckTimerFired()
519 {
520     if (!hasBehaviorRestriction(OverrideUserGestureRequirementForMainContent))
521         return;
522
523     bool wasMainContent = m_isMainContent;
524     m_isMainContent = isMainContent(m_element);
525
526     if (m_isMainContent != wasMainContent)
527         m_element.updateShouldPlay();
528 }
529
530 bool MediaElementSession::updateIsMainContent() const
531 {
532     return m_isMainContent = isMainContent(m_element);
533 }
534
535 }
536
537 #endif // ENABLE(VIDEO)