Bug 1748884 - add PIP support for WebVTT supported videos r=mtigley,mhowell,niklas

This patch integrates WebVTT support for Picture in Picture that can be enabled/disabled via a pref.

Differential Revision: https://phabricator.services.mozilla.com/D135576
This commit is contained in:
Katherine Patenio 2022-02-02 19:31:54 +00:00
Родитель 20994bdb01
Коммит 0bf254ca30
10 изменённых файлов: 926 добавлений и 1 удалений

Просмотреть файл

@ -409,6 +409,7 @@ pref("media.decoder-doctor.verbose", false);
pref("media.decoder-doctor.new-issue-endpoint", "https://webcompat.com/issues/new");
pref("media.videocontrols.picture-in-picture.enabled", false);
pref("media.videocontrols.picture-in-picture.display-text-tracks.enabled", false);
pref("media.videocontrols.picture-in-picture.video-toggle.enabled", false);
pref("media.videocontrols.picture-in-picture.video-toggle.always-show", false);
pref("media.videocontrols.picture-in-picture.video-toggle.min-video-secs", 45);

Просмотреть файл

@ -46,6 +46,7 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/ContentDOMReference.jsm"
);
const { WebVTT } = ChromeUtils.import("resource://gre/modules/vtt.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
@ -53,6 +54,12 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"DISPLAY_TEXT_TRACKS_PREF",
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
false
);
const TOGGLE_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.enabled";
const TOGGLE_TESTING_PREF =
@ -1131,6 +1138,132 @@ class PictureInPictureChild extends JSWindowActorChild {
// A weak reference to this PiP window's content window
weakPlayerContent = null;
// A reference to current WebVTT track currently displayed on the content window
_currentWebVTTTrack = null;
observerFunction = null;
observe(subject, topic, data) {
if (
topic != "nsPref:changed" ||
data !==
"media.videocontrols.picture-in-picture.display-text-tracks.enabled"
) {
return;
}
const originatingVideo = this.getWeakVideo();
let isTextTrackPrefEnabled = Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.display-text-tracks.enabled"
);
// Enable or disable text track support
if (isTextTrackPrefEnabled) {
this.setupTextTracks(originatingVideo);
} else {
this.removeTextTracks(originatingVideo);
}
}
/**
* Sets up Picture-in-Picture to support displaying text tracks from WebVTT
*
* If the originating video supports WebVTT, try to read the
* active track and cues. Display any active cues on the pip window
* right away if applicable.
*
* @param originatingVideo {Element|null}
* The <video> being displayed in Picture-in-Picture mode, or null if that <video> no longer exists.
*/
setupTextTracks(originatingVideo) {
const isWebVTTSupported = !!originatingVideo.textTracks?.length;
if (!isWebVTTSupported) {
return;
}
// Verify active track for originating video
this.setActiveTextTrack(originatingVideo.textTracks);
if (!this._currentWebVTTTrack) {
return;
}
// Listen for changes in tracks and active cues
originatingVideo.textTracks.addEventListener("change", this);
this._currentWebVTTTrack.addEventListener("cuechange", this.onCueChange);
const cues = this._currentWebVTTTrack.activeCues;
this.updateWebVTTTextTracksDisplay(cues);
}
/**
* Removes existing text tracks on the Picture in Picture window.
*
* If the originating video supports WebVTT, clear references to active
* tracks and cues. No longer listen for any track or cue changes.
*
* @param originatingVideo {Element|null}
* The <video> being displayed in Picture-in-Picture mode, or null if that <video> no longer exists.
*/
removeTextTracks(originatingVideo) {
const isWebVTTSupported = !!originatingVideo.textTracks;
if (!isWebVTTSupported) {
return;
}
// No longer listen for changes to tracks and active cues
originatingVideo.textTracks.removeEventListener("change", this);
this._currentWebVTTTrack?.removeEventListener(
"cuechange",
this.onCueChange
);
this._currentWebVTTTrack = null;
this.updateWebVTTTextTracksDisplay(null);
}
/**
* Updates the text content for the container that holds and displays text tracks
* on the pip window.
* @param textTrackCues {TextTrackCueList|null}
* Collection of TextTrackCue objects containing text displayed, or null if there is no cue to display.
*/
updateWebVTTTextTracksDisplay(textTrackCues) {
let pipWindowTracksContainer = this.document.getElementById("texttracks");
let playerVideo = this.document.getElementById("playervideo");
let playerVideoWindow = playerVideo.ownerGlobal;
// To prevent overlap with previous cues, clear all text from the pip window
pipWindowTracksContainer.replaceChildren();
if (!textTrackCues) {
return;
}
const allCuesArray = [...textTrackCues];
let lineNumberUsed = allCuesArray.find(cue => cue.line !== "auto");
// If VTTCue.line is not set to "auto", simplying reading textTrackCues does
// not guarantee that text tracks are displayed in their intended order. In this case,
// sort the cues according to line number.
if (lineNumberUsed) {
allCuesArray.sort((cue1, cue2) => cue1.line - cue2.line);
}
// Parse through WebVTT cue using vtt.js to ensure
// semantic markup like <b> and <i> tags are rendered.
allCuesArray.forEach(cue => {
let text = cue.text;
let cueTextNode = WebVTT.convertCueToDOMTree(playerVideoWindow, text);
let cueDiv = this.document.createElement("div");
cueDiv.appendChild(cueTextNode);
// Whitespaces are usually collapsed. Set to pre-wrap
// so that newlines are rendered.
cueDiv.style = "white-space: pre;";
pipWindowTracksContainer.appendChild(cueDiv);
});
}
/**
* Returns a reference to the PiP's <video> element being displayed in Picture-in-Picture
* mode.
@ -1248,6 +1381,36 @@ class PictureInPictureChild extends JSWindowActorChild {
}
break;
}
case "change": {
// Clear currently stored track data (webvtt support) before reading
// a new track.
if (this._currentWebVTTTrack) {
this._currentWebVTTTrack.removeEventListener(
"cuechange",
this.onCueChange
);
this._currentWebVTTTrack = null;
}
const tracks = event.target;
this.setActiveTextTrack(tracks);
const isCurrentTrackAvailable = this._currentWebVTTTrack;
// If tracks are disabled or invalid while change occurs,
// remove text tracks from the pip window and stop here.
if (!isCurrentTrackAvailable || !tracks.length) {
this.updateWebVTTTextTracksDisplay(null);
return;
}
this._currentWebVTTTrack.addEventListener(
"cuechange",
this.onCueChange
);
const cues = this._currentWebVTTTrack.activeCues;
this.updateWebVTTTextTracksDisplay(cues);
break;
}
}
}
@ -1327,12 +1490,38 @@ class PictureInPictureChild extends JSWindowActorChild {
}
}
/**
* Updates this._currentWebVTTTrack if an active track is found
* for the originating video.
* @param {TextTrackList} textTrackList list of text tracks
*/
setActiveTextTrack(textTrackList) {
this._currentWebVTTTrack = null;
for (let i = 0; i < textTrackList.length; i++) {
let track = textTrackList[i];
let isCCText = track.kind === "subtitles" || track.kind === "captions";
if (isCCText && track.mode === "showing") {
this._currentWebVTTTrack = track;
break;
}
}
}
/**
* Keeps an eye on the originating video's document. If it ever
* goes away, this will cause the Picture-in-Picture window for any
* of its content to go away as well.
*/
trackOriginatingVideo(originatingVideo) {
this.observerFunction = (subject, topic, data) => {
this.observe(subject, topic, data);
};
Services.prefs.addObserver(
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
this.observerFunction
);
let originatingWindow = originatingVideo.ownerGlobal;
if (originatingWindow) {
originatingWindow.addEventListener("pagehide", this);
@ -1341,6 +1530,10 @@ class PictureInPictureChild extends JSWindowActorChild {
originatingVideo.addEventListener("volumechange", this);
originatingVideo.addEventListener("resize", this);
if (DISPLAY_TEXT_TRACKS_PREF) {
this.setupTextTracks(originatingVideo);
}
let chromeEventHandler = originatingWindow.docShell.chromeEventHandler;
chromeEventHandler.addEventListener(
"MozDOMFullscreen:Request",
@ -1362,6 +1555,11 @@ class PictureInPictureChild extends JSWindowActorChild {
* window's document unloads.
*/
untrackOriginatingVideo(originatingVideo) {
Services.prefs.removeObserver(
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
this.observerFunction
);
let originatingWindow = originatingVideo.ownerGlobal;
if (originatingWindow) {
originatingWindow.removeEventListener("pagehide", this);
@ -1370,6 +1568,10 @@ class PictureInPictureChild extends JSWindowActorChild {
originatingVideo.removeEventListener("volumechange", this);
originatingVideo.removeEventListener("resize", this);
if (DISPLAY_TEXT_TRACKS_PREF) {
this.removeTextTracks(originatingVideo);
}
let chromeEventHandler = originatingWindow.docShell.chromeEventHandler;
chromeEventHandler.removeEventListener(
"MozDOMFullscreen:Request",
@ -1463,6 +1665,8 @@ class PictureInPictureChild extends JSWindowActorChild {
let doc = this.document;
let playerVideo = doc.createElement("video");
playerVideo.id = "playervideo";
let textTracks = doc.createElement("div");
doc.body.style.overflow = "hidden";
doc.body.style.margin = "0";
@ -1470,10 +1674,22 @@ class PictureInPictureChild extends JSWindowActorChild {
// Force the player video to assume maximum height and width of the
// containing window
playerVideo.style.height = "100vh";
playerVideo.style.width = "100vw";
playerVideo.style.backgroundColor = "#000";
// Load text tracks container in the content process so that
// we can load text tracks without having to constantly
// access the parent process.
textTracks.id = "texttracks";
// TODO: responsive styling. Hardcoded values until design spec confirmed.
textTracks.style.position = "absolute";
textTracks.style.textAlign = "center";
textTracks.style.width = "100vw";
textTracks.style.bottom = "30px";
textTracks.style.backgroundColor = "black";
textTracks.style.color = "white";
doc.body.appendChild(playerVideo);
doc.body.appendChild(textTracks);
originatingVideo.cloneElementVisually(playerVideo);
@ -1483,6 +1699,7 @@ class PictureInPictureChild extends JSWindowActorChild {
playerVideo.style.transform = "scaleX(-1)";
}
this.onCueChange = this.onCueChange.bind(this);
this.trackOriginatingVideo(originatingVideo);
this.contentWindow.addEventListener(
@ -1527,6 +1744,15 @@ class PictureInPictureChild extends JSWindowActorChild {
}
}
onCueChange(e) {
if (!DISPLAY_TEXT_TRACKS_PREF) {
this.updateWebVTTTextTracksDisplay(null);
} else {
const cues = this._currentWebVTTTrack.activeCues;
this.updateWebVTTTextTracksDisplay(cues);
}
}
/**
* This checks if a given keybinding has been disabled for the specific site
* currently being viewed.

Просмотреть файл

@ -7,6 +7,7 @@ support-files =
test-page.html
test-page-with-iframe.html
test-page-with-sound.html
test-page-with-webvtt.html
test-pointer-events-none.html
test-transparent-overlay-1.html
test-transparent-overlay-2.html
@ -17,11 +18,15 @@ support-files =
test-video-cropped.mp4
test-video-vertical.mp4
test-video-long.mp4
test-webvtt-1.vtt
test-webvtt-2.vtt
test-webvtt-3.vtt
short.mp4
../../../../dom/media/test/gizmo.mp4
../../../../dom/media/test/owl.mp3
prefs =
media.videocontrols.picture-in-picture.display-text-tracks.enabled=false
media.videocontrols.picture-in-picture.enabled=true
media.videocontrols.picture-in-picture.video-toggle.enabled=true
media.videocontrols.picture-in-picture.video-toggle.testing=true
@ -77,6 +82,8 @@ skip-if = os == "win" && bits == 64 && debug # Bug 1683002
[browser_stripVideoStyles.js]
[browser_tabIconOverlayPiP.js]
[browser_telemetry_togglePiP.js]
[browser_text_tracks_webvtt_1.js]
[browser_text_tracks_webvtt_2.js]
[browser_thirdPartyIframe.js]
[browser_toggleButtonOnNanDuration.js]
skip-if =

Просмотреть файл

@ -0,0 +1,168 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Initializes videos and text tracks for the current test case.
* First track is the default track to be loaded onto the video.
* Once initialization is done, play then pause the requested video.
* so that text tracks are loaded.
* @param {Element} browser The <xul:browser> hosting the <video>
* @param {String} videoID The ID of the video being checked
* @param {Integer} defaultTrackIndex The index of the track to be loaded, or none if -1
*/
async function prepareVideosAndTracks(browser, videoID, defaultTrackIndex = 0) {
info("Preparing video and initial text tracks");
await ensureVideosReady(browser);
await SpecialPowers.spawn(
browser,
[{ videoID, defaultTrackIndex }],
async args => {
let video = content.document.getElementById(args.videoID);
let tracks = video.textTracks;
is(tracks.length, 3, "Number of tracks loaded should be 3");
// Enable track for originating video
if (args.defaultTrackIndex >= 0) {
info(`Loading track ${args.defaultTrackIndex + 1}`);
let track = tracks[args.defaultTrackIndex];
tracks.mode = "showing";
track.mode = "showing";
}
// Briefly play the video to load text tracks onto the pip window.
info("Playing video to load text tracks");
video.play();
info("Pausing video");
video.pause();
ok(video.paused, "Video should be paused before proceeding with test");
}
);
}
/**
* This test ensures that text tracks shown on the source video
* do not appear on a newly created pip window if the pref
* is disabled.
*/
add_task(async function test_text_tracks_new_window_pref_disabled() {
info("Running test: new window - pref disabled");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
false,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
ok(
!textTracks.textContent,
"Text tracks should not be visible on the pip window"
);
});
}
);
});
/**
* This test ensures that text tracks shown on the source video
* appear on a newly created pip window if the pref is enabled.
*/
add_task(async function test_text_tracks_new_window_pref_enabled() {
info("Running test: new window - pref enabled");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
});
}
);
});
/**
* This test ensures that text tracks do not appear on a new pip window
* if no track is loaded and the pref is enabled.
*/
add_task(async function test_text_tracks_new_window_no_track() {
info("Running test: new window - no track");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID, -1);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
ok(
!textTracks.textContent,
"Text tracks should not be visible on the pip window"
);
});
}
);
});

