Allow sequential playback of media files when initial playback started with a user gesture
https://bugs.webkit.org/show_bug.cgi?id=197959
<rdar://problem/50655207>
Reviewed by Youenn Fablet.
Source/WebCore:
Test: media/playlist-inherits-user-gesture.html
* dom/Document.cpp:
(WebCore::Document::processingUserGestureForMedia const): Return true if it is within
one second of the last HTMLMediaElement 'ended' event.
* dom/Document.h:
(WebCore::Document::mediaFinishedPlaying):
* html/HTMLMediaElement.cpp:
(WebCore::HTMLMediaElement::parseAttribute): removeBehaviorsRestrictionsAfterFirstUserGesture ->
removeBehaviorRestrictionsAfterFirstUserGesture.
(WebCore::HTMLMediaElement::load): Ditto. Don't call removeBehaviorsRestrictionsAfterFirstUserGesture,
it will be done in prepareForLoad.
(WebCore::HTMLMediaElement::prepareForLoad): removeBehaviorsRestrictionsAfterFirstUserGesture ->
removeBehaviorRestrictionsAfterFirstUserGesture.
(WebCore::HTMLMediaElement::audioTrackEnabledChanged): Ditto.
(WebCore::HTMLMediaElement::play): Ditto.
(WebCore::HTMLMediaElement::pause): Ditto.
(WebCore::HTMLMediaElement::setVolume): Ditto.
(WebCore::HTMLMediaElement::setMuted): Ditto.
(WebCore::HTMLMediaElement::webkitShowPlaybackTargetPicker): Ditto.
(WebCore::HTMLMediaElement::dispatchEvent): Call document().mediaFinishedPlaying()
when dispatching the 'ended' event.
(WebCore::HTMLMediaElement::removeBehaviorRestrictionsAfterFirstUserGesture): Rename. Set
m_removedBehaviorRestrictionsAfterFirstUserGesture.
(WebCore::HTMLMediaElement::removeBehaviorsRestrictionsAfterFirstUserGesture): Deleted.
* html/HTMLMediaElement.h:
* html/HTMLVideoElement.cpp:
(WebCore:HTMLVideoElement::nativeImageForCurrentTime): Convert to runtime logging.
(WebCore:HTMLVideoElement::webkitEnterFullscreen): Ditto.
(WebCore:HTMLVideoElement::webkitSetPresentationMode): Ditto.
(WebCore:HTMLVideoElement::fullscreenModeChanged): Ditto.
* html/MediaElementSession.cpp:
(WebCore::MediaElementSession::removeBehaviorRestriction): Update log message.
LayoutTests:
* media/media-fullscreen.js: Insert a pause between tests to clear the user gesture
used in the first test.
* media/playlist-inherits-user-gesture-expected.txt: Added.
* media/playlist-inherits-user-gesture.html: Added.
git-svn-id: http://svn.webkit.org/repository/webkit/trunk@245467 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 77bac32..bc65398 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,16 @@
+2019-05-17 Eric Carlson <eric.carlson@apple.com>
+
+ Allow sequential playback of media files when initial playback started with a user gesture
+ https://bugs.webkit.org/show_bug.cgi?id=197959
+ <rdar://problem/50655207>
+
+ Reviewed by Youenn Fablet.
+
+ * media/media-fullscreen.js: Insert a pause between tests to clear the user gesture
+ used in the first test.
+ * media/playlist-inherits-user-gesture-expected.txt: Added.
+ * media/playlist-inherits-user-gesture.html: Added.
+
2019-05-17 Truitt Savell <tsavell@apple.com>
Unmark several skipped tests in wk2
diff --git a/LayoutTests/media/media-fullscreen.js b/LayoutTests/media/media-fullscreen.js
index 5e1d291..d339ca0 100644
--- a/LayoutTests/media/media-fullscreen.js
+++ b/LayoutTests/media/media-fullscreen.js
@@ -14,7 +14,10 @@
else {
if (movie.type == 'video')
testDOMException("mediaElement.webkitEnterFullScreen()", "DOMException.INVALID_STATE_ERR");
- openNextMovie();
+
+ // A user gesture will transfer across setTimeout for 1 second, so pause to let that
+ // expire before opening the next movie.
+ setTimeout(openNextMovie, 1010);
}
}
@@ -62,7 +65,6 @@
var movie = movieInfo.movies[movieInfo.current];
consoleWrite("* event handler NOT triggered by a user gesture");
-
if (movie.type == 'video') {
testExpected("mediaElement.webkitSupportsFullscreen", movie.supportsFS);
if (mediaElement.webkitSupportsPresentationMode)
@@ -80,10 +82,7 @@
testDOMException("mediaElement.webkitEnterFullScreen()", "DOMException.INVALID_STATE_ERR");
// Click on the button
- if (window.testRunner)
- setTimeout(clickEnterFullscreenButton, 10);
- else
- openNextMovie();
+ runWithKeyDown(clickEnterFullscreenButton);
}
function openNextMovie()
diff --git a/LayoutTests/media/playlist-inherits-user-gesture-expected.txt b/LayoutTests/media/playlist-inherits-user-gesture-expected.txt
new file mode 100644
index 0000000..a4015d4
--- /dev/null
+++ b/LayoutTests/media/playlist-inherits-user-gesture-expected.txt
@@ -0,0 +1,27 @@
+** Start first video with user gesture.
+RUN(window.internals.settings.setVideoPlaybackRequiresUserGesture(true);)
+RUN(video1 = document.createElement("video"))
+RUN(video1.src = findMediaFile("video", "content/test"))
+RUN(document.body.appendChild(video1))
+EXPECTED (window.internals.pageMediaState().includes('HasUserInteractedWithMediaElement') == 'false') OK
+RUN(video1.play())
+EXPECTED (window.internals.pageMediaState().includes('HasUserInteractedWithMediaElement') == 'true') OK
+EVENT(playing)
+RUN(video1.currentTime = video1.duration - 0.2)
+EVENT(ended)
+
+** Start second video without user gesture but within inheritance window, should succeed.
+RUN(video2 = document.createElement("video"))
+RUN(video2.src = findMediaFile("video", "content/test"))
+RUN(document.body.appendChild(video2))
+Promise resolved OK
+RUN(video2.currentTime = video2.duration - 0.2)
+EVENT(ended)
+
+** Start third video without user gesture but after inheritance window, should fail.
+RUN(video3 = document.createElement("video"))
+RUN(video3.src = findMediaFile("video", "content/test"))
+RUN(document.body.appendChild(video3))
+Promise rejected correctly OK
+END OF TEST
+
diff --git a/LayoutTests/media/playlist-inherits-user-gesture.html b/LayoutTests/media/playlist-inherits-user-gesture.html
new file mode 100644
index 0000000..810cc3f
--- /dev/null
+++ b/LayoutTests/media/playlist-inherits-user-gesture.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>playlist-inherits-user-gesture</title>
+ <script src=media-file.js></script>
+ <script src=video-test.js></script>
+ <script>
+ async function runTest() {
+ consoleWrite("** Start first video with user gesture.")
+ if (window.internals)
+ run('window.internals.settings.setVideoPlaybackRequiresUserGesture(true);');
+ run('video1 = document.createElement("video")');
+ run('video1.src = findMediaFile("video", "content/test")');
+ video1.controls = 1;
+ run('document.body.appendChild(video1)');
+
+ if (window.internals)
+ testExpected("window.internals.pageMediaState().includes('HasUserInteractedWithMediaElement')", false);
+ runWithKeyDown(() => {
+ run('video1.play()');
+ });
+ if (window.internals)
+ testExpected("window.internals.pageMediaState().includes('HasUserInteractedWithMediaElement')", true)
+
+ await waitFor(video1, 'playing');
+ run('video1.currentTime = video1.duration - 0.2');
+ await waitFor(video1, 'ended');
+
+ consoleWrite("<br>** Start second video without user gesture but within inheritance window, should succeed.")
+ run('video2 = document.createElement("video")');
+ run('video2.src = findMediaFile("video", "content/test")');
+ video2.controls = 1;
+ run('document.body.appendChild(video2)');
+
+ await shouldResolve(video2.play());
+ run('video2.currentTime = video2.duration - 0.2');
+ await waitFor(video2, 'ended');
+
+ consoleWrite("<br>** Start third video without user gesture but after inheritance window, should fail.")
+ await sleepFor(1200);
+ run('video3 = document.createElement("video")');
+ run('video3.src = findMediaFile("video", "content/test")');
+ video3.controls = 1;
+ run('document.body.appendChild(video3)');
+
+ await shouldReject(video3.play());
+
+ endTest();
+ }
+ </script>
+</head>
+<body onload="runTest()">
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 6fa4e9f..dbdffce 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,48 @@
+2019-05-17 Eric Carlson <eric.carlson@apple.com>
+
+ Allow sequential playback of media files when initial playback started with a user gesture
+ https://bugs.webkit.org/show_bug.cgi?id=197959
+ <rdar://problem/50655207>
+
+ Reviewed by Youenn Fablet.
+
+ Test: media/playlist-inherits-user-gesture.html
+
+ * dom/Document.cpp:
+ (WebCore::Document::processingUserGestureForMedia const): Return true if it is within
+ one second of the last HTMLMediaElement 'ended' event.
+ * dom/Document.h:
+ (WebCore::Document::mediaFinishedPlaying):
+
+ * html/HTMLMediaElement.cpp:
+ (WebCore::HTMLMediaElement::parseAttribute): removeBehaviorsRestrictionsAfterFirstUserGesture ->
+ removeBehaviorRestrictionsAfterFirstUserGesture.
+ (WebCore::HTMLMediaElement::load): Ditto. Don't call removeBehaviorsRestrictionsAfterFirstUserGesture,
+ it will be done in prepareForLoad.
+ (WebCore::HTMLMediaElement::prepareForLoad): removeBehaviorsRestrictionsAfterFirstUserGesture ->
+ removeBehaviorRestrictionsAfterFirstUserGesture.
+ (WebCore::HTMLMediaElement::audioTrackEnabledChanged): Ditto.
+ (WebCore::HTMLMediaElement::play): Ditto.
+ (WebCore::HTMLMediaElement::pause): Ditto.
+ (WebCore::HTMLMediaElement::setVolume): Ditto.
+ (WebCore::HTMLMediaElement::setMuted): Ditto.
+ (WebCore::HTMLMediaElement::webkitShowPlaybackTargetPicker): Ditto.
+ (WebCore::HTMLMediaElement::dispatchEvent): Call document().mediaFinishedPlaying()
+ when dispatching the 'ended' event.
+ (WebCore::HTMLMediaElement::removeBehaviorRestrictionsAfterFirstUserGesture): Rename. Set
+ m_removedBehaviorRestrictionsAfterFirstUserGesture.
+ (WebCore::HTMLMediaElement::removeBehaviorsRestrictionsAfterFirstUserGesture): Deleted.
+ * html/HTMLMediaElement.h:
+
+ * html/HTMLVideoElement.cpp:
+ (WebCore:HTMLVideoElement::nativeImageForCurrentTime): Convert to runtime logging.
+ (WebCore:HTMLVideoElement::webkitEnterFullscreen): Ditto.
+ (WebCore:HTMLVideoElement::webkitSetPresentationMode): Ditto.
+ (WebCore:HTMLVideoElement::fullscreenModeChanged): Ditto.
+
+ * html/MediaElementSession.cpp:
+ (WebCore::MediaElementSession::removeBehaviorRestriction): Update log message.
+
2019-05-17 Brent Fulgham <bfulgham@apple.com>
Hardening: Prevent FrameLoader crash due to SetForScope
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index cecd766..4191d216 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -328,6 +328,7 @@
static const unsigned cMaxWriteRecursionDepth = 21;
bool Document::hasEverCreatedAnAXObjectCache = false;
+static const Seconds maxIntervalForUserGestureForwardingAfterMediaFinishesPlaying { 1_s };
// DOM Level 2 says (letters added):
//
@@ -6576,6 +6577,9 @@
if (UserGestureIndicator::processingUserGestureForMedia())
return true;
+ if (m_userActivatedMediaFinishedPlayingTimestamp + maxIntervalForUserGestureForwardingAfterMediaFinishesPlaying >= MonotonicTime::now())
+ return true;
+
if (settings().mediaUserGestureInheritsFromDocument())
return topDocument().hasHadUserInteraction();
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index 971fe4c..d061a0b 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -1221,6 +1221,7 @@
bool hasHadUserInteraction() const { return static_cast<bool>(m_lastHandledUserGestureTimestamp); }
void updateLastHandledUserGestureTimestamp(MonotonicTime);
bool processingUserGestureForMedia() const;
+ void userActivatedMediaFinishedPlaying() { m_userActivatedMediaFinishedPlayingTimestamp = MonotonicTime::now(); }
void setUserDidInteractWithPage(bool userDidInteractWithPage) { ASSERT(&topDocument() == this); m_userDidInteractWithPage = userDidInteractWithPage; }
bool userDidInteractWithPage() const { ASSERT(&topDocument() == this); return m_userDidInteractWithPage; }
@@ -1835,6 +1836,7 @@
std::unique_ptr<EventTargetSet> m_wheelEventTargets;
MonotonicTime m_lastHandledUserGestureTimestamp;
+ MonotonicTime m_userActivatedMediaFinishedPlayingTimestamp;
void clearScriptedAnimationController();
RefPtr<ScriptedAnimationController> m_scriptedAnimationController;
diff --git a/Source/WebCore/html/HTMLMediaElement.cpp b/Source/WebCore/html/HTMLMediaElement.cpp
index 6d87bdd..216e937 100644
--- a/Source/WebCore/html/HTMLMediaElement.cpp
+++ b/Source/WebCore/html/HTMLMediaElement.cpp
@@ -839,7 +839,7 @@
setMediaGroup(value);
else if (name == autoplayAttr) {
if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture();
+ removeBehaviorRestrictionsAfterFirstUserGesture();
} else if (name == titleAttr) {
if (m_mediaSession)
m_mediaSession->clientCharacteristicsChanged();
@@ -1178,9 +1178,6 @@
INFO_LOG(LOGIDENTIFIER);
- if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture();
-
prepareForLoad();
m_resourceSelectionTaskQueue.enqueueTask([this] {
prepareToPlay();
@@ -1193,7 +1190,10 @@
// The Media Element Load Algorithm
// 12 February 2017
- INFO_LOG(LOGIDENTIFIER);
+ ALWAYS_LOG(LOGIDENTIFIER, "gesture = ", processingUserGestureForMedia());
+
+ if (processingUserGestureForMedia())
+ removeBehaviorRestrictionsAfterFirstUserGesture();
// 1 - Abort any already-running instance of the resource selection algorithm for this element.
// Perform the cleanup required for the resource load algorithm to run.
@@ -1951,7 +1951,7 @@
if (m_audioTracks && m_audioTracks->contains(track))
m_audioTracks->scheduleChangeEvent();
if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
+ removeBehaviorRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
}
void HTMLMediaElement::textTrackModeChanged(TextTrack& track)
@@ -3515,7 +3515,7 @@
}
if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture();
+ removeBehaviorRestrictionsAfterFirstUserGesture();
m_pendingPlayPromises.append(WTFMove(promise));
playInternal();
@@ -3532,7 +3532,7 @@
return;
}
if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture();
+ removeBehaviorRestrictionsAfterFirstUserGesture();
playInternal();
}
@@ -3633,7 +3633,7 @@
return;
if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::RequireUserGestureToControlControlsManager);
+ removeBehaviorRestrictionsAfterFirstUserGesture(MediaElementSession::RequireUserGestureToControlControlsManager);
pauseInternal();
}
@@ -3744,7 +3744,7 @@
#if !PLATFORM(IOS_FAMILY)
if (volume && processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
+ removeBehaviorRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
m_volume = volume;
m_volumeInitialized = true;
@@ -3783,7 +3783,7 @@
bool mutedStateChanged = m_muted != muted;
if (mutedStateChanged || !m_explicitlyMuted) {
if (processingUserGestureForMedia()) {
- removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
+ removeBehaviorRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
if (hasAudio() && muted)
userDidInterfereWithAutoplay();
@@ -5878,7 +5878,7 @@
{
ALWAYS_LOG(LOGIDENTIFIER);
if (processingUserGestureForMedia())
- removeBehaviorsRestrictionsAfterFirstUserGesture();
+ removeBehaviorRestrictionsAfterFirstUserGesture();
m_mediaSession->showPlaybackTargetPicker();
}
@@ -5917,6 +5917,9 @@
{
DEBUG_LOG(LOGIDENTIFIER, event.type());
+ if (m_removedBehaviorRestrictionsAfterFirstUserGesture && event.type() == eventNames().endedEvent)
+ document().userActivatedMediaFinishedPlaying();
+
HTMLElement::dispatchEvent(event);
}
@@ -7207,7 +7210,7 @@
}
#endif
-void HTMLMediaElement::removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::BehaviorRestrictions mask)
+void HTMLMediaElement::removeBehaviorRestrictionsAfterFirstUserGesture(MediaElementSession::BehaviorRestrictions mask)
{
MediaElementSession::BehaviorRestrictions restrictionsToRemove = mask &
(MediaElementSession::RequireUserGestureForLoad
@@ -7222,6 +7225,8 @@
| MediaElementSession::InvisibleAutoplayNotPermitted
| MediaElementSession::RequireUserGestureToControlControlsManager);
+ m_removedBehaviorRestrictionsAfterFirstUserGesture = true;
+
m_mediaSession->removeBehaviorRestriction(restrictionsToRemove);
document().topDocument().noteUserInteractionWithMediaElement();
}
diff --git a/Source/WebCore/html/HTMLMediaElement.h b/Source/WebCore/html/HTMLMediaElement.h
index a091626..c15dc24 100644
--- a/Source/WebCore/html/HTMLMediaElement.h
+++ b/Source/WebCore/html/HTMLMediaElement.h
@@ -562,6 +562,7 @@
#if !RELEASE_LOG_DISABLED
const Logger& logger() const final { return *m_logger.get(); }
const void* logIdentifier() const final { return m_logIdentifier; }
+ const char* logClassName() const final { return "HTMLMediaElement"; }
WTFLogChannel& logChannel() const final;
#endif
@@ -851,7 +852,7 @@
void changeNetworkStateFromLoadingToIdle();
- void removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::BehaviorRestrictions mask = MediaElementSession::AllRestrictions);
+ void removeBehaviorRestrictionsAfterFirstUserGesture(MediaElementSession::BehaviorRestrictions mask = MediaElementSession::AllRestrictions);
void updateMediaController();
bool isBlocked() const;
@@ -942,8 +943,6 @@
void setInActiveDocument(bool);
#if !RELEASE_LOG_DISABLED
- const char* logClassName() const final { return "HTMLMediaElement"; }
-
const void* mediaPlayerLogIdentifier() final { return logIdentifier(); }
const Logger& mediaPlayerLogger() final { return logger(); }
#endif
@@ -1201,6 +1200,7 @@
bool m_isPlayingToWirelessTarget { false };
bool m_playingOnSecondScreen { false };
+ bool m_removedBehaviorRestrictionsAfterFirstUserGesture { false };
};
String convertEnumerationToString(HTMLMediaElement::AutoplayEventPlaybackState);
diff --git a/Source/WebCore/html/HTMLVideoElement.cpp b/Source/WebCore/html/HTMLVideoElement.cpp
index f47d467..a6f2f83 100644
--- a/Source/WebCore/html/HTMLVideoElement.cpp
+++ b/Source/WebCore/html/HTMLVideoElement.cpp
@@ -318,7 +318,7 @@
ExceptionOr<void> HTMLVideoElement::webkitEnterFullscreen()
{
- LOG(Media, "HTMLVideoElement::webkitEnterFullscreen(%p)", this);
+ ALWAYS_LOG(LOGIDENTIFIER);
if (isFullscreen())
return { };
@@ -333,7 +333,7 @@
void HTMLVideoElement::webkitExitFullscreen()
{
- LOG(Media, "HTMLVideoElement::webkitExitFullscreen(%p)", this);
+ ALWAYS_LOG(LOGIDENTIFIER);
if (isFullscreen())
exitFullscreen();
}
@@ -443,7 +443,7 @@
void HTMLVideoElement::webkitSetPresentationMode(VideoPresentationMode mode)
{
- LOG(Media, "HTMLVideoElement::webkitSetPresentationMode(%p) - %d", this, mode);
+ ALWAYS_LOG(LOGIDENTIFIER, mode);
setFullscreenMode(toFullscreenMode(mode));
}
@@ -483,7 +483,7 @@
void HTMLVideoElement::fullscreenModeChanged(VideoFullscreenMode mode)
{
if (mode != fullscreenMode()) {
- LOG(Media, "HTMLVideoElement::fullscreenModeChanged(%p) - mode changed from %i to %i", this, fullscreenMode(), mode);
+ ALWAYS_LOG(LOGIDENTIFIER, "changed from ", fullscreenMode(), ", to ", mode);
scheduleEvent(eventNames().webkitpresentationmodechangedEvent);
}
diff --git a/Source/WebCore/html/MediaElementSession.cpp b/Source/WebCore/html/MediaElementSession.cpp
index d987c64..e14cd57 100644
--- a/Source/WebCore/html/MediaElementSession.cpp
+++ b/Source/WebCore/html/MediaElementSession.cpp
@@ -242,7 +242,7 @@
if (!(m_restrictions & restriction))
return;
- INFO_LOG(LOGIDENTIFIER, "removing ", restrictionNames(m_restrictions & restriction));
+ INFO_LOG(LOGIDENTIFIER, "removed ", restrictionNames(m_restrictions & restriction));
m_restrictions &= ~restriction;
}