Skip to content

Commit

Permalink
feat: show mute toggle button if the tech supports muting volume (#5052)
Browse files Browse the repository at this point in the history
Currently, VideoJS combines volume control with muted support, and these actions aren't actually the same. Muting/unmuting volume work independently from the volume control. For example, iOS doesn't support controlling volume programmatically but allows muting/unmuting volume.

This change will display the volume control panel and mute toggle button if the tech supports muting volume. The volume slider will continue to be hidden if the platform doesn't allow programmatically control volume. If neither muting nor control volume is supported, volume panel will not be displayed.

Fixes #4478.
  • Loading branch information
bcdarius authored and gkatsev committed Jul 9, 2018
1 parent e630e26 commit 89f6397
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 7 deletions.
3 changes: 3 additions & 0 deletions src/css/components/_volume.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@

@include transition(width 0.1s);
}
&.vjs-mute-toggle-only {
width: 4em;
}
}

@include transition(width 1s);
Expand Down
12 changes: 10 additions & 2 deletions src/js/control-bar/mute-toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import Button from '../button';
import Component from '../component';
import * as Dom from '../utils/dom.js';
import checkVolumeSupport from './volume-control/check-volume-support';
import checkMuteSupport from './volume-control/check-mute-support';
import * as browser from '../utils/browser.js';

/**
* A button component for muting the audio.
Expand All @@ -26,7 +27,7 @@ class MuteToggle extends Button {
super(player, options);

// hide this control if volume support is missing
checkVolumeSupport(this, player);
checkMuteSupport(this, player);

this.on(player, ['loadstart', 'volumechange'], this.update);
}
Expand Down Expand Up @@ -97,6 +98,13 @@ class MuteToggle extends Button {
const vol = this.player_.volume();
let level = 3;

// in iOS when a player is loaded with muted attribute
// and volume is changed with a native mute button
// we want to make sure muted state is updated
if (browser.IS_IOS) {
this.player_.muted(this.player_.tech_.el_.muted);
}

if (vol === 0 || this.player_.muted()) {
level = 0;
} else if (vol < 0.33) {
Expand Down
28 changes: 28 additions & 0 deletions src/js/control-bar/volume-control/check-mute-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Check if muting volume is supported and if it isn't hide the mute toggle
* button.
*
* @param {Component} self
* A reference to the mute toggle button
*
* @param {Player} player
* A reference to the player
*
* @private
*/
const checkMuteSupport = function(self, player) {
// hide mute toggle button if it's not supported by the current tech
if (player.tech_ && !player.tech_.featuresMuteControl) {
self.addClass('vjs-hidden');
}

self.on(player, 'loadstart', function() {
if (!player.tech_.featuresMuteControl) {
self.addClass('vjs-hidden');
} else {
self.removeClass('vjs-hidden');
}
});
};

export default checkMuteSupport;
25 changes: 22 additions & 3 deletions src/js/control-bar/volume-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @file volume-control.js
*/
import Component from '../component.js';
import checkVolumeSupport from './volume-control/check-volume-support';
import {isPlain} from '../utils/obj';

// Required children
Expand Down Expand Up @@ -42,8 +41,7 @@ class VolumePanel extends Component {

super(player, options);

// hide this control if volume support is missing
checkVolumeSupport(this, player);
this.on(player, ['loadstart'], this.volumePanelState_);

// while the slider is active (the mouse has been pressed down and
// is dragging) we do not want to hide the VolumeBar
Expand Down Expand Up @@ -72,6 +70,27 @@ class VolumePanel extends Component {
this.removeClass('vjs-slider-active');
}

/**
* Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
* depending on MuteToggle and VolumeControl state
*
* @listens Player#loadstart
* @private
*/
volumePanelState_() {
// hide volume panel if neither volume control or mute toggle
// are displayed
if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
this.addClass('vjs-hidden');
}

// if only mute toggle is visible we don't want
// volume panel expanding when hovered or active
if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
this.addClass('vjs-mute-toggle-only');
}
}