Просмотреть файл

@ -0,0 +1,443 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Initializes videos and text tracks for the current test case.
* First track is the default track to be loaded onto the video.
* Once initialization is done, play then pause the requested video.
* so that text tracks are loaded.
* @param {Element} browser The <xul:browser> hosting the <video>
* @param {String} videoID The ID of the video being checked
* @param {Integer} defaultTrackIndex The index of the track to be loaded, or none if -1
*/
async function prepareVideosAndTracks(browser, videoID, defaultTrackIndex = 0) {
info("Preparing video and initial text tracks");
await ensureVideosReady(browser);
await SpecialPowers.spawn(
browser,
[{ videoID, defaultTrackIndex }],
async args => {
let video = content.document.getElementById(args.videoID);
let tracks = video.textTracks;
is(tracks.length, 3, "Number of tracks loaded should be 3");
// Enable track for originating video
if (args.defaultTrackIndex >= 0) {
info(`Loading track ${args.defaultTrackIndex + 1}`);
let track = tracks[args.defaultTrackIndex];
tracks.mode = "showing";
track.mode = "showing";
}
// Briefly play the video to load text tracks onto the pip window.
info("Playing video to load text tracks");
video.play();
info("Pausing video");
video.pause();
ok(video.paused, "Video should be paused before proceeding with test");
}
);
}
/**
* Plays originating video until the next cue is loaded.
* Once the next cue is loaded, pause the video.
* @param {Element} browser The <xul:browser> hosting the <video>
* @param {String} videoID The ID of the video being checked
* @param {Integer} textTrackIndex The index of the track to be loaded, or none if -1
*/
async function waitForNextCue(browser, videoID, textTrackIndex = 0) {
if (textTrackIndex < 0) {
ok(false, "Cannot wait for next cue with invalid track index");
}
await SpecialPowers.spawn(
browser,
[{ videoID, textTrackIndex }],
async args => {
let video = content.document.getElementById(args.videoID);
info("Playing video to activate next cue");
video.play();
ok(!video.paused, "Video is playing");
info("Waiting until cuechange is called");
await ContentTaskUtils.waitForEvent(
video.textTracks[args.textTrackIndex],
"cuechange"
);
info("Pausing video to read text track");
video.pause();
ok(video.paused, "Video is paused");
}
);
}
/**
* This test ensures that text tracks disappear from the pip window
* when the pref is disabled.
*/
add_task(async function test_text_tracks_existing_window_pref_disabled() {
info("Running test: existing window - pref disabled");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
});
info("Turning off pref");
await SpecialPowers.popPrefEnv();
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
false,
],
],
});
// Verify that cue is no longer on the pip window
info("Checking that cue is no longer on pip window");
await SpecialPowers.spawn(pipBrowser, [], async () => {
let textTracks = content.document.getElementById("texttracks");
await ContentTaskUtils.waitForCondition(() => {
return !textTracks.textContent;
}, `Text track is still visible on the pip window. Got ${textTracks.textContent}`);
info("Successfully removed text tracks from pip window");
});
}
);
});
/**
* This test ensures that text tracks shown on the source video
* window appear on an existing pip window when the pref is enabled.
*/
add_task(async function test_text_tracks_existing_window_pref_enabled() {
info("Running test: existing window - pref enabled");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
false,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
ok(
!textTracks.textContent,
"Text tracks should not be visible on the pip window"
);
});
info("Turning on pref");
await SpecialPowers.popPrefEnv();
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
info("Checking that cue is on pip window");
await SpecialPowers.spawn(pipBrowser, [], async () => {
let textTracks = content.document.getElementById("texttracks");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
info("Successfully displayed text tracks on pip window");
});
}
);
});
/**
* This test ensures that text tracks update to the correct track
* when a new track is selected.
*/
add_task(async function test_text_tracks_existing_window_new_track() {
info("Running test: existing window - new track");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
ok(
textTracks.textContent.includes("track 1"),
"Track 1 should be loaded"
);
});
// Change track in the content window
await SpecialPowers.spawn(browser, [videoID], async videoID => {
let video = content.document.getElementById(videoID);
let tracks = video.textTracks;
info("Changing to a new track");
let track1 = tracks[0];
track1.mode = "disabled";
let track2 = tracks[1];
track2.mode = "showing";
});
// Ensure new track is loaded
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking new text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
ok(
textTracks.textContent.includes("track 2"),
"Track 2 should be loaded"
);
});
}
);
});
/**
* This test ensures that text tracks are correctly updated.
*/
add_task(async function test_text_tracks_existing_window_cues() {
info("Running test: existing window - cues");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
// Verify that first cue appears
info("Checking first cue on pip window");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
ok(
textTracks.textContent.includes("cue 1"),
`Expected text should be displayed on the pip window. Got ${textTracks.textContent}.`
);
});
// Play video to move to the next cue
await waitForNextCue(browser, videoID);
// Test remaining cues
await SpecialPowers.spawn(pipBrowser, [], async () => {
let textTracks = content.document.getElementById("texttracks");
// Verify that empty cue makes text disappear
info("Checking empty cue in pip window");
ok(
!textTracks.textContent,
"Text track should not be visible on the pip window"
);
});
await waitForNextCue(browser, videoID);
await SpecialPowers.spawn(pipBrowser, [], async () => {
let textTracks = content.document.getElementById("texttracks");
// Verify that second cue appears
info("Checking second cue in pip window");
ok(
textTracks.textContent.includes("cue 2"),
"Text track second cue should be visible on the pip window"
);
});
}
);
});
/**
* This test ensures that text tracks disappear if no track is selected.
*/
add_task(async function test_text_tracks_existing_window_no_track() {
info("Running test: existing window - no track");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
ok(textTracks, "TextTracks container should exist in the pip window");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
});
// Remove track in the content window
await SpecialPowers.spawn(browser, [videoID], async videoID => {
let video = content.document.getElementById(videoID);
let tracks = video.textTracks;
info("Removing tracks");
let track1 = tracks[0];
track1.mode = "disabled";
let track2 = tracks[1];
track2.mode = "disabled";
});
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking that text track disappears from pip window");
let textTracks = content.document.getElementById("texttracks");
await ContentTaskUtils.waitForCondition(() => {
return !textTracks.textContent;
}, `Text track is still visible on the pip window. Got ${textTracks.textContent}`);
});
}
);
});
/**
* This test ensures that text tracks appear correctly if there are multiple active cues.
*/
add_task(async function test_text_tracks_existing_window_multi_cue() {
info("Running test: existing window - multi cue");
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await prepareVideosAndTracks(browser, videoID, 2);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let pipBrowser = pipWin.document.getElementById("browser");
await SpecialPowers.spawn(pipBrowser, [], async () => {
info("Checking text track content in pip window");
let textTracks = content.document.getElementById("texttracks");
// Verify multiple active cues
ok(textTracks, "TextTracks container should exist in the pip window");
await ContentTaskUtils.waitForCondition(() => {
return textTracks.textContent;
}, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`);
is(textTracks.children.length, 2, "Text tracks should load 2 cues");
});
}
);
});

Просмотреть файл

@ -21,6 +21,7 @@ const TEST_PAGE_WITH_IFRAME = TEST_ROOT_2 + "test-page-with-iframe.html";
const TEST_PAGE_WITH_SOUND = TEST_ROOT + "test-page-with-sound.html";
const TEST_PAGE_WITH_NAN_VIDEO_DURATION =
TEST_ROOT + "test-page-with-nan-video-duration.html";
const TEST_PAGE_WITH_WEBVTT = TEST_ROOT + "test-page-with-webvtt.html";
const WINDOW_TYPE = "Toolkit:PictureInPicture";
const TOGGLE_POSITION_PREF =
"media.videocontrols.picture-in-picture.video-toggle.position";

Просмотреть файл

@ -0,0 +1,50 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Picture-in-Picture tests</title>
<script type="text/javascript" src="click-event-helper.js"></script>
</head>
<style>
video {
display: block;
border: 1px solid black;
}
</style>
<body>
<h1>Video with controls</h1>
<video id="with-controls" src="test-video-long.mp4" controls loop="true" width="400" height="225">
<track
id="track1"
kind="captions"
label="[test] en"
srclang="en"
src="test-webvtt-1.vtt"
/>
<track
id="track2"
kind="subtitles"
label="[test] fr"
srclang="fr"
src="test-webvtt-2.vtt"
/>
<track
id="track3"
kind="subtitles"
label="[test] eo"
srclang="eo"
src="test-webvtt-3.vtt"
/>
</video>
<script>
function fireEvents() {
for (let videoID of ["with-controls"]) {
let video = document.getElementById(videoID);
let event = new CustomEvent("MozTogglePictureInPicture", { bubbles: true });
video.dispatchEvent(event);
}
}
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,10 @@
WEBVTT
1
00:00:00.000 --> 00:00:01.000
track 1 - cue 1
2
00:00:02.000 --> 00:00:03.000
- <b>track 1 - cue 2 bold</b>
- <i>track 1 - cue 2 italicized<i>

Просмотреть файл

@ -0,0 +1,10 @@
WEBVTT
1
00:00:00.000 --> 00:00:01.000
track 2 - cue 1
2
00:00:02.000 --> 00:00:03.000
- <b>track 2 - cue 2 bold</b>
- <i>track 2 - cue 2 italicized<i>

Просмотреть файл

@ -0,0 +1,9 @@
WEBVTT
1
00:00:00.000 --> 00:00:01.000
track 3 - cue 1
2
00:00:00.000 --> 00:00:01.000
track 3 - cue 2