[Picture-in-Picture Web API] Implement PictureInPictureWindow
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 30 Oct 2019 00:21:19 +0000 (00:21 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 30 Oct 2019 00:21:19 +0000 (00:21 +0000)
https://bugs.webkit.org/show_bug.cgi?id=202615

Patch by Peng Liu <peng.liu6@apple.com> on 2019-10-29
Reviewed by Eric Carlson.

Source/WebCore:

Tests: media/picture-in-picture-api-enter-pip-1.html
       media/picture-in-picture-api-enter-pip-2.html
       media/picture-in-picture-api-enter-pip-3.html
       media/picture-in-picture-api-enter-pip-4.html
       media/picture-in-picture-api-exit-pip-1.html
       media/picture-in-picture-api-exit-pip-2.html
       media/picture-in-picture-api-pip-events.html
       media/picture-in-picture-api-pip-window.html

* Modules/pictureinpicture/HTMLVideoElementPictureInPicture.cpp:
(WebCore::HTMLVideoElementPictureInPicture::HTMLVideoElementPictureInPicture):
(WebCore::HTMLVideoElementPictureInPicture::requestPictureInPicture):
(WebCore::HTMLVideoElementPictureInPicture::didEnterPictureInPicture):
(WebCore::HTMLVideoElementPictureInPicture::didExitPictureInPicture):
(WebCore::HTMLVideoElementPictureInPicture::pictureInPictureWindowResized):
* Modules/pictureinpicture/HTMLVideoElementPictureInPicture.h:
* Modules/pictureinpicture/PictureInPictureWindow.cpp:
(WebCore::PictureInPictureWindow::create):
(WebCore::PictureInPictureWindow::PictureInPictureWindow):
(WebCore::PictureInPictureWindow::setSize):
(WebCore::PictureInPictureWindow::close):
* Modules/pictureinpicture/PictureInPictureWindow.h:
* html/HTMLVideoElement.cpp:
(WebCore::HTMLVideoElement::fullscreenModeChanged):
(WebCore::HTMLVideoElement::didBecomeFullscreenElement):
(WebCore::HTMLVideoElement::setPictureInPictureObserver):
(WebCore::HTMLVideoElement::setVideoFullscreenFrame):
* html/HTMLVideoElement.h:
* platform/PictureInPictureObserver.h:

LayoutTests:

Add layout test cases for the Picture-in-Picture API.

* TestExpectations:
* media/picture-in-picture-api-enter-pip-1-expected.txt: Added.
* media/picture-in-picture-api-enter-pip-1.html: Added.
* media/picture-in-picture-api-enter-pip-2-expected.txt: Added.
* media/picture-in-picture-api-enter-pip-2.html: Added.
* media/picture-in-picture-api-enter-pip-3-expected.txt: Added.
* media/picture-in-picture-api-enter-pip-3.html: Added.
* media/picture-in-picture-api-enter-pip-4-expected.txt: Added.
* media/picture-in-picture-api-enter-pip-4.html: Added.
* media/picture-in-picture-api-exit-pip-1-expected.txt: Added.
* media/picture-in-picture-api-exit-pip-1.html: Added.
* media/picture-in-picture-api-exit-pip-2-expected.txt: Added.
* media/picture-in-picture-api-exit-pip-2.html: Added.
* media/picture-in-picture-api-pip-events-expected.txt: Added.
* media/picture-in-picture-api-pip-events.html: Added.
* media/picture-in-picture-api-pip-window-expected.txt: Added.
* media/picture-in-picture-api-pip-window.html: Added.
* platform/mac-wk2/TestExpectations:

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251745 268f45cc-cd09-0410-ab3c-d52691b4dbfc

27 files changed:
LayoutTests/ChangeLog
LayoutTests/TestExpectations
LayoutTests/media/picture-in-picture-api-enter-pip-1-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-1.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-2-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-2.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-3-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-3.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-4-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-enter-pip-4.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-exit-pip-1-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-exit-pip-1.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-exit-pip-2-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-exit-pip-2.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-pip-events-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-pip-events.html [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-pip-window-expected.txt [new file with mode: 0644]
LayoutTests/media/picture-in-picture-api-pip-window.html [new file with mode: 0644]
LayoutTests/platform/mac-wk2/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/Modules/pictureinpicture/HTMLVideoElementPictureInPicture.cpp
Source/WebCore/Modules/pictureinpicture/HTMLVideoElementPictureInPicture.h
Source/WebCore/Modules/pictureinpicture/PictureInPictureWindow.cpp
Source/WebCore/Modules/pictureinpicture/PictureInPictureWindow.h
Source/WebCore/html/HTMLVideoElement.cpp
Source/WebCore/html/HTMLVideoElement.h
Source/WebCore/platform/PictureInPictureObserver.h

index a176947..462ebaf 100644 (file)
@@ -1,3 +1,31 @@
+2019-10-29  Peng Liu  <peng.liu6@apple.com>
+
+        [Picture-in-Picture Web API] Implement PictureInPictureWindow
+        https://bugs.webkit.org/show_bug.cgi?id=202615
+
+        Reviewed by Eric Carlson.
+
+        Add layout test cases for the Picture-in-Picture API.
+
+        * TestExpectations:
+        * media/picture-in-picture-api-enter-pip-1-expected.txt: Added.
+        * media/picture-in-picture-api-enter-pip-1.html: Added.
+        * media/picture-in-picture-api-enter-pip-2-expected.txt: Added.
+        * media/picture-in-picture-api-enter-pip-2.html: Added.
+        * media/picture-in-picture-api-enter-pip-3-expected.txt: Added.
+        * media/picture-in-picture-api-enter-pip-3.html: Added.
+        * media/picture-in-picture-api-enter-pip-4-expected.txt: Added.
+        * media/picture-in-picture-api-enter-pip-4.html: Added.
+        * media/picture-in-picture-api-exit-pip-1-expected.txt: Added.
+        * media/picture-in-picture-api-exit-pip-1.html: Added.
+        * media/picture-in-picture-api-exit-pip-2-expected.txt: Added.
+        * media/picture-in-picture-api-exit-pip-2.html: Added.
+        * media/picture-in-picture-api-pip-events-expected.txt: Added.
+        * media/picture-in-picture-api-pip-events.html: Added.
+        * media/picture-in-picture-api-pip-window-expected.txt: Added.
+        * media/picture-in-picture-api-pip-window.html: Added.
+        * platform/mac-wk2/TestExpectations:
+
 2019-10-29  Antoine Quint  <graouts@apple.com>
 
         WebAnimation should never prevent entering the back/forward cache
index 4e400e0..cd85406 100644 (file)
@@ -195,6 +195,14 @@ webkit.org/b/202617 imported/w3c/web-platform-tests/picture-in-picture [ Skip ]
 
 # PiP API tests are only relevant on mac and iPad
 media/picture-in-picture-api-element-attributes.html [ Skip ]
+media/picture-in-picture-api-enter-pip-1.html [ Skip ]
+media/picture-in-picture-api-enter-pip-2.html [ Skip ]
+media/picture-in-picture-api-enter-pip-3.html [ Skip ]
+media/picture-in-picture-api-enter-pip-4.html [ Skip ]
+media/picture-in-picture-api-exit-pip-1.html [ Skip ]
+media/picture-in-picture-api-exit-pip-2.html [ Skip ]
+media/picture-in-picture-api-pip-events.html [ Skip ]
+media/picture-in-picture-api-pip-window.html [ Skip ]
 
 # Shared Workers are unsupported
 imported/w3c/web-platform-tests/secure-contexts/basic-shared-worker.html [ Skip ]
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-1-expected.txt b/LayoutTests/media/picture-in-picture-api-enter-pip-1-expected.txt
new file mode 100644 (file)
index 0000000..ac1c637
--- /dev/null
@@ -0,0 +1,8 @@
+This tests that request Picture-in-Picture requires a user gesture.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+RUN(video.src = findMediaFile("video", "content/test"))
+EVENT(canplaythrough)
+EXPECTED (error.name == 'NotAllowedError') OK
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-1.html b/LayoutTests/media/picture-in-picture-api-enter-pip-1.html
new file mode 100644 (file)
index 0000000..83cf9e0
--- /dev/null
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+            run('video.src = findMediaFile("video", "content/test")');
+            await waitFor(video, 'canplaythrough');
+
+            video.requestPictureInPicture()
+            .then(() => {
+                failTest("request Picture-in-Picture requires a user gesture.");
+            })
+            .catch(error => {
+                window.error = error;
+                testExpected('error.name', 'NotAllowedError');
+                endTest();
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that request Picture-in-Picture requires a user gesture.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-2-expected.txt b/LayoutTests/media/picture-in-picture-api-enter-pip-2-expected.txt
new file mode 100644 (file)
index 0000000..067a047
--- /dev/null
@@ -0,0 +1,6 @@
+This tests that request Picture-in-Picture requires loaded metadata for the video element.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+EXPECTED (error.name == 'InvalidStateError') OK
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-2.html b/LayoutTests/media/picture-in-picture-api-enter-pip-2.html
new file mode 100644 (file)
index 0000000..b1a85b2
--- /dev/null
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+            runWithKeyDown(function() {
+                video.requestPictureInPicture()
+                .then(() => {
+                    failTest('request Picture-in-Picture requires loaded metadata for the video element.')
+                })
+                .catch(error => {
+                    window.error = error;
+                    testExpected('error.name', 'InvalidStateError');
+                    endTest();
+                });
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that request Picture-in-Picture requires loaded metadata for the video element.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-3-expected.txt b/LayoutTests/media/picture-in-picture-api-enter-pip-3-expected.txt
new file mode 100644 (file)
index 0000000..5318137
--- /dev/null
@@ -0,0 +1,8 @@
+This tests that request Picture-in-Picture requires video track for the video element.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+RUN(video.src = findMediaFile("audio", "content/test"))
+EVENT(canplaythrough)
+EXPECTED (error.name == 'InvalidStateError') OK
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-3.html b/LayoutTests/media/picture-in-picture-api-enter-pip-3.html
new file mode 100644 (file)
index 0000000..4cb79a8
--- /dev/null
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+            run('video.src = findMediaFile("audio", "content/test")');
+            await waitFor(video, 'canplaythrough');
+            runWithKeyDown(function() {
+                video.requestPictureInPicture()
+                .then(() => {
+                    failTest('request Picture-in-Picture requires video track for the video element.')
+                })
+                .catch(error => {
+                    window.error = error;
+                    testExpected('error.name', 'InvalidStateError');
+                    endTest();
+                });
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that request Picture-in-Picture requires video track for the video element.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-4-expected.txt b/LayoutTests/media/picture-in-picture-api-enter-pip-4-expected.txt
new file mode 100644 (file)
index 0000000..1e76314
--- /dev/null
@@ -0,0 +1,7 @@
+This tests that request Picture-in-Picture resolves on user click.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+RUN(video.src = findMediaFile("video", "content/test"))
+EVENT(canplaythrough)
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-enter-pip-4.html b/LayoutTests/media/picture-in-picture-api-enter-pip-4.html
new file mode 100644 (file)
index 0000000..ea4e9bc
--- /dev/null
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+            run('video.src = findMediaFile("video", "content/test")');
+            await waitFor(video, 'canplaythrough');
+            runWithKeyDown(function() {
+                video.requestPictureInPicture()
+                .then(() => {
+                    endTest();
+                })
+                .catch(() => {
+                    failTest('request Picture-in-Picture does not resolve on user click.');
+                });
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that request Picture-in-Picture resolves on user click.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-exit-pip-1-expected.txt b/LayoutTests/media/picture-in-picture-api-exit-pip-1-expected.txt
new file mode 100644 (file)
index 0000000..ecc3777
--- /dev/null
@@ -0,0 +1,8 @@
+This tests that exit Picture-in-Picture resolves when there is a Picture-in-Picture video.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+RUN(video.src = findMediaFile("video", "content/test"))
+EVENT(canplaythrough)
+EVENT(enterpictureinpicture)
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-exit-pip-1.html b/LayoutTests/media/picture-in-picture-api-exit-pip-1.html
new file mode 100644 (file)
index 0000000..26839d1
--- /dev/null
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+            run('video.src = findMediaFile("video", "content/test")');
+            await waitFor(video, 'canplaythrough');
+            runWithKeyDown(function() { video.requestPictureInPicture() });
+            await waitFor(video, 'enterpictureinpicture');
+            runWithKeyDown(function() {
+                document.exitPictureInPicture()
+                .then(() => {
+                    endTest();
+                })
+                .catch(() => {
+                    failTest('Exit Picture-in-Picture resolves when there is a Picture-in-Picture video.')
+                });
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that exit Picture-in-Picture resolves when there is a Picture-in-Picture video.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-exit-pip-2-expected.txt b/LayoutTests/media/picture-in-picture-api-exit-pip-2-expected.txt
new file mode 100644 (file)
index 0000000..9115447
--- /dev/null
@@ -0,0 +1,5 @@
+This tests that exit Picture-in-Picture rejects when there is no Picture-in-Picture video.
+
+EXPECTED (error.name == 'InvalidStateError') OK
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-exit-pip-2.html b/LayoutTests/media/picture-in-picture-api-exit-pip-2.html
new file mode 100644 (file)
index 0000000..f10795b
--- /dev/null
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+            runWithKeyDown(function() {
+                document.exitPictureInPicture()
+                .then(() => {
+                    failTest('Exit Picture-in-Picture rejects when there is no Picture-in-Picture video.');
+                })
+                .catch(error => {
+                    window.error = error;
+                    testExpected('error.name', 'InvalidStateError');
+                    endTest();
+                });
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that exit Picture-in-Picture rejects when there is no Picture-in-Picture video.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-pip-events-expected.txt b/LayoutTests/media/picture-in-picture-api-pip-events-expected.txt
new file mode 100644 (file)
index 0000000..92b09ce
--- /dev/null
@@ -0,0 +1,11 @@
+This tests that events are fired correctly when a video element enters and exits the Picture-in-Picture mode.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+RUN(video.src = findMediaFile("video", "content/test"))
+EVENT(canplaythrough)
+RUN(video.play())
+EVENT(playing)
+EVENT(enterpictureinpicture)
+EVENT(leavepictureinpicture)
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-pip-events.html b/LayoutTests/media/picture-in-picture-api-pip-events.html
new file mode 100644 (file)
index 0000000..7985d14
--- /dev/null
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+
+            run('video.src = findMediaFile("video", "content/test")');
+            await waitFor(video, 'canplaythrough');
+
+            run('video.play()');
+            await waitFor(video, 'playing');
+
+            runWithKeyDown(function() { video.requestPictureInPicture(); });
+            await waitFor(video, 'enterpictureinpicture');
+
+            runWithKeyDown(function() { document.exitPictureInPicture(); });
+            await waitFor(video, 'leavepictureinpicture');
+
+            endTest();
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that events are fired correctly when a video element enters and exits the Picture-in-Picture mode.</div>
+    <video controls></video>
+</body>
+</html>
diff --git a/LayoutTests/media/picture-in-picture-api-pip-window-expected.txt b/LayoutTests/media/picture-in-picture-api-pip-window-expected.txt
new file mode 100644 (file)
index 0000000..7f11e74
--- /dev/null
@@ -0,0 +1,11 @@
+This tests that a pip window is returned correctly when a video element enters the Picture-in-Picture mode.
+
+RUN(internals.settings.setAllowsPictureInPictureMediaPlayback(true))
+RUN(video.src = findMediaFile("video", "content/test"))
+EVENT(canplaythrough)
+RUN(video.play())
+EVENT(playing)
+EXPECTED (pipWindow.width > '0') OK
+EXPECTED (pipWindow.height > '0') OK
+END OF TEST
+
diff --git a/LayoutTests/media/picture-in-picture-api-pip-window.html b/LayoutTests/media/picture-in-picture-api-pip-window.html
new file mode 100644 (file)
index 0000000..854552a
--- /dev/null
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="video-test.js"></script>
+    <script src="media-file.js"></script>
+    <script>
+        window.addEventListener('load', async event => {
+            findMediaElement();
+
+            run('internals.settings.setAllowsPictureInPictureMediaPlayback(true)');
+
+            run('video.src = findMediaFile("video", "content/test")');
+            await waitFor(video, 'canplaythrough');
+
+            run('video.play()');
+            await waitFor(video, 'playing');
+
+            runWithKeyDown(function() {
+                video.requestPictureInPicture()
+                .then(pipWindow => {
+                    window.pipWindow = pipWindow;
+                    testExpected('pipWindow.width', 0, '>');
+                    testExpected('pipWindow.height', 0, '>');
+                    endTest();
+                })
+                .catch(error => {
+                    failTest("Failed to enter the Picture-in-Picture mode.");
+                });
+            });
+        });
+    </script>
+</head>
+<body>
+    <div>This tests that a pip window is returned correctly when a video element enters the Picture-in-Picture mode.</div>
+    <video controls></video>
+</body>
+</html>
index c2f09b0..8738847 100644 (file)
@@ -542,6 +542,14 @@ webkit.org/b/172998 [ Sierra+ ] media/fullscreen-api-enabled-media-with-presenta
 webkit.org/b/173199 [ Sierra+ ] media/navigate-with-pip-should-not-crash.html [ Pass Failure ]
 [ Sierra+ ] media/pip-video-going-into-fullscreen.html [ Pass ]
 [ Sierra+ ] media/picture-in-picture-api-element-attributes.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-enter-pip-1.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-enter-pip-2.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-enter-pip-3.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-enter-pip-4.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-exit-pip-1.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-exit-pip-2.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-pip-events.html [ Pass ]
+[ Sierra+ ] media/picture-in-picture-api-pip-window.html [ Pass ]
 
 # RTL Scrollbars are enabled on Sierra WebKit2.
 webkit.org/b/179455 [ Sierra+ ] fast/scrolling/rtl-scrollbars.html [ Pass ImageOnlyFailure ]
index 70b786f..2f3a43b 100644 (file)
@@ -1,3 +1,40 @@
+2019-10-29  Peng Liu  <peng.liu6@apple.com>
+
+        [Picture-in-Picture Web API] Implement PictureInPictureWindow
+        https://bugs.webkit.org/show_bug.cgi?id=202615
+
+        Reviewed by Eric Carlson.
+
+        Tests: media/picture-in-picture-api-enter-pip-1.html
+               media/picture-in-picture-api-enter-pip-2.html
+               media/picture-in-picture-api-enter-pip-3.html
+               media/picture-in-picture-api-enter-pip-4.html
+               media/picture-in-picture-api-exit-pip-1.html
+               media/picture-in-picture-api-exit-pip-2.html
+               media/picture-in-picture-api-pip-events.html
+               media/picture-in-picture-api-pip-window.html
+
+        * Modules/pictureinpicture/HTMLVideoElementPictureInPicture.cpp:
+        (WebCore::HTMLVideoElementPictureInPicture::HTMLVideoElementPictureInPicture):
+        (WebCore::HTMLVideoElementPictureInPicture::requestPictureInPicture):
+        (WebCore::HTMLVideoElementPictureInPicture::didEnterPictureInPicture):
+        (WebCore::HTMLVideoElementPictureInPicture::didExitPictureInPicture):
+        (WebCore::HTMLVideoElementPictureInPicture::pictureInPictureWindowResized):
+        * Modules/pictureinpicture/HTMLVideoElementPictureInPicture.h:
+        * Modules/pictureinpicture/PictureInPictureWindow.cpp:
+        (WebCore::PictureInPictureWindow::create):
+        (WebCore::PictureInPictureWindow::PictureInPictureWindow):
+        (WebCore::PictureInPictureWindow::setSize):
+        (WebCore::PictureInPictureWindow::close):
+        * Modules/pictureinpicture/PictureInPictureWindow.h:
+        * html/HTMLVideoElement.cpp:
+        (WebCore::HTMLVideoElement::fullscreenModeChanged):
+        (WebCore::HTMLVideoElement::didBecomeFullscreenElement):
+        (WebCore::HTMLVideoElement::setPictureInPictureObserver):
+        (WebCore::HTMLVideoElement::setVideoFullscreenFrame):
+        * html/HTMLVideoElement.h:
+        * platform/PictureInPictureObserver.h:
+
 2019-10-29  Antoine Quint  <graouts@apple.com>
 
         WebAnimation should never prevent entering the back/forward cache
index 762e8dc..1ce1cd5 100644 (file)
@@ -31,6 +31,7 @@
 
 #include "HTMLVideoElement.h"
 #include "JSDOMPromiseDeferred.h"
+#include "JSPictureInPictureWindow.h"
 #include "Logging.h"
 #include "PictureInPictureWindow.h"
 #include "VideoTrackList.h"
@@ -42,6 +43,7 @@ WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLVideoElementPictureInPicture);
 
 HTMLVideoElementPictureInPicture::HTMLVideoElementPictureInPicture(HTMLVideoElement& videoElement)
     : m_videoElement(videoElement)
+    , m_pictureInPictureWindow(PictureInPictureWindow::create(videoElement.document()))
 #if !RELEASE_LOG_DISABLED
     , m_logger(videoElement.document().logger())
     , m_logIdentifier(uniqueLogIdentifier())
@@ -70,11 +72,6 @@ HTMLVideoElementPictureInPicture* HTMLVideoElementPictureInPicture::from(HTMLVid
 
 void HTMLVideoElementPictureInPicture::requestPictureInPicture(HTMLVideoElement& videoElement, Ref<DeferredPromise>&& promise)
 {
-    if (!videoElement.player() || !videoElement.player()->supportsPictureInPicture()) {
-        promise->reject(NotSupportedError, "The video element does not support the Picture-in-Picture mode.");
-        return;
-    }
-
     if (videoElement.readyState() == HTMLMediaElementEnums::HAVE_NOTHING) {
         promise->reject(InvalidStateError, "The video element is not ready to enter the Picture-in-Picture mode.");
         return;
@@ -93,12 +90,12 @@ void HTMLVideoElementPictureInPicture::requestPictureInPicture(HTMLVideoElement&
         return;
     }
 
+    auto videoElementPictureInPicture = HTMLVideoElementPictureInPicture::from(videoElement);
     if (videoElement.document().pictureInPictureElement() == &videoElement) {
-        promise->reject(NotAllowedError, "The video element is already in the Picture-in-Picture mode.");
+        promise->resolve<IDLInterface<PictureInPictureWindow>>(*(videoElementPictureInPicture->m_pictureInPictureWindow));
         return;
     }
 
-    auto videoElementPictureInPicture = HTMLVideoElementPictureInPicture::from(videoElement);
     if (videoElementPictureInPicture->m_enterPictureInPicturePromise || videoElementPictureInPicture->m_exitPictureInPicturePromise) {
         promise->reject(NotAllowedError, "The video element is processing a Picture-in-Picture request.");
         return;
@@ -143,17 +140,18 @@ void HTMLVideoElementPictureInPicture::exitPictureInPicture(Ref<DeferredPromise>
     m_videoElement.webkitSetPresentationMode(HTMLVideoElement::VideoPresentationMode::Inline);
 }
 
-void HTMLVideoElementPictureInPicture::didEnterPictureInPicture(IntSize windowSize)
+void HTMLVideoElementPictureInPicture::didEnterPictureInPicture(const IntSize& windowSize)
 {
     INFO_LOG(LOGIDENTIFIER);
     m_videoElement.document().setPictureInPictureElement(&m_videoElement);
+    m_pictureInPictureWindow->setSize(windowSize);
+
     if (m_enterPictureInPicturePromise) {
         EnterPictureInPictureEvent::Init initializer;
         initializer.bubbles = true;
-        initializer.pictureInPictureWindow = PictureInPictureWindow::create(m_videoElement.document(), windowSize.width(), windowSize.height());
+        initializer.pictureInPictureWindow = m_pictureInPictureWindow;
         m_videoElement.scheduleEvent(EnterPictureInPictureEvent::create(eventNames().enterpictureinpictureEvent, WTFMove(initializer)));
-
-        m_enterPictureInPicturePromise->resolve();
+        m_enterPictureInPicturePromise->resolve<IDLInterface<PictureInPictureWindow>>(*m_pictureInPictureWindow);
         m_enterPictureInPicturePromise = nullptr;
     }
 }
@@ -161,15 +159,27 @@ void HTMLVideoElementPictureInPicture::didEnterPictureInPicture(IntSize windowSi
 void HTMLVideoElementPictureInPicture::didExitPictureInPicture()
 {
     INFO_LOG(LOGIDENTIFIER);
+    m_pictureInPictureWindow->close();
     m_videoElement.document().setPictureInPictureElement(nullptr);
+
     if (m_exitPictureInPicturePromise) {
         m_videoElement.scheduleEvent(Event::create(eventNames().leavepictureinpictureEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
-
         m_exitPictureInPicturePromise->resolve();
         m_exitPictureInPicturePromise = nullptr;
     }
 }
 
+void HTMLVideoElementPictureInPicture::pictureInPictureWindowResized(const IntSize& windowSize)
+{
+    if (m_pictureInPictureWindow->width() == windowSize.width() && m_pictureInPictureWindow->height() == windowSize.height())
+        return;
+
+    m_pictureInPictureWindow->setSize(windowSize);
+    auto resizeEvent = Event::create(eventNames().resizeEvent, Event::CanBubble::Yes, Event::IsCancelable::No);
+    resizeEvent->setTarget(m_pictureInPictureWindow);
+    m_videoElement.scheduleEvent(WTFMove(resizeEvent));
+}
+
 #if !RELEASE_LOG_DISABLED
 WTFLogChannel& HTMLVideoElementPictureInPicture::logChannel() const
 {
@@ -177,10 +187,6 @@ WTFLogChannel& HTMLVideoElementPictureInPicture::logChannel() const
 }
 #endif
 
-void HTMLVideoElementPictureInPicture::pictureInPictureWindowResized(IntSize)
-{
-}
-
 } // namespace WebCore
 
 #endif // ENABLE(PICTURE_IN_PICTURE_API)
index 16266dc..e4f0624 100644 (file)
@@ -60,9 +60,9 @@ public:
 
     void exitPictureInPicture(Ref<DeferredPromise>&&);
 
-    void didEnterPictureInPicture(IntSize);
+    void didEnterPictureInPicture(const IntSize&);
     void didExitPictureInPicture();
-    void pictureInPictureWindowResized(IntSize);
+    void pictureInPictureWindowResized(const IntSize&);
 
 #if !RELEASE_LOG_DISABLED
     const Logger& logger() const final { return m_logger.get(); }
@@ -78,6 +78,7 @@ private:
     bool m_disablePictureInPicture { false };
 
     HTMLVideoElement& m_videoElement;
+    RefPtr<PictureInPictureWindow> m_pictureInPictureWindow;
     RefPtr<DeferredPromise> m_enterPictureInPicturePromise;
     RefPtr<DeferredPromise> m_exitPictureInPicturePromise;
 
index 5df2c18..59781c8 100644 (file)
@@ -35,20 +35,28 @@ namespace WebCore {
 
 WTF_MAKE_ISO_ALLOCATED_IMPL(PictureInPictureWindow);
 
-Ref<PictureInPictureWindow> PictureInPictureWindow::create(ScriptExecutionContext& scriptExecutionContext, int width, int height)
+Ref<PictureInPictureWindow> PictureInPictureWindow::create(ScriptExecutionContext& scriptExecutionContext)
 {
-    return adoptRef(*new PictureInPictureWindow(scriptExecutionContext, width, height));
+    return adoptRef(*new PictureInPictureWindow(scriptExecutionContext));
 }
 
-PictureInPictureWindow::PictureInPictureWindow(ScriptExecutionContext& scriptExecutionContext, int width, int height)
+PictureInPictureWindow::PictureInPictureWindow(ScriptExecutionContext& scriptExecutionContext)
     : m_scriptExecutionContext(scriptExecutionContext)
-    , m_width(width)
-    , m_height(height)
 {
 }
 
 PictureInPictureWindow::~PictureInPictureWindow() = default;
 
+void PictureInPictureWindow::setSize(const IntSize& size)
+{
+    m_size = size;
+}
+
+void PictureInPictureWindow::close()
+{
+    m_size = { 0, 0 };
+}
+
 } // namespace WebCore
 
 #endif // ENABLE(PICTURE_IN_PICTURE_API)
index 42fae30..0c5295e 100644 (file)
@@ -38,17 +38,19 @@ class PictureInPictureWindow final
     , public EventTargetWithInlineData {
     WTF_MAKE_ISO_ALLOCATED(PictureInPictureWindow);
 public:
-    static Ref<PictureInPictureWindow> create(ScriptExecutionContext&, int, int);
+    static Ref<PictureInPictureWindow> create(ScriptExecutionContext&);
     virtual ~PictureInPictureWindow();
 
-    int width() const { return m_width; }
-    int height() const { return m_height; }
+    int width() const { return m_size.width(); }
+    int height() const { return m_size.height(); }
+    void setSize(const IntSize&);
+    void close();
 
     using RefCounted<PictureInPictureWindow>::ref;
     using RefCounted<PictureInPictureWindow>::deref;
 
 private:
-    PictureInPictureWindow(ScriptExecutionContext&, int, int);
+    PictureInPictureWindow(ScriptExecutionContext&);
 
     // EventTarget
     void refEventTarget() final { ref(); }
@@ -57,8 +59,7 @@ private:
     ScriptExecutionContext* scriptExecutionContext() const override { return &m_scriptExecutionContext; };
 
     ScriptExecutionContext& m_scriptExecutionContext;
-    int m_width;
-    int m_height;
+    IntSize m_size;
 };
 
 } // namespace WebCore
index a30e583..6cdd36e 100644 (file)
@@ -492,8 +492,10 @@ void HTMLVideoElement::fullscreenModeChanged(VideoFullscreenMode mode)
             HTMLVideoElement::VideoPresentationMode targetVideoPresentationMode = toPresentationMode(mode);
             HTMLVideoElement::VideoPresentationMode sourceVideoPresentationMode = toPresentationMode(fullscreenMode());
 
-            if (targetVideoPresentationMode != HTMLVideoElement::VideoPresentationMode::PictureInPicture && sourceVideoPresentationMode == HTMLVideoElement::VideoPresentationMode::PictureInPicture)
+            if (targetVideoPresentationMode != HTMLVideoElement::VideoPresentationMode::PictureInPicture && sourceVideoPresentationMode == HTMLVideoElement::VideoPresentationMode::PictureInPicture) {
                 m_pictureInPictureObserver->didExitPictureInPicture();
+                m_isFullscreen = false;
+            }
         }
 #endif
     }
@@ -507,6 +509,7 @@ void HTMLVideoElement::fullscreenModeChanged(VideoFullscreenMode mode)
 #if ENABLE(PICTURE_IN_PICTURE_API)
 void HTMLVideoElement::didBecomeFullscreenElement()
 {
+    m_isFullscreen = true;
     m_waitingForPictureInPictureWindowFrame = true;
     HTMLMediaElement::didBecomeFullscreenElement();
 }
@@ -515,7 +518,6 @@ void HTMLVideoElement::setPictureInPictureObserver(PictureInPictureObserver* obs
 {
     m_pictureInPictureObserver = observer;
 }
-
 #endif
 
 #endif
@@ -526,6 +528,11 @@ void HTMLVideoElement::setVideoFullscreenFrame(FloatRect frame)
     HTMLMediaElement::setVideoFullscreenFrame(frame);
 
 #if ENABLE(PICTURE_IN_PICTURE_API)
+    // fullscreenMode() does not always provide the correct fullscreen mode
+    // when mode changing is happening (webkit.org/b/203443)
+    if (!m_isFullscreen)
+        return;
+
     if (toPresentationMode(fullscreenMode()) != VideoPresentationMode::PictureInPicture)
         return;
 
index a3ae71c..162b94e 100644 (file)
@@ -137,6 +137,7 @@ private:
 
 #if ENABLE(PICTURE_IN_PICTURE_API)
     bool m_waitingForPictureInPictureWindowFrame { false };
+    bool m_isFullscreen { false };
     PictureInPictureObserver* m_pictureInPictureObserver { nullptr };
 #endif
 };
index a03c8e2..ed8c162 100644 (file)
@@ -32,9 +32,9 @@ namespace WebCore {
 class PictureInPictureObserver : public CanMakeWeakPtr<PictureInPictureObserver> {
 public:
     virtual ~PictureInPictureObserver() { };
-    virtual void didEnterPictureInPicture(IntSize) = 0;
+    virtual void didEnterPictureInPicture(const IntSize&) = 0;
     virtual void didExitPictureInPicture() = 0;
-    virtual void pictureInPictureWindowResized(IntSize) = 0;
+    virtual void pictureInPictureWindowResized(const IntSize&) = 0;
 };
 
 }