Prerender: Defer Access to Speaker selection API for Prerendering

This CL adds a web tests to ensure that the access of the Speaker
selection API are deferred until prerendered page is activated.

Bug: 1202032
Change-Id: Ice6e4ba1228de1d4acbbfc534f68d277b8f13a73
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2850496
Commit-Queue: Miyoung Shin <myid.shin@igalia.com>
Reviewed-by: Lingqi Chi <lingqi@chromium.org>
Reviewed-by: Matt Falkenhagen <falken@chromium.org>
Reviewed-by: Guido Urdaneta <guidou@chromium.org>
Cr-Commit-Position: refs/heads/master@{#884013}
diff --git a/third_party/blink/renderer/modules/audio_output_devices/html_media_element_audio_output_device.cc b/third_party/blink/renderer/modules/audio_output_devices/html_media_element_audio_output_device.cc
index 8d587c2..c2b6710 100644
--- a/third_party/blink/renderer/modules/audio_output_devices/html_media_element_audio_output_device.cc
+++ b/third_party/blink/renderer/modules/audio_output_devices/html_media_element_audio_output_device.cc
@@ -58,6 +58,8 @@
   ~SetSinkIdResolver() override = default;
   void StartAsync();
 
+  void Start();
+
   void Trace(Visitor*) const override;
 
  private:
@@ -96,6 +98,25 @@
                                       WrapWeakPersistent(this)));
 }
 
+void SetSinkIdResolver::Start() {
+  auto* context = GetExecutionContext();
+  if (!context || context->IsContextDestroyed())
+    return;
+
+  if (LocalDOMWindow* window = DynamicTo<LocalDOMWindow>(context)) {
+    if (window->document()->IsPrerendering()) {
+      window->document()->AddPostPrerenderingActivationStep(
+          WTF::Bind(&SetSinkIdResolver::Start, WrapWeakPersistent(this)));
+      return;
+    }
+  }
+
+  if (sink_id_ == HTMLMediaElementAudioOutputDevice::sinkId(*element_))
+    Resolve();
+  else
+    StartAsync();
+}
+
 void SetSinkIdResolver::DoSetSinkId() {
   auto set_sink_id_completion_callback =
       WTF::Bind(&SetSinkIdResolver::OnSetSinkIdComplete, WrapPersistent(this));
@@ -189,11 +210,7 @@
   SetSinkIdResolver* resolver =
       SetSinkIdResolver::Create(script_state, element, sink_id);
   ScriptPromise promise = resolver->Promise();
-  if (sink_id == HTMLMediaElementAudioOutputDevice::sinkId(element))
-    resolver->Resolve();
-  else
-    resolver->StartAsync();
-
+  resolver->Start();
   return promise;
 }
 
diff --git a/third_party/blink/web_tests/wpt_internal/prerender/resources/audio-setSinkId.https.html b/third_party/blink/web_tests/wpt_internal/prerender/resources/audio-setSinkId.https.html
new file mode 100644
index 0000000..bec8e09b
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/prerender/resources/audio-setSinkId.https.html
@@ -0,0 +1,31 @@
+<!--
+ Copyright 2021 The Chromium Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="utils.js"></script>
+<script src="deferred-promise-utils.js"></script>
+<audio controls id="beat" src="/media/mp3/sound_5.mp3" loop></audio>
+<script>
+
+const params = new URLSearchParams(location.search);
+
+// The main test page (restriction-audio-setSinkId.https.html) loads the
+// initiator page, then the initiator page will prerender itself with the
+// `prerendering` parameter.
+const isPrerendering = params.has('prerendering');
+
+if (!isPrerendering) {
+  loadInitiatorPage();
+} else {
+  const prerenderEventCollector = new PrerenderEventCollector();
+
+  const promise = beat.setSinkId(
+      params.get('sinkId') === 'invalid' ? 'fakeId' : '');
+  prerenderEventCollector.start(promise, 'Audio.setSinkId');
+}
+</script>
diff --git a/third_party/blink/web_tests/wpt_internal/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.html b/third_party/blink/web_tests/wpt_internal/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.html
new file mode 100644
index 0000000..66122a2
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.html
@@ -0,0 +1,64 @@
+<!--
+ Copyright 2021 The Chromium Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+
+<!DOCTYPE html>
+<!--
+This file cannot be upstreamed to WPT until:
+* startPrerendering() usage is replaced with a WebDriver API
+* 'speaker-selection' gets permission to use speaker devices with a WebDriver
+*  API
+-->
+
+<title>
+Access to the setSinkId of the Audio API with an invalid value is deferred
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+promise_test(async t => {
+  const bc = new BroadcastChannel('test-channel');
+  t.add_cleanup(_ => bc.close());
+
+  const gotMessage = new Promise(resolve => {
+    bc.addEventListener('message', e => {
+      resolve(e.data);
+    }, {
+      once: true
+    });
+  });
+
+  const url = `resources/audio-setSinkId.https.html?sinkId=invalid`;
+  window.open(url, '_blank', 'noopener');
+
+  const result = await gotMessage;
+  const expected = [
+    {
+      event: 'started waiting Audio.setSinkId',
+      prerendering: true
+    },
+    {
+      event: 'prerendering change',
+      prerendering: false
+    },
+    {
+      event: 'Audio.setSinkId rejected: NotFoundError',
+      prerendering: false
+    },
+  ];
+  assert_equals(result.length, expected.length);
+  for (let i = 0; i < result.length; i++) {
+    assert_equals(result[i].event, expected[i].event, `event${i}`);
+    assert_equals(result[i].prerendering, expected[i].prerendering,
+      `prerendering${i}`);
+  }
+
+}, `the access to the setSinkId of Audio API with the invalid sinkId should be
+    deferred until the prerendered page is activated`);
+</script>
+</body>
diff --git a/third_party/blink/web_tests/wpt_internal/prerender/restriction-audio-setSinkId.https.html b/third_party/blink/web_tests/wpt_internal/prerender/restriction-audio-setSinkId.https.html
new file mode 100644
index 0000000..c7309c5
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/prerender/restriction-audio-setSinkId.https.html
@@ -0,0 +1,62 @@
+<!--
+ Copyright 2021 The Chromium Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+
+<!DOCTYPE html>
+<!--
+This file cannot be upstreamed to WPT until:
+* startPrerendering() usage is replaced with a WebDriver API
+* 'speaker-selection' is used with a WebDriver API
+-->
+
+<title>Access to the setSinkId of the Audio API is deferred</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+promise_test(async t => {
+  const bc = new BroadcastChannel('test-channel');
+  t.add_cleanup(_ => bc.close());
+
+  const gotMessage = new Promise(resolve => {
+    bc.addEventListener('message', e => {
+      resolve(e.data);
+    }, {
+      once: true
+    });
+  });
+
+  const url = `resources/audio-setSinkId.https.html?sinkId=default`;
+  window.open(url, '_blank', 'noopener');
+
+  const result = await gotMessage;
+  const expected = [
+    {
+      event: 'started waiting Audio.setSinkId',
+      prerendering: true
+    },
+    {
+      event: 'prerendering change',
+      prerendering: false
+    },
+    {
+      event: 'finished waiting Audio.setSinkId',
+      prerendering: false
+    },
+  ];
+  assert_equals(result.length, expected.length);
+  for (let i = 0; i < result.length; i++) {
+    assert_equals(result[i].event, expected[i].event, `event${i}`);
+    assert_equals(result[i].prerendering, expected[i].prerendering,
+      `prerendering${i}`);
+  }
+
+}, `the access to the setSinkId of Audio API should be deferred until the
+    prerendered page is activated`);
+
+</script>
+</body>