1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-02 06:32:07 +02:00

feat: Combine captions and subtitles tracks control (#4028)

This commit is contained in:
mister-ben 2017-03-02 14:23:41 -06:00 committed by Gary Katsevman
parent f95815bcd0
commit 74eb5d4772
10 changed files with 289 additions and 30 deletions

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<title>Video.js Sandbox</title>
<!-- Add ES5 shim and sham for IE8 -->
<script src="../build/temp/ie8/videojs-ie8.js"></script>
<!-- Load the source files -->
<link href="../build/temp/video-js.css" rel="stylesheet" type="text/css">
<script src="../build/temp/video.js"></script>
<script src="../node_modules/videojs-flash/dist/videojs-flash.js"></script>
<!-- Set the location of the flash SWF -->
<script>
videojs.options.flash.swf = '../build/temp/video-js.swf';
</script>
</head>
<body>
<div style="background-color:#eee; border: 1px solid #777; padding: 10px; margin-bottom: 20px; font-size: .8em; line-height: 1.5em; font-family: Verdana, sans-serif;">
<p>You can use /sandbox/ for writing and testing your own code. Nothing in /sandbox/ will get checked into the repo, except files that end in .example (so don't edit or add those files). To get started make a copy of index.html.example and rename it to index.html.</p>
<pre>cp sandbox/index.html.example sandbox/index.html</pre>
<pre>npm run start</pre>
<pre>open http://localhost:9999/sandbox/index.html</pre>
</div>
<video id="vid1" class="video-js vjs-default-skin" lang="en" controls preload="auto" width="640" height="264" poster="http://vjs.zencdn.net/v/oceans.png">
<source src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.mp4" type="video/mp4">
<source src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.ogg" type="video/ogg">
<track kind="captions" src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/captions.en.vtt" srclang="en" label="English"></track><!-- Tracks need an ending tag thanks to IE9 -->
<track kind="subtitles" src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/captions.ar.vtt" srclang="ar" label="Arabic"></track><!-- Tracks need an ending tag thanks to IE9 -->
<track kind="subtitles" src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/captions.sv.vtt" srclang="sv" label="Swedish"></track><!-- Tracks need an ending tag thanks to IE9 -->
<track kind="subtitles" src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/captions.ru.vtt" srclang="ru" label="Russian"></track><!-- Tracks need an ending tag thanks to IE9 -->
<track kind="subtitles" src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/captions.ja.vtt" srclang="ja" label="Japanese"></track><!-- Tracks need an ending tag thanks to IE9 -->
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>
<p>This player has the captions-only CpationsButton, the subtiles-only subtitlesButton and the subsCapsButton which shows both kinds. Typically you'll use either just the subsCapsButton alone, or one or both of the captionsButton and subtitlesButton.</p>
<script>
var vid = document.getElementById("vid1");
var player = videojs(vid, {
controlBar: {
children: [
'playToggle',
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'liveDisplay',
'remainingTimeDisplay',
'customControlSpacer',
'playbackRateMenuButton',
'chaptersButton',
'descriptionsButton',
'subtitlesButton',
'captionsButton',
'subsCapsButton',
'audioTrackButton',
'fullscreenToggle'
]
}
});
console.log(player.language());
</script>
</body>
</html>

View File

@ -0,0 +1,22 @@
// North America uses 'CC' icon
.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,
.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder {
@extend .vjs-icon-captions;
}
// ROW uses 'subtitles'
// Double selector because @extend puts these rules above the captions icon
.video-js .vjs-subs-caps-button .vjs-icon-placeholder,
.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,
.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,
.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,
.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder {
@extend .vjs-icon-subtitles;
}
.video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text:after {
content: " \f10d";
font-family: VideoJS;
vertical-align: bottom;
font-size: 1.5em;
}

View File

@ -36,6 +36,7 @@
@import "components/chapters";
@import "components/descriptions";
@import "components/subtitles";
@import "components/subs-caps";
@import "components/audio";
@import "components/adaptive";
@import "components/captions-settings";

View File

@ -17,6 +17,7 @@ import './text-track-controls/chapters-button.js';
import './text-track-controls/descriptions-button.js';
import './text-track-controls/subtitles-button.js';
import './text-track-controls/captions-button.js';
import './text-track-controls/subs-caps-button.js';
import './audio-track-controls/audio-track-button.js';
import './playback-rate-menu/playback-rate-menu-button.js';
import './spacer-controls/custom-control-spacer.js';
@ -66,8 +67,7 @@ ControlBar.prototype.options_ = {
'playbackRateMenuButton',
'chaptersButton',
'descriptionsButton',
'subtitlesButton',
'captionsButton',
'subsCapsButton',
'audioTrackButton',
'fullscreenToggle'
]

View File

@ -26,11 +26,21 @@ class OffTextTrackMenuItem extends TextTrackMenuItem {
options.track = {
player,
kind: options.kind,
label: options.kind + ' off',
kinds: options.kinds,
default: false,
mode: 'disabled'
};
if (!options.kinds) {
options.kinds = [options.kind];
}
if (options.label) {
options.track.label = options.label;
} else {
options.track.label = options.kinds.join(' and ') + ' off';
}
// MenuItem is selectable
options.selectable = true;
@ -51,7 +61,7 @@ class OffTextTrackMenuItem extends TextTrackMenuItem {
for (let i = 0, l = tracks.length; i < l; i++) {
const track = tracks[i];
if (track.kind === this.track.kind && track.mode === 'showing') {
if ((this.options_.kinds.indexOf(track.kind) > -1) && track.mode === 'showing') {
selected = false;
break;
}

View File

@ -0,0 +1,102 @@
/**
* @file sub-caps-button.js
*/
import TextTrackButton from './text-track-button.js';
import Component from '../../component.js';
import CaptionSettingsMenuItem from './caption-settings-menu-item.js';
import toTitleCase from '../../utils/to-title-case.js';
/**
* The button component for toggling and selecting captions and/or subtitles
*
* @extends TextTrackButton
*/
class SubsCapsButton extends TextTrackButton {
constructor(player, options = {}) {
super(player, options);
// Although North America uses "captions" in most cases for
// "captions and subtitles" other locales use "subtitles"
this.label_ = 'subtitles';
if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
this.label_ = 'captions';
}
this.menuButton_.controlText(toTitleCase(this.label_));
}
/**
* Builds the default DOM `className`.
*
* @return {string}
* The DOM `className` for this object.
*/
buildCSSClass() {
return `vjs-subs-caps-button ${super.buildCSSClass()}`;
}
/**
* Update caption menu items
*
* @param {EventTarget~Event} [event]
* The `addtrack` or `removetrack` event that caused this function to be
* called.
*
* @listens TextTrackList#addtrack
* @listens TextTrackList#removetrack
*/
update(event) {
let threshold = 2;
super.update();
// if native, then threshold is 1 because no settings button
if (this.player().tech_ && this.player().tech_.featuresNativeTextTracks) {
threshold = 1;
}
if (this.items && this.items.length > threshold) {
this.show();
} else {
this.hide();
}
}
/**
* Create caption/subtitles menu items
*
* @return {CaptionSettingsMenuItem[]}
* The array of current menu items.
*/
createItems() {
let items = [];
if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks)) {
items.push(new CaptionSettingsMenuItem(this.player_, {kind: this.label_}));
}
items = super.createItems(items);
return items;
}
}
/**
* `kind`s of TextTrack to look for to associate it with this menu.
*
* @type {array}
* @private
*/
SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
/**
* The text that should display over the `SubsCapsButton`s controls.
*
*
* @type {string}
* @private
*/
SubsCapsButton.prototype.controlText_ = 'Subtitles';
Component.registerComponent('SubsCapsButton', SubsCapsButton);
export default SubsCapsButton;

View File

@ -26,6 +26,10 @@ class TextTrackButton extends TrackButton {
options.tracks = player.textTracks();
super(player, options);
if (!Array.isArray(this.kinds_)) {
this.kinds_ = [this.kind_];
}
}
/**
@ -38,21 +42,36 @@ class TextTrackButton extends TrackButton {
* Array of menu items that were created
*/
createItems(items = []) {
// Label is an overide for the [track] off label
// USed to localise captions/subtitles
let label;
if (this.label_) {
label = `${this.label_} off`;
}
// Add an OFF menu item to turn all tracks off
items.push(new OffTextTrackMenuItem(this.player_, {kind: this.kind_}));
items.push(new OffTextTrackMenuItem(this.player_, {
kinds: this.kinds_,
kind: this.kind_,
label
}));
const tracks = this.player_.textTracks();
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
// only add tracks that are of the appropriate kind and have a label
if (track.kind === this.kind_) {
items.push(new TextTrackMenuItem(this.player_, {
// only add tracks that are of an appropriate kind and have a label
if (this.kinds_.indexOf(track.kind) > -1) {
const item = new TextTrackMenuItem(this.player_, {
track,
// MenuItem is selectable
selectable: true
}));
});
item.addClass(`vjs-${track.kind}-menu-item`);
items.push(item);
}
}

View File

@ -83,8 +83,13 @@ class TextTrackMenuItem extends MenuItem {
*/
handleClick(event) {
const kind = this.track.kind;
let kinds = this.track.kinds;
const tracks = this.player_.textTracks();
if (!kinds) {
kinds = [kind];
}
super.handleClick(event);
if (!tracks) {
@ -94,11 +99,7 @@ class TextTrackMenuItem extends MenuItem {
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
if (track.kind !== kind) {
continue;
}
if (track === this.track) {
if (track === this.track && (kinds.indexOf(track.kind) > -1)) {
track.mode = 'showing';
} else {
track.mode = 'disabled';

View File

@ -25,7 +25,7 @@ QUnit.test('should be displayed when text tracks list is not empty', function(as
this.clock.tick(1000);
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-hidden'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
'control is displayed');
assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
@ -37,7 +37,7 @@ QUnit.test('should be displayed when a text track is added to an empty track lis
player.addRemoteTextTrack(track, true);
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-hidden'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
'control is displayed');
assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
@ -47,7 +47,7 @@ QUnit.test('should be displayed when a text track is added to an empty track lis
QUnit.test('should not be displayed when text tracks list is empty', function(assert) {
const player = TestHelpers.makePlayer();
assert.ok(player.controlBar.captionsButton.hasClass('vjs-hidden'),
assert.ok(player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
'control is not displayed');
assert.equal(player.textTracks().length, 0, 'textTracks is empty');
@ -61,7 +61,7 @@ QUnit.test('should not be displayed when last text track is removed', function(a
player.removeRemoteTextTrack(player.textTracks()[0]);
assert.ok(player.controlBar.captionsButton.hasClass('vjs-hidden'),
assert.ok(player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
'control is not displayed');
assert.equal(player.textTracks().length, 0, 'textTracks is empty');
@ -70,12 +70,13 @@ QUnit.test('should not be displayed when last text track is removed', function(a
QUnit.test('menu should contain "Settings", "Off" and one track', function(assert) {
const player = TestHelpers.makePlayer({
language: 'en',
tracks: [track]
});
this.clock.tick(1000);
const menuItems = player.controlBar.captionsButton.items;
const menuItems = player.controlBar.subsCapsButton.items;
assert.equal(menuItems.length, 3, 'menu contains three items');
assert.equal(menuItems[0].track.label,
@ -87,6 +88,29 @@ QUnit.test('menu should contain "Settings", "Off" and one track', function(asser
player.dispose();
});
QUnit.test('menu should contain "Settings", "Off", one captions and one subtitles track', function(assert) {
const player = TestHelpers.makePlayer({
language: 'en',
tracks: [track, {
kind: 'subtitles',
label: 'test subs'
}]
});
this.clock.tick(1000);
const menuItems = player.controlBar.subsCapsButton.items;
assert.equal(menuItems.length, 4, 'menu contains three items');
assert.equal(menuItems[0].track.label,
'captions settings',
'menu contains "captions settings"');
assert.equal(menuItems[1].track.label, 'captions off', 'menu contains "captions off"');
assert.equal(menuItems[2].track.label, 'test', 'menu contains "test" track');
player.dispose();
});
QUnit.test('menu should update with addRemoteTextTrack', function(assert) {
const player = TestHelpers.makePlayer({
tracks: [track]
@ -96,7 +120,7 @@ QUnit.test('menu should update with addRemoteTextTrack', function(assert) {
player.addRemoteTextTrack(track, true);
assert.equal(player.controlBar.captionsButton.items.length,
assert.equal(player.controlBar.subsCapsButton.items.length,
4,
'menu does contain added track');
assert.equal(player.textTracks().length, 2, 'textTracks contains two items');
@ -113,7 +137,7 @@ QUnit.test('menu should update with removeRemoteTextTrack', function(assert) {
player.removeRemoteTextTrack(player.textTracks()[0]);
assert.equal(player.controlBar.captionsButton.items.length,
assert.equal(player.controlBar.subsCapsButton.items.length,
3,
'menu does not contain removed track');
assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
@ -203,13 +227,13 @@ QUnit.test('enabling a captions track should disable the descriptions menu butto
this.clock.tick(1000);
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-hidden'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
'captions control is displayed');
assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
'descriptions control is displayed');
assert.equal(player.textTracks().length, 2, 'textTracks contains two items');
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
'captions control is NOT disabled');
assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
'descriptions control is NOT disabled');
@ -225,7 +249,7 @@ QUnit.test('enabling a captions track should disable the descriptions menu butto
this.clock.tick(1000);
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
'captions control is NOT disabled');
assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
'descriptions control is NOT disabled');
@ -241,7 +265,7 @@ QUnit.test('enabling a captions track should disable the descriptions menu butto
this.clock.tick(1000);
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
'captions control is NOT disabled');
assert.ok(player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
'descriptions control IS disabled');
@ -257,7 +281,7 @@ QUnit.test('enabling a captions track should disable the descriptions menu butto
this.clock.tick(1000);
assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
assert.ok(!player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
'captions control is NOT disabled');
assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
'descriptions control is NOT disabled');

View File

@ -3,6 +3,7 @@ import ChaptersButton from '../../../src/js/control-bar/text-track-controls/chap
import DescriptionsButton from '../../../src/js/control-bar/text-track-controls/descriptions-button.js';
import SubtitlesButton from '../../../src/js/control-bar/text-track-controls/subtitles-button.js';
import CaptionsButton from '../../../src/js/control-bar/text-track-controls/captions-button.js';
import SubsCapsButton from '../../../src/js/control-bar/text-track-controls/subs-caps-button.js';
import TextTrack from '../../../src/js/tracks/text-track.js';
import TextTrackDisplay from '../../../src/js/tracks/text-track-display.js';
@ -130,6 +131,7 @@ QUnit.test('update texttrack buttons on removetrack or addtrack', function(asser
const oldSubsUpdate = SubtitlesButton.prototype.update;
const oldDescriptionsUpdate = DescriptionsButton.prototype.update;
const oldChaptersUpdate = ChaptersButton.prototype.update;
const oldSubsCapsUpdate = SubsCapsButton.prototype.update;
CaptionsButton.prototype.update = function() {
update++;
@ -147,6 +149,10 @@ QUnit.test('update texttrack buttons on removetrack or addtrack', function(asser
update++;
oldChaptersUpdate.call(this);
};
SubsCapsButton.prototype.update = function() {
update++;
oldSubsCapsUpdate.call(this);
};
Tech.prototype.featuresNativeTextTracks = true;
@ -182,29 +188,35 @@ QUnit.test('update texttrack buttons on removetrack or addtrack', function(asser
track2.src = '#es.vtt';
tag.appendChild(track2);
const player = TestHelpers.makePlayer({}, tag);
const player = TestHelpers.makePlayer({
controlBar: {
captionsButton: true,
subtitlesButton: true
}
}, tag);
player.player_ = player;
assert.equal(update, 4, 'update was called on the four buttons during init');
assert.equal(update, 5, 'update was called on the five buttons during init');
for (let i = 0; i < events.removetrack.length; i++) {
events.removetrack[i]();
}
assert.equal(update, 8, 'update was called on the four buttons for remove track');
assert.equal(update, 10, 'update was called on the five buttons for remove track');
for (let i = 0; i < events.addtrack.length; i++) {
events.addtrack[i]();
}
assert.equal(update, 12, 'update was called on the four buttons for remove track');
assert.equal(update, 15, 'update was called on the five buttons for remove track');
Tech.prototype.textTracks = oldTextTracks;
Tech.prototype.featuresNativeTextTracks = false;
CaptionsButton.prototype.update = oldCaptionsUpdate;
SubtitlesButton.prototype.update = oldSubsUpdate;
ChaptersButton.prototype.update = oldChaptersUpdate;
SubsCapsButton.prototype.update = oldSubsCapsUpdate;
player.dispose();
});