зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
20994bdb01
Коммит
0bf254ca30
|
@ -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
|
Загрузка…
Ссылка в новой задаче