Loading toolkit/actors/PictureInPictureChild.jsm +30 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,10 @@ XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", }); XPCOMUtils.defineLazyModuleGetters(this, { NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm", }); XPCOMUtils.defineLazyPreferenceGetter( this, "DISPLAY_TEXT_TRACKS_PREF", Loading Loading @@ -930,6 +934,24 @@ class PictureInPictureToggleChild extends JSWindowActorChild { toggle.removeAttribute("policy"); } const nimbusExperimentVariables = NimbusFeatures.pictureinpicture.getAllVariables( { defaultValues: { title: null, message: false, showIconOnly: false } } ); // nimbusExperimentVariables will be defaultValues when the experiment is disabled if (nimbusExperimentVariables.title && nimbusExperimentVariables.message) { let pipExplainer = shadowRoot.querySelector(".pip-explainer"); let pipLabel = shadowRoot.querySelector(".pip-label"); pipExplainer.innerText = nimbusExperimentVariables.message; pipLabel.innerText = nimbusExperimentVariables.title; } else if (nimbusExperimentVariables.showIconOnly) { // We only want to show the PiP icon in this experiment scenario let pipExpanded = shadowRoot.querySelector(".pip-expanded"); pipExpanded.style.display = "none"; let pipIcon = shadowRoot.querySelector(".pip-icon"); pipIcon.style.display = "block"; } controlsOverlay.removeAttribute("hidetoggle"); // The hideToggleDeferredTask we create here is for automatically hiding Loading Loading @@ -966,6 +988,14 @@ class PictureInPictureToggleChild extends JSWindowActorChild { !toggle.hasAttribute("hidden") ) { Services.telemetry.scalarAdd("pictureinpicture.saw_toggle", 1); // only record if this is the first time seeing the toggle if ( !Services.prefs.getBoolPref( "media.videocontrols.picture-in-picture.video-toggle.has-used" ) ) { NimbusFeatures.pictureinpicture.recordExposureEvent(); } } // Now that we're hovering the video, we'll check to see if we're Loading toolkit/components/nimbus/FeatureManifest.yaml +14 −0 Original line number Diff line number Diff line Loading @@ -473,3 +473,17 @@ pbNewtab: schema: >- browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json variables: {} pictureinpicture: description: Message for first time Picture-in-Picture users hasExposure: true exposureDescription: Exposure is sent when a user hovers over a video and Picture-in-Picture has not been used before variables: title: type: string description: The title to be used for the PiP toggle message: type: string description: The message to be used in the PiP toggle showIconOnly: type: boolean description: Whether to show the first time PiP toggle or show the PiP icon only toolkit/components/pictureinpicture/tests/browser.ini +2 −0 Original line number Diff line number Diff line Loading @@ -61,6 +61,8 @@ support-files = skip-if = debug os == 'linux' && bits == 64 && !debug # Bug 1549875 [browser_nimbusMessageFirstTimePip.js] [browser_nimbusShowIconOnly.js] [browser_noPlayerControlsOnMiddleRightClick.js] [browser_noToggleOnAudio.js] [browser_playerControls.js] Loading toolkit/components/pictureinpicture/tests/browser_nimbusMessageFirstTimePip.js 0 → 100644 +178 −0 Original line number Diff line number Diff line /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ExperimentFakes } = ChromeUtils.import( "resource://testing-common/NimbusTestUtils.jsm" ); const PIP_EXPERIMENT_MESSAGE = "Hello world message"; const PIP_EXPERIMENT_TITLE = "Hello world title"; /** * This function will hover over the middle of the video and then * hover over the toggle to reveal the first time PiP message * @param browser The current browser * @param videoID The video element id */ async function hoverToggle(browser, videoID) { await prepareForToggleClick(browser, videoID); // Hover the mouse over the video to reveal the toggle. await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mouseover", }, browser ); info("Checking toggle policy"); await assertTogglePolicy(browser, videoID, null); let toggleClientRect = await getToggleClientRect(browser, videoID); info("Hovering the toggle rect now."); let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2; let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2; await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mouseover", }, browser ); } /** * This tests that the original DTD string is shown for the PiP toggle */ add_task(async function test_experiment_control() { await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { const s = `<!DOCTYPE bindings [ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> %videocontrolsDTD; ]> <html xmlns=\"http://www.w3.org/1999/xhtml\"> <head> <meta charset=\"utf-8\"/> <div class="pip-message">&pictureInPictureExplainer;</div> </head> </html>`; const parser = new DOMParser(); parser.forceEnableDTD(); let doc = parser.parseFromString(s, "application/xhtml+xml"); const pipDTDMessage = doc.querySelector(".pip-message").innerHTML.trim(); await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn(browser, [pipDTDMessage], async function( pipDTDMessage ) { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipButton = shadowRoot.querySelector(".pip-explainer"); Assert.equal( pipButton.textContent.trim(), pipDTDMessage, "The PiP explainer is default" ); }); } ); }); /** * This tests that the experiment message is shown for the PiP toggle */ add_task(async function test_experiment_message() { let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ featureId: "pictureinpicture", value: { title: PIP_EXPERIMENT_TITLE, message: PIP_EXPERIMENT_MESSAGE, }, }); registerCleanupFunction(async function() { await doExperimentCleanup(); }); await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn( browser, [PIP_EXPERIMENT_MESSAGE, PIP_EXPERIMENT_TITLE], async function(PIP_EXPERIMENT_MESSAGE, PIP_EXPERIMENT_TITLE) { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipExplainer = shadowRoot.querySelector(".pip-explainer"); let pipLabel = shadowRoot.querySelector(".pip-label"); Assert.equal( pipExplainer.textContent.trim(), PIP_EXPERIMENT_MESSAGE, "The PiP explainer is being overridden by the experiment" ); Assert.equal( pipLabel.textContent.trim(), PIP_EXPERIMENT_TITLE, "The PiP label is being overridden by the experiment" ); } ); } ); }); toolkit/components/pictureinpicture/tests/browser_nimbusShowIconOnly.js 0 → 100644 +171 −0 Original line number Diff line number Diff line /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ExperimentFakes } = ChromeUtils.import( "resource://testing-common/NimbusTestUtils.jsm" ); const PIP_EXPERIMENT_MESSAGE = "Hello world message"; const PIP_EXPERIMENT_TITLE = "Hello world title"; /** * This function will hover over the middle of the video and then * hover over the toggle to reveal the first time PiP message * @param browser The current browser * @param videoID The video element id */ async function hoverToggle(browser, videoID) { await prepareForToggleClick(browser, videoID); // Hover the mouse over the video to reveal the toggle. await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mouseover", }, browser ); info("Checking toggle policy"); await assertTogglePolicy(browser, videoID, null); let toggleClientRect = await getToggleClientRect(browser, videoID); info("Hovering the toggle rect now."); let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2; let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2; await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mouseover", }, browser ); } /** * This tests that the original DTD string is shown for the PiP toggle */ add_task(async function test_experiment_control() { await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { const s = `<!DOCTYPE bindings [ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> %videocontrolsDTD; ]> <html xmlns=\"http://www.w3.org/1999/xhtml\"> <head> <meta charset=\"utf-8\"/> <div class="pip-message">&pictureInPictureExplainer;</div> </head> </html>`; const parser = new DOMParser(); parser.forceEnableDTD(); let doc = parser.parseFromString(s, "application/xhtml+xml"); const pipDTDMessage = doc.querySelector(".pip-message").innerHTML.trim(); await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn(browser, [pipDTDMessage], async function( pipDTDMessage ) { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipButton = shadowRoot.querySelector(".pip-explainer"); Assert.equal( pipButton.textContent.trim(), pipDTDMessage, "The PiP explainer is default" ); }); } ); }); /** * This tests that the experiment is showing the icon only */ add_task(async function test_experiment_iconOnly() { let experimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ featureId: "pictureinpicture", value: { showIconOnly: true, }, }); registerCleanupFunction(async function() { await experimentCleanup(); }); await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn(browser, [], async function() { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipExpanded = shadowRoot.querySelector(".pip-expanded"); let pipIcon = shadowRoot.querySelector("div.pip-icon"); Assert.ok( ContentTaskUtils.is_hidden(pipExpanded), "The PiP explainer hidden by the experiment" ); Assert.ok( ContentTaskUtils.is_visible(pipIcon), "The PiP icon is visible by the experiment" ); }); } ); }); Loading
toolkit/actors/PictureInPictureChild.jsm +30 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,10 @@ XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", }); XPCOMUtils.defineLazyModuleGetters(this, { NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm", }); XPCOMUtils.defineLazyPreferenceGetter( this, "DISPLAY_TEXT_TRACKS_PREF", Loading Loading @@ -930,6 +934,24 @@ class PictureInPictureToggleChild extends JSWindowActorChild { toggle.removeAttribute("policy"); } const nimbusExperimentVariables = NimbusFeatures.pictureinpicture.getAllVariables( { defaultValues: { title: null, message: false, showIconOnly: false } } ); // nimbusExperimentVariables will be defaultValues when the experiment is disabled if (nimbusExperimentVariables.title && nimbusExperimentVariables.message) { let pipExplainer = shadowRoot.querySelector(".pip-explainer"); let pipLabel = shadowRoot.querySelector(".pip-label"); pipExplainer.innerText = nimbusExperimentVariables.message; pipLabel.innerText = nimbusExperimentVariables.title; } else if (nimbusExperimentVariables.showIconOnly) { // We only want to show the PiP icon in this experiment scenario let pipExpanded = shadowRoot.querySelector(".pip-expanded"); pipExpanded.style.display = "none"; let pipIcon = shadowRoot.querySelector(".pip-icon"); pipIcon.style.display = "block"; } controlsOverlay.removeAttribute("hidetoggle"); // The hideToggleDeferredTask we create here is for automatically hiding Loading Loading @@ -966,6 +988,14 @@ class PictureInPictureToggleChild extends JSWindowActorChild { !toggle.hasAttribute("hidden") ) { Services.telemetry.scalarAdd("pictureinpicture.saw_toggle", 1); // only record if this is the first time seeing the toggle if ( !Services.prefs.getBoolPref( "media.videocontrols.picture-in-picture.video-toggle.has-used" ) ) { NimbusFeatures.pictureinpicture.recordExposureEvent(); } } // Now that we're hovering the video, we'll check to see if we're Loading
toolkit/components/nimbus/FeatureManifest.yaml +14 −0 Original line number Diff line number Diff line Loading @@ -473,3 +473,17 @@ pbNewtab: schema: >- browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json variables: {} pictureinpicture: description: Message for first time Picture-in-Picture users hasExposure: true exposureDescription: Exposure is sent when a user hovers over a video and Picture-in-Picture has not been used before variables: title: type: string description: The title to be used for the PiP toggle message: type: string description: The message to be used in the PiP toggle showIconOnly: type: boolean description: Whether to show the first time PiP toggle or show the PiP icon only
toolkit/components/pictureinpicture/tests/browser.ini +2 −0 Original line number Diff line number Diff line Loading @@ -61,6 +61,8 @@ support-files = skip-if = debug os == 'linux' && bits == 64 && !debug # Bug 1549875 [browser_nimbusMessageFirstTimePip.js] [browser_nimbusShowIconOnly.js] [browser_noPlayerControlsOnMiddleRightClick.js] [browser_noToggleOnAudio.js] [browser_playerControls.js] Loading
toolkit/components/pictureinpicture/tests/browser_nimbusMessageFirstTimePip.js 0 → 100644 +178 −0 Original line number Diff line number Diff line /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ExperimentFakes } = ChromeUtils.import( "resource://testing-common/NimbusTestUtils.jsm" ); const PIP_EXPERIMENT_MESSAGE = "Hello world message"; const PIP_EXPERIMENT_TITLE = "Hello world title"; /** * This function will hover over the middle of the video and then * hover over the toggle to reveal the first time PiP message * @param browser The current browser * @param videoID The video element id */ async function hoverToggle(browser, videoID) { await prepareForToggleClick(browser, videoID); // Hover the mouse over the video to reveal the toggle. await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mouseover", }, browser ); info("Checking toggle policy"); await assertTogglePolicy(browser, videoID, null); let toggleClientRect = await getToggleClientRect(browser, videoID); info("Hovering the toggle rect now."); let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2; let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2; await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mouseover", }, browser ); } /** * This tests that the original DTD string is shown for the PiP toggle */ add_task(async function test_experiment_control() { await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { const s = `<!DOCTYPE bindings [ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> %videocontrolsDTD; ]> <html xmlns=\"http://www.w3.org/1999/xhtml\"> <head> <meta charset=\"utf-8\"/> <div class="pip-message">&pictureInPictureExplainer;</div> </head> </html>`; const parser = new DOMParser(); parser.forceEnableDTD(); let doc = parser.parseFromString(s, "application/xhtml+xml"); const pipDTDMessage = doc.querySelector(".pip-message").innerHTML.trim(); await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn(browser, [pipDTDMessage], async function( pipDTDMessage ) { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipButton = shadowRoot.querySelector(".pip-explainer"); Assert.equal( pipButton.textContent.trim(), pipDTDMessage, "The PiP explainer is default" ); }); } ); }); /** * This tests that the experiment message is shown for the PiP toggle */ add_task(async function test_experiment_message() { let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ featureId: "pictureinpicture", value: { title: PIP_EXPERIMENT_TITLE, message: PIP_EXPERIMENT_MESSAGE, }, }); registerCleanupFunction(async function() { await doExperimentCleanup(); }); await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn( browser, [PIP_EXPERIMENT_MESSAGE, PIP_EXPERIMENT_TITLE], async function(PIP_EXPERIMENT_MESSAGE, PIP_EXPERIMENT_TITLE) { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipExplainer = shadowRoot.querySelector(".pip-explainer"); let pipLabel = shadowRoot.querySelector(".pip-label"); Assert.equal( pipExplainer.textContent.trim(), PIP_EXPERIMENT_MESSAGE, "The PiP explainer is being overridden by the experiment" ); Assert.equal( pipLabel.textContent.trim(), PIP_EXPERIMENT_TITLE, "The PiP label is being overridden by the experiment" ); } ); } ); });
toolkit/components/pictureinpicture/tests/browser_nimbusShowIconOnly.js 0 → 100644 +171 −0 Original line number Diff line number Diff line /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ExperimentFakes } = ChromeUtils.import( "resource://testing-common/NimbusTestUtils.jsm" ); const PIP_EXPERIMENT_MESSAGE = "Hello world message"; const PIP_EXPERIMENT_TITLE = "Hello world title"; /** * This function will hover over the middle of the video and then * hover over the toggle to reveal the first time PiP message * @param browser The current browser * @param videoID The video element id */ async function hoverToggle(browser, videoID) { await prepareForToggleClick(browser, videoID); // Hover the mouse over the video to reveal the toggle. await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtCenter( `#${videoID}`, { type: "mouseover", }, browser ); info("Checking toggle policy"); await assertTogglePolicy(browser, videoID, null); let toggleClientRect = await getToggleClientRect(browser, videoID); info("Hovering the toggle rect now."); let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2; let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2; await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mousemove", }, browser ); await BrowserTestUtils.synthesizeMouseAtPoint( toggleCenterX, toggleCenterY, { type: "mouseover", }, browser ); } /** * This tests that the original DTD string is shown for the PiP toggle */ add_task(async function test_experiment_control() { await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { const s = `<!DOCTYPE bindings [ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> %videocontrolsDTD; ]> <html xmlns=\"http://www.w3.org/1999/xhtml\"> <head> <meta charset=\"utf-8\"/> <div class="pip-message">&pictureInPictureExplainer;</div> </head> </html>`; const parser = new DOMParser(); parser.forceEnableDTD(); let doc = parser.parseFromString(s, "application/xhtml+xml"); const pipDTDMessage = doc.querySelector(".pip-message").innerHTML.trim(); await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn(browser, [pipDTDMessage], async function( pipDTDMessage ) { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipButton = shadowRoot.querySelector(".pip-explainer"); Assert.equal( pipButton.textContent.trim(), pipDTDMessage, "The PiP explainer is default" ); }); } ); }); /** * This tests that the experiment is showing the icon only */ add_task(async function test_experiment_iconOnly() { let experimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ featureId: "pictureinpicture", value: { showIconOnly: true, }, }); registerCleanupFunction(async function() { await experimentCleanup(); }); await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PAGE, }, async browser => { await SimpleTest.promiseFocus(browser); await ensureVideosReady(browser); const PIP_PREF = "media.videocontrols.picture-in-picture.video-toggle.has-used"; await SpecialPowers.pushPrefEnv({ set: [[PIP_PREF, false]], }); let videoID = "with-controls"; await hoverToggle(browser, videoID); await SpecialPowers.spawn(browser, [], async function() { let video = content.document.getElementById("with-controls"); let shadowRoot = video.openOrClosedShadowRoot; let pipExpanded = shadowRoot.querySelector(".pip-expanded"); let pipIcon = shadowRoot.querySelector("div.pip-icon"); Assert.ok( ContentTaskUtils.is_hidden(pipExpanded), "The PiP explainer hidden by the experiment" ); Assert.ok( ContentTaskUtils.is_visible(pipIcon), "The PiP icon is visible by the experiment" ); }); } ); });