Bug 1580095 - Add audio scrubber to PiP. r=pip-reviewers,desktop-theme-reviewers,kpatenio,dao

Differential Revision: https://phabricator.services.mozilla.com/D179556
This commit is contained in:
Niklas Baumgardner 2023-06-17 17:01:58 +00:00
parent 78718dabb9
commit f7058e9858
8 changed files with 457 additions and 38 deletions

View File

@ -122,7 +122,7 @@ add_task(async function test_volume_change_with_keyboard() {
// Decrease volume with arrow down // Decrease volume with arrow down
EventUtils.synthesizeKey("KEY_ArrowDown", {}, pipWin); EventUtils.synthesizeKey("KEY_ArrowDown", {}, pipWin);
ok(!(await isVideoMuted(browser, videoID)), "The audio is playing."); ok(await isVideoMuted(browser, videoID), "The audio is not playing.");
// Increase volume with arrow up // Increase volume with arrow up
EventUtils.synthesizeKey("KEY_ArrowUp", {}, pipWin); EventUtils.synthesizeKey("KEY_ArrowUp", {}, pipWin);

View File

@ -224,6 +224,7 @@ export class PictureInPictureLauncherChild extends JSWindowActorChild {
webVTTSubtitles: !!video.textTracks?.length, webVTTSubtitles: !!video.textTracks?.length,
scrubberPosition, scrubberPosition,
timestamp, timestamp,
volume: PictureInPictureChild.videoWrapper.getVolume(video),
}); });
let args = { let args = {
@ -1939,6 +1940,9 @@ export class PictureInPictureChild extends JSWindowActorChild {
} else { } else {
this.sendAsyncMessage("PictureInPicture:Unmuting"); this.sendAsyncMessage("PictureInPicture:Unmuting");
} }
this.sendAsyncMessage("PictureInPicture:VolumeChange", {
volume: this.videoWrapper.getVolume(video),
});
break; break;
} }
case "resize": { case "resize": {
@ -2143,6 +2147,12 @@ export class PictureInPictureChild extends JSWindowActorChild {
this.setVideoTime(scrubberPosition, wasPlaying); this.setVideoTime(scrubberPosition, wasPlaying);
break; break;
} }
case "PictureInPicture:SetVolume": {
const { volume } = message.data;
let video = this.getWeakVideo();
this.videoWrapper.setVolume(video, volume);
break;
}
} }
} }
@ -2565,12 +2575,16 @@ export class PictureInPictureChild extends JSWindowActorChild {
this.closePictureInPicture({ reason: "closePlayerShortcut" }); this.closePictureInPicture({ reason: "closePlayerShortcut" });
break; break;
case "downArrow" /* Volume decrease */: case "downArrow" /* Volume decrease */:
if (this.isKeyDisabled(lazy.KEYBOARD_CONTROLS.VOLUME)) { if (
this.isKeyDisabled(lazy.KEYBOARD_CONTROLS.VOLUME) ||
this.videoWrapper.isMuted(video)
) {
return; return;
} }
oldval = this.videoWrapper.getVolume(video); oldval = this.videoWrapper.getVolume(video);
this.videoWrapper.setVolume(video, oldval < 0.1 ? 0 : oldval - 0.1); newval = oldval < 0.1 ? 0 : oldval - 0.1;
this.videoWrapper.setMuted(video, false); this.videoWrapper.setVolume(video, newval);
this.videoWrapper.setMuted(video, newval === 0);
break; break;
case "upArrow" /* Volume increase */: case "upArrow" /* Volume increase */:
if (this.isKeyDisabled(lazy.KEYBOARD_CONTROLS.VOLUME)) { if (this.isKeyDisabled(lazy.KEYBOARD_CONTROLS.VOLUME)) {

View File

@ -186,6 +186,12 @@ export class PictureInPictureParent extends JSWindowActorParent {
player.setScrubberPosition(scrubberPosition); player.setScrubberPosition(scrubberPosition);
break; break;
} }
case "PictureInPicture:VolumeChange": {
let { volume } = aMessage.data;
let player = PictureInPicture.getWeakPipPlayer(this);
player.setVolume(volume);
break;
}
} }
} }
} }
@ -815,6 +821,7 @@ export var PictureInPicture = {
win.setScrubberPosition(videoData.scrubberPosition); win.setScrubberPosition(videoData.scrubberPosition);
win.setTimestamp(videoData.timestamp); win.setTimestamp(videoData.timestamp);
win.setVolume(videoData.volume);
Services.prefs.setBoolPref(TOGGLE_HAS_USED_PREF, true); Services.prefs.setBoolPref(TOGGLE_HAS_USED_PREF, true);

View File

@ -115,6 +115,10 @@ function setTimestamp(timeString) {
Player.setTimestamp(timeString); Player.setTimestamp(timeString);
} }
function setVolume(volume) {
Player.setVolume(volume);
}
/** /**
* The Player object handles initializing the player, holds state, and handles * The Player object handles initializing the player, holds state, and handles
* events for updating state. * events for updating state.
@ -236,6 +240,19 @@ let Player = {
this.handleScrubbingDone(event); this.handleScrubbingDone(event);
}); });
this.audioScrubber.addEventListener("input", event => {
this.audioScrubbing = true;
this.handleAudioScrubbing(event.target.value);
});
this.audioScrubber.addEventListener("change", event => {
this.audioScrubbing = false;
});
this.audioScrubber.addEventListener("pointerdown", event => {
if (this.isMuted) {
this.audioScrubber.max = 1;
}
});
for (let radio of document.querySelectorAll( for (let radio of document.querySelectorAll(
'input[type=radio][name="cc-size"]' 'input[type=radio][name="cc-size"]'
)) { )) {
@ -259,6 +276,9 @@ let Player = {
if (Services.prefs.getBoolPref(AUDIO_TOGGLE_ENABLED_PREF, false)) { if (Services.prefs.getBoolPref(AUDIO_TOGGLE_ENABLED_PREF, false)) {
const audioButton = document.getElementById("audio"); const audioButton = document.getElementById("audio");
audioButton.hidden = false; audioButton.hidden = false;
const audioScrubber = document.getElementById("audio-scrubber");
audioScrubber.hidden = false;
} }
if (Services.prefs.getBoolPref(CAPTIONS_ENABLED_PREF, false)) { if (Services.prefs.getBoolPref(CAPTIONS_ENABLED_PREF, false)) {
@ -489,7 +509,8 @@ let Player = {
handleScrubbing(event) { handleScrubbing(event) {
// When using the keyboard to scrub, we get both a keydown and an input // When using the keyboard to scrub, we get both a keydown and an input
// event. The input event is fired after the keydown and we have already // event. The input event is fired after the keydown and we have already
// handle the keydown event in onKeyDown and we don't want to handle it twice // handled the keydown event in onKeyDown so we set preventNextInputEvent
// to true in onKeyDown as to not set the current time twice.
if (this.preventNextInputEvent) { if (this.preventNextInputEvent) {
this.preventNextInputEvent = false; this.preventNextInputEvent = false;
return; return;
@ -522,6 +543,36 @@ let Player = {
this.scrubbing = false; this.scrubbing = false;
}, },
/**
* Set the volume on the video and unmute if the video was muted.
* If the volume is changed via the keyboard, onKeyDown will set
* this.preventNextInputEvent to true.
* @param {Number} volume A number between 0 and 1 that represents the volume
*/
handleAudioScrubbing(volume) {
// When using the keyboard to adjust the volume, we get both a keydown and
// an input event. The input event is fired after the keydown event and we
// have already handled the keydown event in onKeyDown so we set
// preventNextInputEvent to true in onKeyDown as to not set the volume twice.
if (this.preventNextInputEvent) {
this.preventNextInputEvent = false;
return;
}
if (this.isMuted) {
this.isMuted = false;
this.actor.sendAsyncMessage("PictureInPicture:Unmute");
}
if (volume == 0) {
this.actor.sendAsyncMessage("PictureInPicture:Mute");
}
this.actor.sendAsyncMessage("PictureInPicture:SetVolume", {
volume,
});
},
getScrubberPositionFromEvent(event) { getScrubberPositionFromEvent(event) {
return event.target.value; return event.target.value;
}, },
@ -549,6 +600,14 @@ let Player = {
this.timestamp.hidden = timestamp === undefined; this.timestamp.hidden = timestamp === undefined;
}, },
setVolume(volume) {
if (volume < Number.EPSILON) {
this.actor.sendAsyncMessage("PictureInPicture:Mute");
}
this.audioScrubber.value = volume;
},
closePipWindow(closeData) { closePipWindow(closeData) {
// Set the subtitles font size prefs // Set the subtitles font size prefs
Services.prefs.setBoolPref( Services.prefs.setBoolPref(
@ -577,11 +636,7 @@ let Player = {
onClick(event) { onClick(event) {
switch (event.target.id) { switch (event.target.id) {
case "audio": { case "audio": {
if (this.isMuted) { this.toggleMute();
this.actor.sendAsyncMessage("PictureInPicture:Unmute");
} else {
this.actor.sendAsyncMessage("PictureInPicture:Mute");
}
break; break;
} }
@ -717,6 +772,20 @@ let Player = {
} }
}, },
/**
* Toggle the mute state of the video
*/
toggleMute() {
if (this.isMuted) {
// We unmute in handleAudioScrubbing so no need to also do it here
this.audioScrubber.max = 1;
this.handleAudioScrubbing(this.lastVolume ?? 1);
} else {
this.lastVolume = this.audioScrubber.value;
this.actor.sendAsyncMessage("PictureInPicture:Mute");
}
},
resizeToVideo(rect) { resizeToVideo(rect) {
if (this.isFullscreen) { if (this.isFullscreen) {
// We store the size and position because resizing the PiP window // We store the size and position because resizing the PiP window
@ -749,7 +818,7 @@ let Player = {
}; };
// If the up or down arrow is pressed while the scrubber is focused then we // If the up or down arrow is pressed while the scrubber is focused then we
// want to hijack these keydown events to act as left or right arrow // want to hijack these keydown events to act as left or right arrows
// respectively to correctly seek the video. // respectively to correctly seek the video.
if ( if (
event.target.id === "scrubber" && event.target.id === "scrubber" &&
@ -763,17 +832,33 @@ let Player = {
eventKeys.keyCode = window.KeyEvent.DOM_VK_LEFT; eventKeys.keyCode = window.KeyEvent.DOM_VK_LEFT;
} }
// If the keydown event was one of the arrow keys and the scrubber was // If the left or right arrow is pressed while the audio scrubber is focused
// focused then we will also get an input event that will overwrite the // then we want to hijack these keydown events to act as up or down arrows
// keydown event if we dont' prevent the input event. // respectively to correctly change the volume.
if ( if (
event.target.id === "scrubber" && event.target.id === "audio-scrubber" &&
event.keyCode === window.KeyEvent.DOM_VK_RIGHT
) {
eventKeys.keyCode = window.KeyEvent.DOM_VK_UP;
} else if (
event.target.id === "audio-scrubber" &&
event.keyCode === window.KeyEvent.DOM_VK_LEFT
) {
eventKeys.keyCode = window.KeyEvent.DOM_VK_DOWN;
}
// If the keydown event was one of the arrow keys and the scrubber or the
// audio scrubber was focused then we want to prevent the subsequent input
// event from overwriting the keydown event.
if (
event.target.id === "audio-scrubber" ||
(event.target.id === "scrubber" &&
[ [
window.KeyEvent.DOM_VK_LEFT, window.KeyEvent.DOM_VK_LEFT,
window.KeyEvent.DOM_VK_RIGHT, window.KeyEvent.DOM_VK_RIGHT,
window.KeyEvent.DOM_VK_UP, window.KeyEvent.DOM_VK_UP,
window.KeyEvent.DOM_VK_DOWN, window.KeyEvent.DOM_VK_DOWN,
].includes(event.keyCode) ].includes(event.keyCode))
) { ) {
this.preventNextInputEvent = true; this.preventNextInputEvent = true;
} }
@ -1075,6 +1160,11 @@ let Player = {
return (this.scrubber = document.getElementById("scrubber")); return (this.scrubber = document.getElementById("scrubber"));
}, },
get audioScrubber() {
delete this.audioScrubber;
return (this.audioScrubber = document.getElementById("audio-scrubber"));
},
get timestamp() { get timestamp() {
delete this.timestamp; delete this.timestamp;
return (this.timestamp = document.getElementById("timestamp")); return (this.timestamp = document.getElementById("timestamp"));
@ -1143,6 +1233,11 @@ let Player = {
set isMuted(isMuted) { set isMuted(isMuted) {
this._isMuted = isMuted; this._isMuted = isMuted;
if (!isMuted) {
this.audioScrubber.max = 1;
} else if (!this.audioScrubbing) {
this.audioScrubber.max = 0;
}
this.controls.classList.toggle("muted", isMuted); this.controls.classList.toggle("muted", isMuted);
let strId = isMuted let strId = isMuted
? `pictureinpicture-unmute-btn` ? `pictureinpicture-unmute-btn`

View File

@ -45,25 +45,25 @@
class="control-item control-button tooltip-under-controls" data-l10n-attrs="tooltip" class="control-item control-button tooltip-under-controls" data-l10n-attrs="tooltip"
#ifdef XP_MACOSX #ifdef XP_MACOSX
mac="true" mac="true"
tabindex="8"
#else
tabindex="9" tabindex="9"
#else
tabindex="10"
#endif #endif
/> />
<button id="unpip" <button id="unpip"
class="control-item control-button tooltip-under-controls" data-l10n-id="pictureinpicture-unpip-btn" data-l10n-attrs="tooltip" class="control-item control-button tooltip-under-controls" data-l10n-id="pictureinpicture-unpip-btn" data-l10n-attrs="tooltip"
#ifdef XP_MACOSX #ifdef XP_MACOSX
mac="true" mac="true"
tabindex="9" tabindex="10"
#else #else
tabindex="8" tabindex="9"
#endif #endif
/> />
<div id="controls-bottom-gradient" class="control-item"></div> <div id="controls-bottom-gradient" class="control-item"></div>
<div id="controls-bottom"> <div id="controls-bottom">
<div class="controls-bottom-upper"> <div class="controls-bottom-upper">
<div class="scrubber-no-drag"> <div class="scrubber-no-drag">
<input id="scrubber" class="control-item" min="0" max="1" step=".001" type="range" tabindex="10" hidden="true"/> <input id="scrubber" class="control-item" min="0" max="1" step=".001" type="range" tabindex="11" hidden="true"/>
</div> </div>
</div> </div>
<div class="controls-bottom-lower"> <div class="controls-bottom-lower">
@ -71,21 +71,22 @@
<div id="timestamp" class="control-item" hidden="true"></div> <div id="timestamp" class="control-item" hidden="true"></div>
</div> </div>
<div class="center-controls"> <div class="center-controls">
<button id="seekBackward" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-id="pictureinpicture-seekbackward-btn" data-l10n-attrs="tooltip" tabindex="11"></button> <button id="seekBackward" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-id="pictureinpicture-seekbackward-btn" data-l10n-attrs="tooltip" tabindex="12"></button>
<button id="playpause" class="control-item control-button tooltip-over-controls center-tooltip" tabindex="1" <button id="playpause" class="control-item control-button tooltip-over-controls center-tooltip" tabindex="1"
data-l10n-id="pictureinpicture-pause-btn" data-l10n-attrs="tooltip"/> data-l10n-id="pictureinpicture-pause-btn" data-l10n-attrs="tooltip"/>
<button id="seekForward" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-id="pictureinpicture-seekforward-btn" data-l10n-attrs="tooltip" tabindex="2"></button> <button id="seekForward" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-id="pictureinpicture-seekforward-btn" data-l10n-attrs="tooltip" tabindex="2"></button>
</div> </div>
<div class="end-controls"> <div class="end-controls">
<button id="audio" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-attrs="tooltip" tabindex="3"/> <button id="audio" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-attrs="tooltip" tabindex="3"/>
<button id="closed-caption" class="control-item control-button tooltip-over-controls center-tooltip closed-caption" hidden="true" disabled="true" data-l10n-id="pictureinpicture-subtitles-btn" data-l10n-attrs="tooltip" tabindex="4"></button> <input id="audio-scrubber" class="control-item" min="0" max="1" step=".001" type="range" tabindex="4" hidden="true"/>
<button id="closed-caption" class="control-item control-button tooltip-over-controls center-tooltip closed-caption" hidden="true" disabled="true" data-l10n-id="pictureinpicture-subtitles-btn" data-l10n-attrs="tooltip" tabindex="5"></button>
<div id="settings" class="hide panel"> <div id="settings" class="hide panel">
<fieldset class="box panel-fieldset"> <fieldset class="box panel-fieldset">
<legend class="a11y-only panel-legend" data-l10n-id="pictureinpicture-subtitles-panel-accessible"></legend> <legend class="a11y-only panel-legend" data-l10n-id="pictureinpicture-subtitles-panel-accessible"></legend>
<div class="subtitle-grid"> <div class="subtitle-grid">
<label id="subtitles-toggle-label" data-l10n-id="pictureinpicture-subtitles-label" class="bold" for="subtitles-toggle"></label> <label id="subtitles-toggle-label" data-l10n-id="pictureinpicture-subtitles-label" class="bold" for="subtitles-toggle"></label>
<label class="switch"> <label class="switch">
<input id="subtitles-toggle" type="checkbox" tabindex="5" checked=""/> <input id="subtitles-toggle" type="checkbox" tabindex="6" checked=""/>
<span class="slider" role="presentation"></span> <span class="slider" role="presentation"></span>
</label> </label>
</div> </div>
@ -93,22 +94,22 @@
<fieldset class="font-size-selection panel-fieldset"> <fieldset class="font-size-selection panel-fieldset">
<legend data-l10n-id="pictureinpicture-font-size-label" class="bold panel-legend"></legend> <legend data-l10n-id="pictureinpicture-font-size-label" class="bold panel-legend"></legend>
<div id="font-size-selection-radio-small" class="font-size-selection-radio"> <div id="font-size-selection-radio-small" class="font-size-selection-radio">
<input id="small" type="radio" name="cc-size" tabindex="6"/> <input id="small" type="radio" name="cc-size" tabindex="7"/>
<label data-l10n-id="pictureinpicture-font-size-small" for="small"></label> <label data-l10n-id="pictureinpicture-font-size-small" for="small"></label>
</div> </div>
<div id="font-size-selection-radio-medium" class="font-size-selection-radio"> <div id="font-size-selection-radio-medium" class="font-size-selection-radio">
<input id="medium" type="radio" name="cc-size" tabindex="6"/> <input id="medium" type="radio" name="cc-size" tabindex="7"/>
<label data-l10n-id="pictureinpicture-font-size-medium" for="medium"></label> <label data-l10n-id="pictureinpicture-font-size-medium" for="medium"></label>
</div> </div>
<div id="font-size-selection-radio-large" class="font-size-selection-radio"> <div id="font-size-selection-radio-large" class="font-size-selection-radio">
<input id="large" type="radio" name="cc-size" tabindex="6"/> <input id="large" type="radio" name="cc-size" tabindex="7"/>
<label data-l10n-id="pictureinpicture-font-size-large" for="large"></label> <label data-l10n-id="pictureinpicture-font-size-large" for="large"></label>
</div> </div>
</fieldset> </fieldset>
</fieldset> </fieldset>
<div class="arrow"></div> <div class="arrow"></div>
</div> </div>
<button id="fullscreen" class="control-item control-button tooltip-over-controls inline-end-tooltip" hidden="true" data-l10n-attrs="tooltip" tabindex="7"></button> <button id="fullscreen" class="control-item control-button tooltip-over-controls inline-end-tooltip" hidden="true" data-l10n-attrs="tooltip" tabindex="8"></button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -44,6 +44,7 @@ prefs =
[browser_aaa_run_first_firstTimePiPToggleEvents.js] [browser_aaa_run_first_firstTimePiPToggleEvents.js]
[browser_aaa_telemetry_togglePiP.js] [browser_aaa_telemetry_togglePiP.js]
[browser_audioScrubber.js]
[browser_backgroundTab.js] [browser_backgroundTab.js]
[browser_cannotTriggerFromContent.js] [browser_cannotTriggerFromContent.js]
[browser_changePiPSrcInFullscreen.js] [browser_changePiPSrcInFullscreen.js]

View File

@ -0,0 +1,258 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const IMPROVED_CONTROLS_ENABLED_PREF =
"media.videocontrols.picture-in-picture.improved-video-controls.enabled";
const videoID = "with-controls";
async function getVideoVolume(browser, videoID) {
return SpecialPowers.spawn(browser, [videoID], async videoID => {
return content.document.getElementById(videoID).volume;
});
}
async function getVideoMuted(browser, videoID) {
return SpecialPowers.spawn(browser, [videoID], async videoID => {
return content.document.getElementById(videoID).muted;
});
}
add_task(async function test_audioScrubber() {
await SpecialPowers.pushPrefEnv({
set: [[IMPROVED_CONTROLS_ENABLED_PREF, true]],
});
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE,
gBrowser,
},
async browser => {
await ensureVideosReady(browser);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
// Resize the PiP window so we know the audio scrubber is visible
let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
let ratio = pipWin.innerWidth * pipWin.innerHeight;
pipWin.resizeTo(750 * ratio, 750);
await resizePromise;
let audioScrubber = pipWin.document.getElementById("audio-scrubber");
audioScrubber.focus();
// Volume should be 1 and video should not be muted when opening this video
let actualVolume = await getVideoVolume(browser, videoID);
let expectedVolume = 1;
let actualMuted = await getVideoMuted(browser, videoID);
isfuzzy(
actualVolume,
expectedVolume,
Number.EPSILON,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
ok(!actualMuted, "Video is not muted");
let volumeChangePromise = BrowserTestUtils.waitForContentEvent(
browser,
"volumechange",
true
);
// Test that left arrow key changes volume by -0.1
EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin);
await volumeChangePromise;
actualVolume = await getVideoVolume(browser, videoID);
expectedVolume = 0.9;
isfuzzy(
actualVolume,
expectedVolume,
Number.EPSILON,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
// Test that right arrow key changes volume by +0.1 and does not exceed 1
EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin);
EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin);
EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin);
actualVolume = await getVideoVolume(browser, videoID);
expectedVolume = 1;
isfuzzy(
actualVolume,
expectedVolume,
Number.EPSILON,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
// Test that the mouse can move the audio scrubber to 0.5
let rect = audioScrubber.getBoundingClientRect();
volumeChangePromise = BrowserTestUtils.waitForContentEvent(
browser,
"volumechange",
true
);
EventUtils.synthesizeMouse(
audioScrubber,
rect.width / 2,
rect.height / 2,
{},
pipWin
);
await volumeChangePromise;
actualVolume = await getVideoVolume(browser, videoID);
expectedVolume = 0.5;
isfuzzy(
actualVolume,
expectedVolume,
0.01,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
// Test muting and unmuting the video
let muteButton = pipWin.document.getElementById("audio");
volumeChangePromise = BrowserTestUtils.waitForContentEvent(
browser,
"volumechange",
true
);
muteButton.click();
await volumeChangePromise;
ok(
await getVideoMuted(browser, videoID),
"The video is muted aftering clicking the mute button"
);
is(
audioScrubber.max,
"0",
"The max of the audio scrubber is 0, so the volume slider appears that the volume is 0"
);
volumeChangePromise = BrowserTestUtils.waitForContentEvent(
browser,
"volumechange",
true
);
muteButton.click();
await volumeChangePromise;
ok(
!(await getVideoMuted(browser, videoID)),
"The video is muted aftering clicking the mute button"
);
isfuzzy(
actualVolume,
expectedVolume,
0.01,
`The volume is still ${actualVolume}.`
);
volumeChangePromise = BrowserTestUtils.waitForContentEvent(
browser,
"volumechange",
true
);
// Test that the audio scrubber can be dragged from 0.5 to 0 and the video gets muted
EventUtils.synthesizeMouse(
audioScrubber,
rect.width / 2,
rect.height / 2,
{ type: "mousedown" },
pipWin
);
EventUtils.synthesizeMouse(
audioScrubber,
0,
rect.height / 2,
{ type: "mousemove" },
pipWin
);
EventUtils.synthesizeMouse(
audioScrubber,
0,
rect.height / 2,
{ type: "mouseup" },
pipWin
);
await volumeChangePromise;
actualVolume = await getVideoVolume(browser, videoID);
expectedVolume = 0;
isfuzzy(
actualVolume,
expectedVolume,
Number.EPSILON,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
ok(
await getVideoMuted(browser, videoID),
"Video is now muted because slider was moved to 0"
);
// Test that the left arrow key does not unmute the video and the volume remains at 0
EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin);
EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin);
actualVolume = await getVideoVolume(browser, videoID);
isfuzzy(
actualVolume,
expectedVolume,
Number.EPSILON,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
ok(
await getVideoMuted(browser, videoID),
"Video is now muted because slider is still at 0"
);
volumeChangePromise = BrowserTestUtils.waitForContentEvent(
browser,
"volumechange",
true
);
// Test that the right arrow key will increase the volume by +0.1 and will unmute the video
EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin);
await volumeChangePromise;
actualVolume = await getVideoVolume(browser, videoID);
expectedVolume = 0.1;
isfuzzy(
actualVolume,
expectedVolume,
Number.EPSILON,
`The actual volume ${actualVolume}. The expected volume ${expectedVolume}`
);
ok(
!(await getVideoMuted(browser, videoID)),
"Video is no longer muted because we moved the slider"
);
}
);
});

View File

@ -104,8 +104,8 @@ browser {
.end-controls { .end-controls {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: 1fr 2fr 1fr 1fr;
grid-template-areas: "audio closedcaption fullscreen"; grid-template-areas: "audio audio-scrubber closedcaption fullscreen";
justify-self: end; justify-self: end;
gap: 8px; gap: 8px;
} }
@ -253,8 +253,7 @@ body:fullscreen #controls[showing]:hover .control-item:not(:hover),
/* Background gradient is the only control item with a fixed opacity value. */ /* Background gradient is the only control item with a fixed opacity value. */
#controls[keying] .control-item#controls-bottom-gradient, #controls[keying] .control-item#controls-bottom-gradient,
#controls[showing] .control-item#controls-bottom-gradient, #controls[showing] .control-item#controls-bottom-gradient,
#controls:hover .control-item#controls-bottom-gradient:hover #controls:hover .control-item#controls-bottom-gradient:hover #controls[donthide] .control-item#controls-bottom-gradient {
#controls[donthide] .control-item#controls-bottom-gradient {
opacity: 0.8; opacity: 0.8;
} }
@ -306,6 +305,39 @@ body:fullscreen #controls[showing]:hover {
grid-area: audio; grid-area: audio;
} }
#audio-scrubber {
grid-area: audio-scrubber;
width: 64px;
background-color: transparent;
padding: 15px 0;
margin: 0;
}
#audio-scrubber::-moz-range-thumb {
border-radius: 8px;
background-color: #FFFFFF;
position: relative;
width: 8px;
height: 8px;
bottom: 24px;
padding: 0;
}
#audio-scrubber::-moz-range-track {
background-color: #969696;
}
#audio-scrubber::-moz-range-progress {
background-color: #FFFFFF;
}
#audio-scrubber,
#audio-scrubber::-moz-range-track,
#audio-scrubber::-moz-range-progress {
height: 2px;
border-radius: 4px;
}
#fullscreen { #fullscreen {
grid-area: fullscreen; grid-area: fullscreen;
} }
@ -386,7 +418,7 @@ body:not(:fullscreen) #fullscreen {
font-weight: 590; font-weight: 590;
} }
.box > input[type="radio"]{ .box > input[type="radio"] {
background-color: red; background-color: red;
fill: currentColor; fill: currentColor;
} }
@ -591,6 +623,17 @@ input:checked + .slider::before {
content: unset; content: unset;
} }
@media (width <= 630px) {
#audio-scrubber {
display: none;
}
.end-controls {
grid-template-columns: repeat(3, 1fr);
grid-template-areas: "audio closedcaption fullscreen";
}
}
@media (height <= 325px), (width <= 475px) { @media (height <= 325px), (width <= 475px) {
#closed-caption { #closed-caption {
display: none; display: none;