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;
 }