/**
* Create the `Component`'s DOM element
*
Expand Down
35 changes: 35 additions & 0 deletions src/js/tech/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,33 @@ Html5.canControlVolume = function() {
}
};

/**
* Check if the volume can be muted in this browser/device.
* Some devices, e.g. iOS, don't allow changing volume
* but permits muting/unmuting.
*
* @return {bolean}
* - True if volume can be muted
* - False otherwise
*/
Html5.canMuteVolume = function() {
try {
const muted = Html5.TEST_VID.muted;

// in some versions of iOS muted property doesn't always
// work, so we want to set both property and attribute
Html5.TEST_VID.muted = !muted;
if (Html5.TEST_VID.muted) {
Dom.setAttribute(Html5.TEST_VID, 'muted', 'muted');
} else {
Dom.removeAttribute(Html5.TEST_VID, 'muted', 'muted');
}
return muted !== Html5.TEST_VID.muted;
} catch (e) {
return false;
}
};

/**
* Check if the playback rate can be changed in this browser/device.
*
Expand Down Expand Up @@ -1015,6 +1042,14 @@ Html5.Events = [
*/
Html5.prototype.featuresVolumeControl = Html5.canControlVolume();

/**
* Boolean indicating whether the `Tech` supports muting volume.
*
* @type {bolean}
* @default {@link Html5.canMuteVolume}
*/
Html5.prototype.featuresMuteControl = Html5.canMuteVolume();

/**
* Boolean indicating whether the `Tech` supports changing the speed at which the media
* plays. Examples:
Expand Down
10 changes: 9 additions & 1 deletion src/js/tech/tech.js
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,15 @@ TRACK_TYPES.ALL.names.forEach(function(name) {
Tech.prototype.featuresVolumeControl = true;

/**
* Boolean indicating wether the `Tech` support fullscreen resize control.
* Boolean indicating whether the `Tech` supports muting volume.
*
* @type {bolean}
* @default
*/
Tech.prototype.featuresMuteControl = true;

/**
* Boolean indicating whether the `Tech` supports fullscreen resize control.
* Resizing plugins using request fullscreen reloads the plugin
*
* @type {boolean}
Expand Down
6 changes: 5 additions & 1 deletion test/unit/controls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ QUnit.module('Controls', {
}
});

QUnit.test('should hide volume control if it\'s not supported', function(assert) {
QUnit.test('should hide volume and mute toggle control if it\'s not supported', function(assert) {
assert.expect(2);

const player = TestHelpers.makePlayer();

player.tech_.featuresVolumeControl = false;
player.tech_.featuresMuteControl = false;

const volumeControl = new VolumeControl(player);
const muteToggle = new MuteToggle(player);
Expand All @@ -39,6 +40,7 @@ QUnit.test('should test and toggle volume control on `loadstart`', function(asse
const player = TestHelpers.makePlayer();

player.tech_.featuresVolumeControl = true;
player.tech_.featuresMuteControl = true;

const volumeControl = new VolumeControl(player);
const muteToggle = new MuteToggle(player);
Expand All @@ -47,12 +49,14 @@ QUnit.test('should test and toggle volume control on `loadstart`', function(asse
assert.equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle is hidden initially');

player.tech_.featuresVolumeControl = false;
player.tech_.featuresMuteControl = false;
player.trigger('loadstart');

assert.equal(volumeControl.hasClass('vjs-hidden'), true, 'volumeControl does not hide itself');
assert.equal(muteToggle.hasClass('vjs-hidden'), true, 'muteToggle does not hide itself');

player.tech_.featuresVolumeControl = true;
player.tech_.featuresMuteControl = true;
player.trigger('loadstart');

assert.equal(volumeControl.hasClass('vjs-hidden'), false, 'volumeControl does not show itself');
Expand Down

0 comments on commit 89f6397

Please sign in to comment.